本案例演示了如何在 Rust 终端应用中,利用 ratatui-kit 实现多页面路由功能。用户可以在不同页面间自由切换,如首页、计数器、Markdown 阅读器和文本输入页面,每个页面都具备独立的交互体验。通过本示例,你可以学习到 RouterProvider 路由组件的用法、页面跳转、事件处理以及如何构建结构化的终端多页面应用。
use ratatui::{
style::{Style, Stylize},
text::Line,
};
use ratatui_kit::{
crossterm::event::KeyEvent,
ratatui::{self, layout::Direction},
};
use ratatui_kit::{
crossterm::event::{Event, KeyCode, KeyEventKind},
prelude::*,
ratatui::layout::Constraint,
};
use std::fs;
#[tokio::main]
async fn main() {
let routes = routes! {
"/" => HomePage,
"/counter" => CounterPage,
"/markdown" => MarkdownReader,
"/input" => InputPage,
};
element!(RouterProvider(
routes:routes,
index_path:"/",
))
.fullscreen()
.await
.expect("Failed to run the application");
}
#[component]
fn HomePage(mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
let mut navigate = hooks.use_navigate();
hooks.use_events(move |event| {
if let Event::Key(key_event) = event {
if key_event.kind == KeyEventKind::Press {
match key_event.code {
KeyCode::Char('1') => navigate.push("/counter"),
KeyCode::Char('2') => navigate.push("/markdown"),
KeyCode::Char('3') => navigate.push("/input"),
_ => {}
}
}
}
});
element!(
Fragment{
Border(
style:Style::default().blue(),
height:Constraint::Length(8),
top_title:Line::from("🏠 Home - 多页面路由示例").centered().bold(),
){
$Line::from("1. 计数器页面 (Counter)")
$Line::from("2. Markdown 阅读器")
$Line::from("3. 文本输入页面")
}
}
)
}
#[component]
fn CounterPage(mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
let mut state = hooks.use_state(|| 0);
let mut navigate = hooks.use_navigate();
hooks.use_future(async move {
loop {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
state += 1;
}
});
hooks.use_events(move |event| {
if let Event::Key(key_event) = event {
if key_event.kind == KeyEventKind::Press && key_event.code == KeyCode::Esc {
navigate.back();
}
}
});
element!(
Border(
style:Style::default().green(),
height:Constraint::Length(5),
top_title:Line::from("计数器页面 (ESC 返回)").centered(),
){
$Line::styled(
format!("Counter: {state}"),
Style::default().fg(ratatui::style::Color::Green).bold(),
).centered().bold().underlined()
}
)
}
#[component]
fn MarkdownReader(mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
// 读取 README.md 内容
let lines = hooks.use_memo(
|| {
let content = fs::read_to_string("README.md")
.unwrap_or_else(|_| "无法读取 README.md".to_string());
content.lines().map(|l| l.to_string()).collect::<Vec<_>>()
},
(),
);
let mut navigate = hooks.use_navigate();
let scroll_view_state = hooks.use_state(ScrollViewState::default);
hooks.use_local_events(move |event| match event {
Event::Key(KeyEvent {
kind: KeyEventKind::Press,
code: KeyCode::Esc,
..
}) => {
navigate.back();
}
_ => {
scroll_view_state.write().handle_event(&event);
}
});
// 简单 markdown 渲染:标题高亮,其余普通文本
let rendered: Vec<Line> = lines
.into_iter()
.map(|line| {
if line.starts_with("# ") {
Line::styled(line, Style::default().yellow().bold())
} else if line.starts_with("## ") {
Line::styled(line, Style::default().green().bold())
} else if line.starts_with("### ") {
Line::styled(line, Style::default().cyan())
} else {
Line::from(line)
}
})
.collect();
// 渲染每一行为 AnyElement
let rendered_elements: Vec<AnyElement> = rendered
.into_iter()
.map(|line| {
element!(View(height:Constraint::Length(1)){
$line
})
.into_any()
})
.collect();
element!(
View(
flex_direction:ratatui::layout::Direction::Vertical,
gap:1,
){
Border(
border_style:Style::default().blue(),
top_title:Some(Line::from("Markdown 阅读器 (ESC 返回)").centered()),
bottom_title:Some(Line::from("上下/翻页滚动,Ctrl+C 退出").centered()),
){
ScrollView(
flex_direction:Direction::Vertical,
scroll_view_state: scroll_view_state.get(),
){
#(rendered_elements)
}
}
}
)
}
#[component]
fn InputPage(mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
let mut value = hooks.use_state(String::new);
let mut navigate = hooks.use_navigate();
hooks.use_events(move |event| {
if let Event::Key(key_event) = event {
if key_event.kind == KeyEventKind::Press && key_event.code == KeyCode::Esc {
navigate.back();
}
}
});
element!(
Border(
style:Style::default().cyan(),
height:Constraint::Length(6),
top_title:Line::from("文本输入页面 (ESC 返回)").centered(),
){
TextArea(
value: value.read().to_string(),
is_focus: true,
on_change: move |new_value: String| {
value.set(new_value);
},
multiline: true,
cursor_style: Style::default().on_cyan(),
placeholder: Some("请输入内容...".to_string()),
placeholder_style: Style::default().cyan(),
)
}
)
}
运行结果如下: