use envision::component::{
Button, ButtonState, Checkbox, CheckboxMessage, CheckboxState, ProgressBar, ProgressBarState,
SelectableList, SelectableListMessage, SelectableListState,
};
use envision::prelude::*;
use ratatui::layout::{Alignment, Constraint, Layout};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Paragraph};
struct ThemedApp;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
enum ActiveTheme {
#[default]
Default,
Nord,
Dracula,
SolarizedDark,
GruvboxDark,
CatppuccinMocha,
}
impl ActiveTheme {
fn name(&self) -> &'static str {
match self {
ActiveTheme::Default => "Default",
ActiveTheme::Nord => "Nord",
ActiveTheme::Dracula => "Dracula",
ActiveTheme::SolarizedDark => "Solarized Dark",
ActiveTheme::GruvboxDark => "Gruvbox Dark",
ActiveTheme::CatppuccinMocha => "Catppuccin Mocha",
}
}
fn next(&self) -> Self {
match self {
ActiveTheme::Default => ActiveTheme::Nord,
ActiveTheme::Nord => ActiveTheme::Dracula,
ActiveTheme::Dracula => ActiveTheme::SolarizedDark,
ActiveTheme::SolarizedDark => ActiveTheme::GruvboxDark,
ActiveTheme::GruvboxDark => ActiveTheme::CatppuccinMocha,
ActiveTheme::CatppuccinMocha => ActiveTheme::Default,
}
}
}
#[derive(Clone)]
struct State {
active_theme: ActiveTheme,
button_state: ButtonState,
checkbox_state: CheckboxState,
progress_state: ProgressBarState,
list_state: SelectableListState<String>,
}
impl Default for State {
fn default() -> Self {
let items = vec![
"First item".to_string(),
"Second item".to_string(),
"Third item".to_string(),
"Fourth item".to_string(),
];
let mut list_state = SelectableListState::with_items(items);
SelectableList::<String>::set_focused(&mut list_state, true);
list_state.select(Some(0));
let mut button_state = ButtonState::new("Click Me");
Button::set_focused(&mut button_state, true);
Self {
active_theme: ActiveTheme::default(),
button_state,
checkbox_state: CheckboxState::new("Enable feature"),
progress_state: ProgressBarState::with_progress(0.65),
list_state,
}
}
}
#[derive(Clone, Debug)]
enum Msg {
ToggleTheme,
ButtonPressed,
CheckboxToggled,
IncreaseProgress,
DecreaseProgress,
NextItem,
PrevItem,
Quit,
}
impl App for ThemedApp {
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::ToggleTheme => {
state.active_theme = state.active_theme.next();
}
Msg::ButtonPressed => {
}
Msg::CheckboxToggled => {
Checkbox::update(&mut state.checkbox_state, CheckboxMessage::Toggle);
}
Msg::IncreaseProgress => {
let current = state.progress_state.progress();
state.progress_state.set_progress((current + 0.1).min(1.0));
}
Msg::DecreaseProgress => {
let current = state.progress_state.progress();
state.progress_state.set_progress((current - 0.1).max(0.0));
}
Msg::NextItem => {
SelectableList::<String>::update(
&mut state.list_state,
SelectableListMessage::Down,
);
}
Msg::PrevItem => {
SelectableList::<String>::update(&mut state.list_state, SelectableListMessage::Up);
}
Msg::Quit => {
return Command::quit();
}
}
Command::none()
}
fn view(state: &State, frame: &mut Frame) {
let theme = match state.active_theme {
ActiveTheme::Default => Theme::default(),
ActiveTheme::Nord => Theme::nord(),
ActiveTheme::Dracula => Theme::dracula(),
ActiveTheme::SolarizedDark => Theme::solarized_dark(),
ActiveTheme::GruvboxDark => Theme::gruvbox_dark(),
ActiveTheme::CatppuccinMocha => Theme::catppuccin_mocha(),
};
let area = frame.area();
let chunks = Layout::vertical([
Constraint::Length(3), Constraint::Length(3), Constraint::Length(4), Constraint::Length(3), Constraint::Min(5), Constraint::Length(3), ])
.split(area);
let title_style = Style::default()
.fg(theme.primary)
.add_modifier(Modifier::BOLD);
let title = Paragraph::new("Themed App - Envision Theming Demo")
.style(title_style)
.alignment(Alignment::Center)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(theme.border_style()),
);
frame.render_widget(title, chunks[0]);
let theme_indicator = Paragraph::new(Line::from(vec![
Span::raw("Current Theme: "),
Span::styled(state.active_theme.name(), theme.focused_bold_style()),
Span::raw(" | Press "),
Span::styled("[T]", theme.focused_style()),
Span::raw(" to toggle"),
]))
.alignment(Alignment::Center)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(theme.border_style())
.title("Theme"),
);
frame.render_widget(theme_indicator, chunks[1]);
let button_checkbox_chunks =
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(chunks[2]);
Button::view(
&state.button_state,
frame,
button_checkbox_chunks[0],
&theme,
&ViewContext::default(),
);
Checkbox::view(
&state.checkbox_state,
frame,
button_checkbox_chunks[1],
&theme,
&ViewContext::default(),
);
ProgressBar::view(
&state.progress_state,
frame,
chunks[3],
&theme,
&ViewContext::default(),
);
let list_area = chunks[4];
let is_list_focused = SelectableList::<String>::is_focused(&state.list_state);
let list_block = Block::default()
.borders(Borders::ALL)
.border_style(if is_list_focused {
theme.focused_border_style()
} else {
theme.border_style()
})
.title("Items");
let inner_area = list_block.inner(list_area);
frame.render_widget(list_block, list_area);
SelectableList::view(
&state.list_state,
frame,
inner_area,
&theme,
&ViewContext::default(),
);
let controls = Paragraph::new(Line::from(vec![
Span::styled("[T]", theme.info_style()),
Span::raw(" Theme "),
Span::styled("[Space]", theme.info_style()),
Span::raw(" Toggle "),
Span::styled("[+/-]", theme.info_style()),
Span::raw(" Progress "),
Span::styled("[Up/Dn]", theme.info_style()),
Span::raw(" Navigate "),
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, chunks[5]);
}
fn handle_event(event: &Event) -> Option<Msg> {
use crossterm::event::KeyCode;
if let Some(key) = event.as_key() {
match key.code {
KeyCode::Char('t') | KeyCode::Char('T') => Some(Msg::ToggleTheme),
KeyCode::Char(' ') => Some(Msg::CheckboxToggled),
KeyCode::Char('+') | KeyCode::Char('=') => Some(Msg::IncreaseProgress),
KeyCode::Char('-') => Some(Msg::DecreaseProgress),
KeyCode::Up | KeyCode::Char('k') => Some(Msg::PrevItem),
KeyCode::Down | KeyCode::Char('j') => Some(Msg::NextItem),
KeyCode::Enter => Some(Msg::ButtonPressed),
KeyCode::Char('q') | KeyCode::Char('Q') | KeyCode::Esc => Some(Msg::Quit),
_ => None,
}
} else {
None
}
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut vt = Runtime::<ThemedApp, _>::virtual_terminal(60, 24)?;
println!("=== Themed App Demo ===\n");
println!("This example demonstrates Envision's theming system.\n");
vt.tick()?;
println!("Default Theme:");
println!("{}\n", vt.display_ansi());
vt.dispatch(Msg::ToggleTheme);
vt.tick()?;
println!("Nord Theme:");
println!("{}\n", vt.display_ansi());
vt.dispatch(Msg::CheckboxToggled);
vt.dispatch(Msg::IncreaseProgress);
vt.dispatch(Msg::NextItem);
vt.tick()?;
println!("Nord Theme (after interactions):");
println!("{}\n", vt.display_ansi());
for _ in 0..4 {
vt.dispatch(Msg::ToggleTheme);
vt.tick()?;
println!("{} Theme:", vt.state().active_theme.name());
println!("{}\n", vt.display_ansi());
}
println!("=== Theme Comparison ===");
println!("Default: Yellow focus, DarkGray disabled, Cyan primary");
println!("Nord: Light Blue focus (#88C0D0), Muted gray disabled, Dark blue primary");
println!("Dracula: Purple focus (#BD93F9), Comment gray disabled, Cyan primary");
println!("Solarized Dark: Blue focus (#268BD2), Base01 disabled, Blue primary");
println!("Gruvbox Dark: Yellow focus (#FABD2F), Gray disabled, Aqua primary");
println!("Catppuccin: Lavender focus (#B4BEFE), Surface2 disabled, Blue primary");
Ok(())
}