use envision::component::code_block::highlight::Language;
use envision::component::{
ButtonState, CheckboxState, CodeBlock, CodeBlockState, Column, CommandPalette,
CommandPaletteState, Component, Dialog, DialogMessage, DialogOutput, DialogState, FocusManager,
Gauge, GaugeState, GaugeVariant, Heatmap, HeatmapState, InputFieldState, MenuItem, MenuOutput,
MenuState, PaletteItem, ProgressBarState, RadioGroupState, SelectableListState, Sparkline,
SparklineState, Spinner, SpinnerMessage, SpinnerState, Table, TableOutput, TableRow,
TableState, Tabs, TabsState, Timeline, TimelineEvent, TimelineSpan, TimelineState, Toast,
ToastMessage, ToastState,
};
use envision::prelude::*;
use ratatui::layout::{Alignment, Constraint, Layout};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Paragraph};
#[derive(Clone, Debug, PartialEq, Eq)]
enum FocusId {
Menu,
Tabs,
Input,
Checkbox,
Radio,
SubmitButton,
List,
Table,
Progress,
Heatmap,
Timeline,
CommandPalette,
CodeBlock,
}
#[derive(Clone, Debug)]
struct UserRow {
name: String,
role: String,
status: String,
}
impl TableRow for UserRow {
fn cells(&self) -> Vec<String> {
vec![self.name.clone(), self.role.clone(), self.status.clone()]
}
}
struct ShowcaseApp;
#[derive(Clone, Debug, PartialEq, Eq)]
enum Panel {
Form,
Data,
Status,
Viz,
}
impl std::fmt::Display for Panel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Panel::Form => write!(f, "Form"),
Panel::Data => write!(f, "Data"),
Panel::Status => write!(f, "Status"),
Panel::Viz => write!(f, "Viz"),
}
}
}
#[derive(Clone)]
struct State {
focus: FocusManager<FocusId>,
tabs: TabsState<Panel>,
menu: MenuState,
input: InputFieldState,
checkbox: CheckboxState,
radio: RadioGroupState<String>,
submit_button: ButtonState,
list: SelectableListState<String>,
table: TableState<UserRow>,
progress: ProgressBarState,
spinner: SpinnerState,
toast: ToastState,
sparkline: SparklineState,
gauge: GaugeState,
heatmap: HeatmapState,
timeline: TimelineState,
command_palette: CommandPaletteState,
code_block: CodeBlockState,
dialog: DialogState,
submission_count: u32,
}
impl Default for State {
fn default() -> Self {
let mut focus = FocusManager::with_initial_focus(vec![
FocusId::Menu,
FocusId::Tabs,
FocusId::Input,
FocusId::Checkbox,
FocusId::Radio,
FocusId::SubmitButton,
FocusId::List,
FocusId::Table,
FocusId::Progress,
FocusId::Heatmap,
FocusId::Timeline,
FocusId::CommandPalette,
FocusId::CodeBlock,
]);
focus.focus(&FocusId::Tabs);
let tabs = TabsState::new(vec![Panel::Form, Panel::Data, Panel::Status, Panel::Viz]);
let menu = MenuState::new(vec![
MenuItem::new("File"),
MenuItem::new("Edit"),
MenuItem::new("Help"),
]);
let mut input = InputFieldState::new();
input.set_placeholder("Enter your name...");
let checkbox = CheckboxState::new("Subscribe to newsletter");
let radio = RadioGroupState::new(vec![
"Standard".to_string(),
"Premium".to_string(),
"Enterprise".to_string(),
]);
let submit_button = ButtonState::new("Submit");
let list = SelectableListState::with_items(vec![
"Alice Johnson".to_string(),
"Bob Smith".to_string(),
"Carol Williams".to_string(),
"David Brown".to_string(),
"Eve Davis".to_string(),
]);
let rows = vec![
UserRow {
name: "Alice".to_string(),
role: "Admin".to_string(),
status: "Active".to_string(),
},
UserRow {
name: "Bob".to_string(),
role: "Editor".to_string(),
status: "Active".to_string(),
},
UserRow {
name: "Carol".to_string(),
role: "Viewer".to_string(),
status: "Inactive".to_string(),
},
UserRow {
name: "David".to_string(),
role: "Admin".to_string(),
status: "Active".to_string(),
},
];
let columns = vec![
Column::new("Name", Constraint::Length(15)),
Column::new("Role", Constraint::Length(12)),
Column::new("Status", Constraint::Length(10)),
];
let table = TableState::new(rows, columns);
let progress = ProgressBarState::with_progress(0.35);
let mut spinner = SpinnerState::new();
spinner.set_label(Some("Loading data...".to_string()));
let toast = ToastState::with_max_visible(3);
let sparkline =
SparklineState::with_data(vec![2, 5, 8, 12, 7, 4, 9, 15, 11, 6, 3, 8, 10, 14, 9, 5])
.with_title("Request Rate");
let gauge = GaugeState::new(73.0, 100.0)
.with_label("CPU Usage")
.with_units("%")
.with_variant(GaugeVariant::Full);
let heatmap = HeatmapState::with_data(vec![
vec![1.0, 3.0, 5.0, 2.0, 7.0],
vec![4.0, 6.0, 2.0, 8.0, 3.0],
vec![2.0, 1.0, 9.0, 4.0, 6.0],
])
.with_row_labels(vec!["Mon".into(), "Tue".into(), "Wed".into()])
.with_col_labels(vec![
"00:00".into(),
"06:00".into(),
"12:00".into(),
"18:00".into(),
"24:00".into(),
])
.with_title("Error Rate by Day/Hour");
let timeline = TimelineState::new()
.with_events(vec![
TimelineEvent::new("e1", 100.0, "Deploy v2.1"),
TimelineEvent::new("e2", 450.0, "Alert fired"),
TimelineEvent::new("e3", 800.0, "Resolved"),
])
.with_spans(vec![
TimelineSpan::new("s1", 100.0, 300.0, "Build"),
TimelineSpan::new("s2", 300.0, 700.0, "Test"),
TimelineSpan::new("s3", 700.0, 900.0, "Deploy"),
])
.with_view_range(0.0, 1000.0)
.with_title("CI/CD Pipeline");
let command_palette = CommandPaletteState::new(vec![
PaletteItem::new("open", "Open File"),
PaletteItem::new("save", "Save File"),
PaletteItem::new("quit", "Quit Application"),
PaletteItem::new("find", "Find in Files"),
PaletteItem::new("replace", "Find and Replace"),
PaletteItem::new("settings", "Open Settings"),
])
.with_title("Command Palette")
.with_visible(true);
let code_block = CodeBlockState::new()
.with_code(
"fn main() {\n let data = vec![1, 2, 3];\n for item in &data {\n println!(\"{item}\");\n }\n}",
)
.with_language(Language::Rust)
.with_line_numbers(true)
.with_title("Example Code");
let dialog = DialogState::confirm("Confirm Submission", "Submit the form?");
Self {
focus,
tabs,
menu,
input,
checkbox,
radio,
submit_button,
list,
table,
progress,
spinner,
toast,
sparkline,
gauge,
heatmap,
timeline,
command_palette,
code_block,
dialog,
submission_count: 0,
}
}
}
#[derive(Clone, Debug)]
enum Msg {
FocusNext,
FocusPrev,
ComponentEvent(Event),
SpinnerTick,
ToastTick,
Quit,
}
impl App for ShowcaseApp {
type State = State;
type Message = Msg;
fn init() -> (State, Command<Msg>) {
(State::default(), Command::none())
}
fn update(state: &mut State, msg: Msg) -> Command<Msg> {
match msg {
Msg::Quit => return Command::quit(),
Msg::FocusNext => {
state.focus.focus_next();
sync_focus(state);
}
Msg::FocusPrev => {
state.focus.focus_prev();
sync_focus(state);
}
Msg::ComponentEvent(event) => {
if state.dialog.is_visible() {
if let Some(output) = state.dialog.dispatch_event(&event) {
handle_dialog_output(state, output);
}
return Command::none();
}
let focused = state.focus.focused().cloned();
match focused {
Some(FocusId::Menu) => {
if let Some(MenuOutput::Selected(idx)) = state.menu.dispatch_event(&event) {
let label = state.menu.items()[idx].label();
push_toast(
&mut state.toast,
format!("Menu: {label} clicked"),
envision::component::ToastLevel::Info,
);
}
}
Some(FocusId::Tabs) => {
state.tabs.dispatch_event(&event);
}
Some(FocusId::Input) => {
state.input.dispatch_event(&event);
}
Some(FocusId::Checkbox) => {
state.checkbox.dispatch_event(&event);
}
Some(FocusId::Radio) => {
state.radio.dispatch_event(&event);
}
Some(FocusId::SubmitButton) => {
if state.submit_button.dispatch_event(&event).is_some() {
Dialog::update(&mut state.dialog, DialogMessage::Open);
}
}
Some(FocusId::List) => {
state.list.dispatch_event(&event);
}
Some(FocusId::Table) => {
if let Some(TableOutput::Selected(row)) = state.table.dispatch_event(&event)
{
push_toast(
&mut state.toast,
format!("Selected user: {}", row.name),
envision::component::ToastLevel::Info,
);
}
}
Some(FocusId::Progress) => {
}
Some(FocusId::Heatmap) => {
state.heatmap.dispatch_event(&event);
}
Some(FocusId::Timeline) => {
state.timeline.dispatch_event(&event);
}
Some(FocusId::CommandPalette) => {
state.command_palette.dispatch_event(&event);
}
Some(FocusId::CodeBlock) => {
state.code_block.dispatch_event(&event);
}
None => {}
}
}
Msg::SpinnerTick => {
Spinner::update(&mut state.spinner, SpinnerMessage::Tick);
}
Msg::ToastTick => {
Toast::update(&mut state.toast, ToastMessage::Tick(100));
}
}
Command::none()
}
fn view(state: &State, frame: &mut Frame) {
let theme = Theme::default();
let area = frame.area();
let main_chunks = Layout::vertical([
Constraint::Length(1), Constraint::Length(3), Constraint::Min(10), Constraint::Length(3), ])
.split(area);
envision::component::Menu::view(
&state.menu,
frame,
main_chunks[0],
&theme,
&ViewContext::default(),
);
Tabs::view(
&state.tabs,
frame,
main_chunks[1],
&theme,
&ViewContext::default(),
);
let content_area = main_chunks[2];
match state.tabs.selected_item() {
Some(Panel::Form) => render_form_panel(state, frame, content_area, &theme),
Some(Panel::Data) => render_data_panel(state, frame, content_area, &theme),
Some(Panel::Status) => render_status_panel(state, frame, content_area, &theme),
Some(Panel::Viz) => render_viz_panel(state, frame, content_area, &theme),
None => {}
}
let controls = Paragraph::new(Line::from(vec![
Span::styled("[Tab]", theme.info_style()),
Span::raw(" Focus "),
Span::styled("[←→]", theme.info_style()),
Span::raw(" Navigate "),
Span::styled("[Enter]", theme.info_style()),
Span::raw(" Confirm "),
Span::styled("[Q]", theme.error_style()),
Span::raw(" Quit"),
]))
.alignment(Alignment::Center)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(theme.border_style())
.title("Controls"),
);
frame.render_widget(controls, main_chunks[3]);
if state.dialog.is_visible() {
let dialog_area = centered_rect(40, 8, area);
Dialog::view(
&state.dialog,
frame,
dialog_area,
&theme,
&ViewContext::default(),
);
}
}
fn handle_event(event: &Event) -> Option<Msg> {
use crossterm::event::KeyCode;
if let Some(key) = event.as_key() {
match key.code {
KeyCode::Char('q') | KeyCode::Char('Q') | KeyCode::Esc => Some(Msg::Quit),
KeyCode::Tab => Some(Msg::FocusNext),
KeyCode::BackTab => Some(Msg::FocusPrev),
_ => Some(Msg::ComponentEvent(event.clone())),
}
} else {
None
}
}
}
fn handle_dialog_output(state: &mut State, output: DialogOutput) {
match output {
DialogOutput::ButtonPressed(id) if id == "ok" => {
state.submission_count += 1;
let count = state.submission_count;
push_toast(
&mut state.toast,
format!("Form submitted! (#{count})"),
envision::component::ToastLevel::Success,
);
}
DialogOutput::Closed => {}
_ => {}
}
}
fn push_toast(toast: &mut ToastState, message: String, level: envision::component::ToastLevel) {
Toast::update(
toast,
ToastMessage::Push {
message,
level,
duration_ms: Some(3000),
},
);
}
fn render_form_panel(state: &State, frame: &mut Frame, area: Rect, theme: &Theme) {
let block = Block::default()
.borders(Borders::ALL)
.border_style(theme.border_style())
.title("Form Panel");
let inner = block.inner(area);
frame.render_widget(block, area);
let chunks = Layout::vertical([
Constraint::Length(3), Constraint::Length(1), Constraint::Length(5), Constraint::Length(3), ])
.split(inner);
envision::component::InputField::view(
&state.input,
frame,
chunks[0],
theme,
&ViewContext::default(),
);
envision::component::Checkbox::view(
&state.checkbox,
frame,
chunks[1],
theme,
&ViewContext::default(),
);
envision::component::RadioGroup::view(
&state.radio,
frame,
chunks[2],
theme,
&ViewContext::default(),
);
envision::component::Button::view(
&state.submit_button,
frame,
chunks[3],
theme,
&ViewContext::default(),
);
}
fn render_data_panel(state: &State, frame: &mut Frame, area: Rect, theme: &Theme) {
let block = Block::default()
.borders(Borders::ALL)
.border_style(theme.border_style())
.title("Data Panel");
let inner = block.inner(area);
frame.render_widget(block, area);
let chunks =
Layout::horizontal([Constraint::Percentage(40), Constraint::Percentage(60)]).split(inner);
let list_block = Block::default()
.borders(Borders::ALL)
.border_style(theme.border_style())
.title("Users");
let list_inner = list_block.inner(chunks[0]);
frame.render_widget(list_block, chunks[0]);
envision::component::SelectableList::view(
&state.list,
frame,
list_inner,
theme,
&ViewContext::default(),
);
Table::view(
&state.table,
frame,
chunks[1],
theme,
&ViewContext::default(),
);
}
fn render_status_panel(state: &State, frame: &mut Frame, area: Rect, theme: &Theme) {
let block = Block::default()
.borders(Borders::ALL)
.border_style(theme.border_style())
.title("Status Panel");
let inner = block.inner(area);
frame.render_widget(block, area);
let chunks = Layout::vertical([
Constraint::Length(3), Constraint::Length(1), Constraint::Min(3), ])
.split(inner);
envision::component::ProgressBar::view(
&state.progress,
frame,
chunks[0],
theme,
&ViewContext::default(),
);
Spinner::view(
&state.spinner,
frame,
chunks[1],
theme,
&ViewContext::default(),
);
Toast::view(
&state.toast,
frame,
chunks[2],
theme,
&ViewContext::default(),
);
}
fn render_viz_panel(state: &State, frame: &mut Frame, area: Rect, theme: &Theme) {
let block = Block::default()
.borders(Borders::ALL)
.border_style(theme.border_style())
.title("Visualization Panel");
let inner = block.inner(area);
frame.render_widget(block, area);
let rows = Layout::vertical([
Constraint::Length(5), Constraint::Length(7), Constraint::Length(8), Constraint::Min(6), ])
.split(inner);
let top_cols =
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]).split(rows[0]);
Sparkline::view(
&state.sparkline,
frame,
top_cols[0],
theme,
&ViewContext::default(),
);
Gauge::view(
&state.gauge,
frame,
top_cols[1],
theme,
&ViewContext::default(),
);
Heatmap::view(
&state.heatmap,
frame,
rows[1],
theme,
&ViewContext::default(),
);
Timeline::view(
&state.timeline,
frame,
rows[2],
theme,
&ViewContext::default(),
);
let bottom_cols =
Layout::horizontal([Constraint::Percentage(40), Constraint::Percentage(60)]).split(rows[3]);
CommandPalette::view(
&state.command_palette,
frame,
bottom_cols[0],
theme,
&ViewContext::default(),
);
CodeBlock::view(
&state.code_block,
frame,
bottom_cols[1],
theme,
&ViewContext::default(),
);
}
fn sync_focus(state: &mut State) {
let focused = state.focus.focused().cloned();
state.menu.set_focused(focused == Some(FocusId::Menu));
state.tabs.set_focused(focused == Some(FocusId::Tabs));
state.input.set_focused(focused == Some(FocusId::Input));
state
.checkbox
.set_focused(focused == Some(FocusId::Checkbox));
state.radio.set_focused(focused == Some(FocusId::Radio));
state
.submit_button
.set_focused(focused == Some(FocusId::SubmitButton));
state.list.set_focused(focused == Some(FocusId::List));
state.table.set_focused(focused == Some(FocusId::Table));
state.heatmap.set_focused(focused == Some(FocusId::Heatmap));
state
.timeline
.set_focused(focused == Some(FocusId::Timeline));
state
.command_palette
.set_focused(focused == Some(FocusId::CommandPalette));
state
.code_block
.set_focused(focused == Some(FocusId::CodeBlock));
}
fn centered_rect(width: u16, height: u16, area: Rect) -> Rect {
let x = area.x + area.width.saturating_sub(width) / 2;
let y = area.y + area.height.saturating_sub(height) / 2;
Rect::new(x, y, width.min(area.width), height.min(area.height))
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut vt = Runtime::<ShowcaseApp, _>::virtual_terminal(80, 40)?;
println!("=== Component Showcase ===\n");
println!("Demonstrating 18 Envision components with simplified event routing.\n");
vt.tick()?;
println!("--- Initial State (Form Panel) ---");
println!("{}\n", vt.display_ansi());
vt.dispatch(Msg::ComponentEvent(Event::key(
crossterm::event::KeyCode::Right,
)));
vt.dispatch(Msg::ComponentEvent(Event::key(
crossterm::event::KeyCode::Enter,
)));
vt.dispatch(Msg::FocusNext);
vt.dispatch(Msg::ComponentEvent(Event::key(
crossterm::event::KeyCode::Right,
)));
vt.tick()?;
println!("--- Data Panel ---");
println!("{}\n", vt.display_ansi());
vt.dispatch(Msg::FocusNext); vt.dispatch(Msg::FocusNext); vt.dispatch(Msg::FocusNext); vt.dispatch(Msg::FocusNext); vt.dispatch(Msg::FocusNext); vt.dispatch(Msg::ComponentEvent(Event::key(
crossterm::event::KeyCode::Down,
)));
vt.dispatch(Msg::ComponentEvent(Event::key(
crossterm::event::KeyCode::Down,
)));
vt.dispatch(Msg::FocusNext); vt.dispatch(Msg::ComponentEvent(Event::key(
crossterm::event::KeyCode::Down,
)));
vt.dispatch(Msg::ComponentEvent(Event::key(
crossterm::event::KeyCode::Enter,
)));
vt.tick()?;
println!("--- Data Panel (after navigation) ---");
println!("{}\n", vt.display_ansi());
vt.dispatch(Msg::FocusNext); vt.dispatch(Msg::FocusNext); vt.dispatch(Msg::FocusNext); vt.dispatch(Msg::ComponentEvent(Event::key(
crossterm::event::KeyCode::Right,
)));
vt.dispatch(Msg::SpinnerTick);
vt.dispatch(Msg::ToastTick);
vt.tick()?;
println!("--- Status Panel ---");
println!("{}\n", vt.display_ansi());
vt.dispatch(Msg::ComponentEvent(Event::key(
crossterm::event::KeyCode::Left,
)));
vt.dispatch(Msg::ComponentEvent(Event::key(
crossterm::event::KeyCode::Left,
)));
vt.dispatch(Msg::FocusNext); vt.dispatch(Msg::FocusNext); vt.dispatch(Msg::FocusNext); vt.dispatch(Msg::FocusNext); vt.dispatch(Msg::ComponentEvent(Event::key(
crossterm::event::KeyCode::Enter,
)));
vt.tick()?;
println!("--- Form Panel with Dialog Overlay ---");
println!("{}\n", vt.display_ansi());
vt.dispatch(Msg::ComponentEvent(Event::key(
crossterm::event::KeyCode::Tab,
)));
vt.dispatch(Msg::ComponentEvent(Event::key(
crossterm::event::KeyCode::Enter,
)));
vt.tick()?;
println!("--- After Submission (toast notification) ---");
println!("{}\n", vt.display_ansi());
vt.dispatch(Msg::FocusNext); vt.dispatch(Msg::FocusNext); vt.dispatch(Msg::FocusNext); vt.dispatch(Msg::FocusNext); vt.dispatch(Msg::FocusNext); vt.dispatch(Msg::FocusNext); vt.dispatch(Msg::FocusNext); vt.dispatch(Msg::FocusNext); vt.dispatch(Msg::FocusNext); vt.dispatch(Msg::ComponentEvent(Event::key(
crossterm::event::KeyCode::Right,
)));
vt.dispatch(Msg::ComponentEvent(Event::key(
crossterm::event::KeyCode::Right,
)));
vt.dispatch(Msg::ComponentEvent(Event::key(
crossterm::event::KeyCode::Right,
)));
vt.tick()?;
println!("--- Viz Panel (Sparkline, Gauge, Heatmap, Timeline, CommandPalette, CodeBlock) ---");
println!("{}\n", vt.display_ansi());
println!("=== Showcase Complete ===");
println!("This example demonstrated Menu, Tabs, InputField, Checkbox,");
println!("RadioGroup, Button, SelectableList, Table, ProgressBar,");
println!("Spinner, Toast, Dialog, Sparkline, Gauge, Heatmap,");
println!("Timeline, CommandPalette, and CodeBlock components working together.");
println!("\nKey patterns shown:");
println!(" - Msg enum: 6 variants (was 30+)");
println!(" - sync_focus: no turbofish needed");
println!(" - dispatch_event: routes events to focused component directly");
Ok(())
}