use color_eyre::Result;
use crossterm::event::KeyCode;
use ratatui::Frame;
use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph};
use ratflow::{Render, Runtime, create_interval, create_memo, on_key_press, run};
use reactive_graph::computed::Memo;
use reactive_graph::owner::{expect_context, provide_context};
use reactive_graph::signal::{WriteSignal, signal};
use reactive_graph::traits::{Set, Update};
use std::sync::Arc;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum View {
Menu,
Counter,
Input,
}
#[derive(Debug, Clone, Copy)]
struct Views {
view: WriteSignal<View>,
}
impl Views {
pub const fn new(view: WriteSignal<View>) -> Views {
Views { view }
}
pub fn goto(&self, view: View) {
self.view.set(view);
}
}
fn menu() -> impl Render {
let selected = {
let runtime = expect_context::<Runtime>();
let views = expect_context::<Views>();
let (selected, set_selected) = signal(0_usize);
on_key_press(move |key| match key.code {
KeyCode::Up => set_selected.update(|s| *s = (*s).saturating_sub(1)),
KeyCode::Down => set_selected.update(|s| *s = (*s + 1).min(3)),
KeyCode::Enter => {
match selected() {
0 => views.goto(View::Counter),
1 => views.goto(View::Input),
2 => runtime.quit(),
_ => (),
};
}
KeyCode::Char('q') => runtime.quit(),
_ => (),
});
selected
};
let menu_items = ["Counter", "Input", "Quit"];
let list = create_memo(move || {
let items: Vec<ListItem> = menu_items
.iter()
.enumerate()
.map(|(i, item)| {
let content = if i == selected() {
format!("> {item}")
} else {
format!(" {item}")
};
ListItem::new(content)
})
.collect();
List::new(items).block(
Block::default()
.borders(Borders::ALL)
.title("Menu (↑/↓: navigate, Enter: select, q: quit)"),
)
});
move |frame: &mut Frame| {
frame.render_widget(list(), frame.area());
}
}
fn counter() -> impl Render {
let count = {
let (count, set_count) = signal(0);
create_interval(
move || set_count.update(|c| *c += 1),
Duration::from_secs(1),
);
count
};
let views = expect_context::<Views>();
let runtime = expect_context::<Runtime>();
on_key_press(move |key| match key.code {
KeyCode::Char('b') => views.goto(View::Menu),
KeyCode::Char('q') => runtime.quit(),
_ => (),
});
let paragraph = create_memo(move || {
let text = format!(
"Count: {}\n\nPress 'b' or Esc to go back\nPress 'q' to quit",
count()
);
Paragraph::new(text).block(Block::default().borders(Borders::ALL).title("Counter"))
});
move |frame: &mut Frame| {
frame.render_widget(paragraph(), frame.area());
}
}
fn input() -> impl Render {
let (text, history) = {
let views = expect_context::<Views>();
let (text, set_text) = signal(String::new());
let (history, set_history) = signal(Vec::new());
on_key_press(move |key| match key.code {
KeyCode::Char(c) => set_text.update(|text| text.push(c)),
KeyCode::Backspace => set_text.update(|text| {
text.pop();
}),
KeyCode::Enter if !text().is_empty() => {
set_history.update(|history| history.push(text().clone()));
set_text(String::new());
}
KeyCode::Esc => views.goto(View::Menu),
_ => (),
});
(text, history)
};
let paragraph = create_memo(move || {
use std::fmt::Write;
let mut content = String::from("Type to enter text, Enter to submit, Esc to go back\n\n");
let _ = write!(content, "Input: {}_\n\n", text());
content.push_str("History:\n");
for (i, item) in history().iter().enumerate() {
let _ = writeln!(content, " {}. {item}", i + 1);
}
Paragraph::new(content).block(Block::default().borders(Borders::ALL).title("Text Input"))
});
move |frame: &mut Frame| {
frame.render_widget(paragraph(), frame.area());
}
}
fn create_view(view: View) -> Arc<dyn Render> {
match view {
View::Menu => Arc::new(menu()),
View::Counter => Arc::new(counter()),
View::Input => Arc::new(input()),
}
}
fn app() -> impl Render {
let (view, set_view) = signal(View::Menu);
provide_context(Views::new(set_view));
let view = Memo::new_with_compare(
move |old: Option<&(View, Arc<dyn Render>)>| {
let view = view();
if let Some(pair) = old
&& pair.0 == view
{
pair.clone()
} else {
(view, create_view(view))
}
},
move |old, new| old.map(|e| e.0) != new.map(|e| e.0),
);
move |frame: &mut Frame| {
view().1.render(frame);
}
}
#[tokio::main]
async fn main() -> Result<()> {
color_eyre::install()?;
run(app).await?;
Ok(())
}