ratatui-interact Examples

March 30, 2026 · View on GitHub

Detailed code examples for each component in the ratatui-interact library.

Table of Contents


Progress Bar

use ratatui_interact::components::{Progress, ProgressStyle};

// From ratio (0.0 to 1.0)
let progress = Progress::new(0.75)
    .label("Downloading")
    .show_percentage(true);

// From step counts
let progress = Progress::from_steps(3, 10)
    .label("Processing")
    .show_steps(true);

// Different styles
let success = Progress::new(1.0).style(ProgressStyle::success());
let warning = Progress::new(0.9).style(ProgressStyle::warning());

Spinner

use ratatui_interact::components::{Spinner, SpinnerState, SpinnerStyle, SpinnerFrames};

// Create state (call tick() each frame to animate)
let mut state = SpinnerState::new();

// Simple spinner
let spinner = Spinner::new(&state);

// With label
let spinner = Spinner::new(&state)
    .label("Loading...");

// Different frame styles
let spinner = Spinner::new(&state)
    .frames(SpinnerFrames::Braille)
    .label("Processing");

// Custom color
let spinner = Spinner::new(&state)
    .color(Color::Green)
    .label("Success!");

// In your event loop, advance the animation
let frame_count = SpinnerFrames::Dots.frames().len();
state.tick_with_frames(frame_count);

// Or use state configured for specific frames
let mut state = SpinnerState::for_frames(SpinnerFrames::Moon);
state.tick_with_frames(SpinnerFrames::Moon.frames().len());

Available frame styles:

  • Dots - ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏ (default)
  • Braille - ⣾ ⣽ ⣻ ⢿ ⡿ ⣟ ⣯ ⣷
  • Line - | / - \
  • Circle - ◐ ◓ ◑ ◒
  • Arrow - ← ↖ ↑ ↗ → ↘ ↓ ↙
  • Clock - 🕐 🕑 🕒 ... (12 frames)
  • Moon - 🌑 🌒 🌓 🌔 🌕 🌖 🌗 🌘
  • And more: Box, Bounce, Grow, Ascii, Toggle

Style presets: SpinnerStyle::success(), warning(), error(), info(), minimal()


Animated Text

use ratatui_interact::components::{AnimatedText, AnimatedTextState, AnimatedTextStyle, AnimatedTextEffect};

// Create state (call tick() each frame to animate)
let mut state = AnimatedTextState::new();

// Pulse effect - entire text oscillates between two colors
let text = AnimatedText::new("Loading...", &state)
    .style(AnimatedTextStyle::pulse(Color::Cyan, Color::Blue));

// Wave effect - highlight travels back and forth
let text = AnimatedText::new("Processing data", &state)
    .style(AnimatedTextStyle::wave(Color::White, Color::Yellow).wave_width(5));

// Rainbow effect - colors cycle across characters
let text = AnimatedText::new("Welcome!", &state)
    .style(AnimatedTextStyle::rainbow());

// Gradient shift - smooth color gradient that moves
let text = AnimatedText::new("Smooth transition", &state)
    .style(AnimatedTextStyle::gradient_shift(Color::Green, Color::Cyan));

// Sparkle effect - random characters flash
let text = AnimatedText::new("Sparkling!", &state)
    .style(AnimatedTextStyle::sparkle(Color::White, Color::Yellow));

// In your event loop, advance the animation
state.tick_with_text_width(text_length);

Effects:

  • Pulse - Entire text oscillates between primary and secondary colors (default)
  • Wave - A highlighted region travels back and forth across the text
  • Rainbow - Colors cycle through a spectrum across each character
  • GradientShift - A smooth gradient that shifts over time
  • Sparkle - Random characters flash with the secondary color

Style presets: AnimatedTextStyle::success(), warning(), error(), info(), loading(), highlight()


Marquee Text

use ratatui_interact::components::{MarqueeText, MarqueeState, MarqueeStyle, MarqueeMode};

// Create state (call tick() each frame to animate)
let mut state = MarqueeState::new();

// Continuous scrolling (loops around)
let marquee = MarqueeText::new("This is a long message that scrolls continuously", &mut state)
    .style(MarqueeStyle::default().mode(MarqueeMode::Continuous));

// Bounce mode (scrolls back and forth) - great for file paths
let mut state = MarqueeState::new();
let marquee = MarqueeText::new("/home/user/very/long/path/to/file.rs", &mut state)
    .style(MarqueeStyle::file_path());

// Static mode (truncate with ellipsis)
let mut state = MarqueeState::new();
let marquee = MarqueeText::new("Long text truncated with ellipsis", &mut state)
    .style(MarqueeStyle::default().mode(MarqueeMode::Static));

// In your event loop, advance the animation
state.tick(text_width, viewport_width, &style);

Marquee modes:

  • Continuous - Text loops with a separator (default: " ")
  • Bounce - Text scrolls to end, pauses, then scrolls back
  • Static - No animation, just truncate with ellipsis

Style presets:

  • MarqueeStyle::file_path() - Cyan, bounce mode, longer pause
  • MarqueeStyle::status() - Yellow bold, continuous
  • MarqueeStyle::title() - Bold, bounce mode, long pause

Select (Dropdown)

use ratatui_interact::components::{Select, SelectState, SelectStyle, handle_select_key, handle_select_mouse};

let options = vec!["Red", "Green", "Blue", "Yellow"];
let mut state = SelectState::new(options.len());

// Pre-select an option
let mut state = SelectState::with_selected(options.len(), 1); // "Green"

// Render the select box
let select = Select::new(&options, &state)
    .label("Color")
    .placeholder("Choose a color...");
let click_region = select.render_stateful(frame, area);

// Render dropdown when open (must be rendered last to appear on top)
let mut dropdown_regions = Vec::new();
if state.is_open {
    dropdown_regions = select.render_dropdown(frame, area, screen_area);
}

// Handle keyboard (Enter/Space to open, Up/Down to navigate, Enter to select, Esc to close)
if let Some(action) = handle_select_key(&key_event, &mut state) {
    match action {
        SelectAction::Select(idx) => println!("Selected: {}", options[idx]),
        _ => {}
    }
}

// Handle mouse clicks
handle_select_mouse(&mouse_event, &mut state, area, &dropdown_regions);

Style presets:

  • SelectStyle::default() - Yellow highlight, checkmark indicator
  • SelectStyle::minimal() - Subtle yellow text highlight
  • SelectStyle::arrow() - Arrow indicator ()
  • SelectStyle::bracket() - Bracket indicator ([x])

Context Menu

use ratatui_interact::components::{
    ContextMenu, ContextMenuItem, ContextMenuState, ContextMenuStyle,
    handle_context_menu_key, handle_context_menu_mouse, is_context_menu_trigger,
};

// Create menu items with actions, separators, and submenus
let items = vec![
    ContextMenuItem::action("open", "Open").icon("📂").shortcut("Enter"),
    ContextMenuItem::action("edit", "Edit").icon("✏️").shortcut("E"),
    ContextMenuItem::separator(),
    ContextMenuItem::action("copy", "Copy").icon("📋").shortcut("Ctrl+C"),
    ContextMenuItem::action("paste", "Paste").icon("📄").enabled(false), // Disabled
    ContextMenuItem::separator(),
    ContextMenuItem::submenu("More", vec![
        ContextMenuItem::action("new_file", "New File").icon("📄"),
        ContextMenuItem::action("new_folder", "New Folder").icon("📁"),
    ]).icon("➕"),
    ContextMenuItem::separator(),
    ContextMenuItem::action("delete", "Delete").icon("🗑️").shortcut("Del"),
];

// Create state
let mut state = ContextMenuState::new();

// Open menu on right-click
if is_context_menu_trigger(&mouse_event) {
    state.open_at(mouse_event.column, mouse_event.row);
}

// Render the menu (must be rendered last to appear on top)
if state.is_open {
    let menu = ContextMenu::new(&items, &state)
        .style(ContextMenuStyle::default());
    let (menu_area, click_regions) = menu.render_stateful(frame, screen_area);
}

// Handle keyboard (Up/Down to navigate, Enter to select, Esc to close, Right for submenu)
if let Some(action) = handle_context_menu_key(&key_event, &mut state, &items) {
    match action {
        ContextMenuAction::Select(id) => println!("Selected: {}", id),
        ContextMenuAction::Close => println!("Menu closed"),
        _ => {}
    }
}

// Handle mouse clicks
handle_context_menu_mouse(&mouse_event, &mut state, menu_area, &click_regions);

Key bindings:

  • Up/Down: Navigate items (skips separators)
  • Enter/Space: Select item or open submenu
  • Right: Open submenu
  • Left/Esc: Close submenu or close menu
  • Home/End: Jump to first/last item

Style presets:

  • ContextMenuStyle::default() - Dark theme with blue highlight
  • ContextMenuStyle::light() - Light theme
  • ContextMenuStyle::minimal() - Simple style with reset background

use ratatui_interact::components::{
    Menu, MenuBar, MenuBarItem, MenuBarState, MenuBarStyle,
    handle_menu_bar_key, handle_menu_bar_mouse,
};

// Create menus with items, separators, shortcuts, and submenus
let menus = vec![
    Menu::new("File").items(vec![
        MenuBarItem::action("new", "New").shortcut("Ctrl+N"),
        MenuBarItem::action("open", "Open...").shortcut("Ctrl+O"),
        MenuBarItem::separator(),
        MenuBarItem::action("save", "Save").shortcut("Ctrl+S"),
        MenuBarItem::submenu("Export", vec![
            MenuBarItem::action("export_pdf", "Export as PDF"),
            MenuBarItem::action("export_html", "Export as HTML"),
        ]),
        MenuBarItem::separator(),
        MenuBarItem::action("quit", "Quit").shortcut("Ctrl+Q"),
    ]),
    Menu::new("Edit").items(vec![
        MenuBarItem::action("undo", "Undo").shortcut("Ctrl+Z"),
        MenuBarItem::action("redo", "Redo").shortcut("Ctrl+Y"),
        MenuBarItem::separator(),
        MenuBarItem::action("cut", "Cut").shortcut("Ctrl+X"),
        MenuBarItem::action("copy", "Copy").shortcut("Ctrl+C"),
        MenuBarItem::action("paste", "Paste").shortcut("Ctrl+V").enabled(false), // Disabled
    ]),
];

// Create state
let mut state = MenuBarState::new();
state.focused = true;

// Render the menu bar
let menu_bar = MenuBar::new(&menus, &state)
    .style(MenuBarStyle::default());
let (bar_area, dropdown_area, click_regions) = menu_bar.render_stateful(frame, area);

// Handle keyboard (arrows navigate, Enter selects, Esc closes)
if let Some(action) = handle_menu_bar_key(&key_event, &mut state, &menus) {
    match action {
        MenuBarAction::ItemSelect(id) => println!("Selected: {}", id),
        MenuBarAction::MenuOpen(idx) => println!("Menu {} opened", idx),
        MenuBarAction::MenuClose => println!("Menu closed"),
        _ => {}
    }
}

// Handle mouse (click to open, hover to switch menus)
handle_menu_bar_mouse(&mouse_event, &mut state, bar_area, dropdown_area, &click_regions, &menus);

Key bindings:

  • Left/Right: Navigate between menus
  • Up/Down: Navigate items in dropdown (opens menu if closed)
  • Enter/Space: Select item or toggle menu
  • Right (on submenu): Open submenu
  • Left/Esc: Close submenu or close menu
  • Home/End: Jump to first/last item

Style presets:

  • MenuBarStyle::default() - Dark theme
  • MenuBarStyle::light() - Light theme
  • MenuBarStyle::minimal() - Simple style with reset background

Mouse Pointer

use ratatui_interact::components::{MousePointer, MousePointerState, MousePointerStyle};

// Create state (disabled by default)
let mut state = MousePointerState::default();

// Enable and update position from mouse events
state.set_enabled(true);
state.update_position(mouse.column, mouse.row);

// Create pointer with custom style
let pointer = MousePointer::new(&state)
    .style(MousePointerStyle::crosshair());

// Render LAST to appear on top of other widgets
pointer.render(frame.buffer_mut());

// Toggle visibility
state.toggle();

Style presets:

  • MousePointerStyle::default() - Yellow block ()
  • MousePointerStyle::crosshair() - Cyan crosshair ()
  • MousePointerStyle::arrow() - White arrow ()
  • MousePointerStyle::dot() - Green dot ()
  • MousePointerStyle::plus() - Magenta plus (+)
  • MousePointerStyle::custom(symbol, color) - User-defined

Custom styling:

let style = MousePointerStyle::default()
    .symbol("◆")
    .fg(Color::Rgb(255, 128, 0))  // Orange
    .bg(Color::DarkGray);

List Picker

use ratatui_interact::components::{ListPicker, ListPickerState};
use ratatui::text::Line;

let items = vec!["Option A", "Option B", "Option C"];
let mut state = ListPickerState::new(items.len());

// Navigate
state.select_next();
state.select_prev();

// Custom rendering
let picker = ListPicker::new(&items, &state)
    .title("Select Option")
    .render_item(|item, _idx, selected| {
        vec![Line::from(item.to_string())]
    });

Tree View

use ratatui_interact::components::{TreeView, TreeViewState, TreeNode};

#[derive(Clone, Debug)]
struct Task { name: String, done: bool }

let nodes = vec![
    TreeNode::new("1", Task { name: "Build".into(), done: false })
        .with_children(vec![
            TreeNode::new("1.1", Task { name: "Compile".into(), done: true }),
            TreeNode::new("1.2", Task { name: "Link".into(), done: false }),
        ]),
];

let mut state = TreeViewState::new();
state.toggle_collapsed("1"); // Collapse/expand

let tree = TreeView::new(&nodes, &state)
    .render_item(|node, selected| {
        format!("[{}] {}", if node.data.done { "x" } else { " " }, node.data.name)
    });

Accordion

use ratatui_interact::components::{Accordion, AccordionState, AccordionMode};

// Single mode: only one section expanded at a time (FAQ-style)
let mut state = AccordionState::new(items.len())
    .with_mode(AccordionMode::Single);

// Multiple mode: any number can be expanded (settings-style)
let mut state = AccordionState::new(items.len())
    .with_mode(AccordionMode::Multiple)
    .with_expanded(vec!["section1".into()]);

// Toggle, expand, collapse
state.toggle("faq1");
state.expand("faq2");
state.collapse("faq1");

// Create accordion with custom renderers
let accordion = Accordion::new(&items, &state)
    .id_fn(|item, _| item.id.clone())
    .render_header(|item, _idx, is_focused| {
        Line::raw(item.title.clone())
    })
    .render_content(|item, _idx, area, buf| {
        let paragraph = Paragraph::new(item.content.as_str());
        paragraph.render(area, buf);
    });

use ratatui_interact::components::{
    Breadcrumb, BreadcrumbItem, BreadcrumbState, BreadcrumbStyle,
    handle_breadcrumb_key, handle_breadcrumb_mouse,
};

// Create breadcrumb items with optional icons
let items = vec![
    BreadcrumbItem::new("home", "Home").icon("🏠"),
    BreadcrumbItem::new("users", "Users"),
    BreadcrumbItem::new("profile", "Profile Settings"),
];

// Create state
let mut state = BreadcrumbState::new(items);
state.focused = true;

// Create breadcrumb with default style (uses " > " separator)
let breadcrumb = Breadcrumb::new(&state);
let click_regions = breadcrumb.render_stateful(area, buf);

// Different style presets:
// - BreadcrumbStyle::slash()   - " / " (Unix path style)
// - BreadcrumbStyle::chevron() - " › " (Unicode chevron)
// - BreadcrumbStyle::arrow()   - " → " (Unicode arrow)
// - BreadcrumbStyle::minimal() - Subdued colors

let breadcrumb = Breadcrumb::new(&state)
    .style(BreadcrumbStyle::chevron());

// Handle keyboard (arrows navigate, Enter activates, e expands ellipsis)
if let Some(action) = handle_breadcrumb_key(&key_event, &mut state) {
    match action {
        BreadcrumbAction::Navigate(id) => println!("Navigate to: {}", id),
        BreadcrumbAction::ExpandEllipsis => println!("Ellipsis toggled"),
    }
}

// Handle mouse clicks
handle_breadcrumb_mouse(&mouse_event, &mut state, &click_regions);

// Dynamic path manipulation
state.push(BreadcrumbItem::new("new_item", "New Item"));
state.pop();
state.clear();

Ellipsis collapsing: Long paths automatically collapse with ... (configurable threshold). Example: Home > ... > Settings > Profile when showing 7+ items.


Tab View

use ratatui_interact::components::{
    Tab, TabView, TabViewState, TabViewStyle, TabPosition,
    handle_tab_view_key, handle_tab_view_mouse,
};
use ratatui_interact::traits::ClickRegionRegistry;

// Create tabs with optional icons and badges
let tabs = vec![
    Tab::new("General").icon("⚙"),
    Tab::new("Network").icon("🌐").badge("3"),
    Tab::new("Security").icon("🔒"),
];

// Create state
let mut state = TabViewState::new(tabs.len());

// Create style (tabs on left side)
let style = TabViewStyle::left().tab_width(18);

// Create tab view with content renderer
let tab_view = TabView::new(&tabs, &state)
    .style(style)
    .content(|idx, area, buf| {
        let text = match idx {
            0 => "General settings content",
            1 => "Network configuration content",
            _ => "Security options content",
        };
        Paragraph::new(text).render(area, buf);
    });

// Render and register click regions
let mut registry: ClickRegionRegistry<TabViewAction> = ClickRegionRegistry::new();
tab_view.render_with_registry(area, buf, &mut registry);

// Handle keyboard (arrows navigate, Enter focuses content, Esc focuses tabs, 1-9 direct select)
handle_tab_view_key(&mut state, &key_event, style.position);

// Handle mouse clicks
handle_tab_view_mouse(&mut state, &registry, &mouse_event);

Style presets:

  • TabViewStyle::top() - Horizontal tabs above content (default)
  • TabViewStyle::bottom() - Horizontal tabs below content
  • TabViewStyle::left() - Vertical tabs on left side
  • TabViewStyle::right() - Vertical tabs on right side
  • TabViewStyle::minimal() - No borders, simple dividers

Split Pane

use ratatui_interact::components::{
    SplitPane, SplitPaneState, SplitPaneStyle, SplitPaneAction, Orientation,
    handle_split_pane_key, handle_split_pane_mouse,
};
use ratatui_interact::traits::ClickRegionRegistry;

// Create state with initial split percentage (50% = equal split)
let mut state = SplitPaneState::new(50);
state.divider_focused = true; // Enable keyboard resize

// Create split pane with horizontal orientation (left | right)
let split_pane = SplitPane::new(&state)
    .orientation(Orientation::Horizontal)
    .style(SplitPaneStyle::default())
    .min_percent(10)  // Minimum 10% for first pane
    .max_percent(90); // Maximum 90% for first pane

// Calculate areas for manual rendering
let (first_area, divider_area, second_area) = split_pane.calculate_areas(area);

// Register click regions for mouse support
let mut registry: ClickRegionRegistry<SplitPaneAction> = ClickRegionRegistry::new();
registry.register(first_area, SplitPaneAction::FirstPaneClick);
registry.register(divider_area, SplitPaneAction::DividerDrag);
registry.register(second_area, SplitPaneAction::SecondPaneClick);

// Or use the all-in-one render method with closures
split_pane.render_with_content(
    area,
    buf,
    &mut state,
    |first_area, buf| { /* render first pane content */ },
    |second_area, buf| { /* render second pane content */ },
    &mut registry,
);

// Handle keyboard (arrows resize when divider focused, Home/End for min/max)
handle_split_pane_key(&mut state, &key_event, Orientation::Horizontal, 5, 10, 90);

// Handle mouse (drag divider to resize)
handle_split_pane_mouse(&mut state, &mouse_event, Orientation::Horizontal, &registry, 10, 90);

Orientations:

  • Orientation::Horizontal - Left | Right split (default)
  • Orientation::Vertical - Top / Bottom split

Style presets:

  • SplitPaneStyle::default() - Dark gray divider with grab indicator
  • SplitPaneStyle::minimal() - Thin line divider, no background
  • SplitPaneStyle::prominent() - Blue divider with high visibility

Log Viewer

use ratatui_interact::components::{LogViewer, LogViewerState};

let logs = vec![
    "[INFO] Application started".to_string(),
    "[ERROR] Connection failed".to_string(),
];

let mut state = LogViewerState::new(logs);

// Search
state.search("ERROR");
state.next_match();

// Scroll
state.scroll_down(5);
state.scroll_right(10);

let viewer = LogViewer::new(&state)
    .title("Application Log")
    .show_line_numbers(true);

Diff Viewer

use ratatui_interact::components::{
    DiffViewer, DiffViewerState, DiffViewMode, DiffData,
    handle_diff_viewer_key, handle_diff_viewer_mouse,
};

// Parse a unified diff (e.g., from `git diff`)
let diff_text = r#"--- a/src/main.rs
+++ b/src/main.rs
@@ -1,5 +1,6 @@
 fn main() {
-    println!("Hello, world!");
+    println!("Hello, Rust!");
+    println!("Welcome!");
 }
"#;

let mut state = DiffViewerState::from_unified_diff(diff_text);

// Toggle view mode
state.toggle_view_mode(); // Switches between Unified and SideBySide

// Hunk navigation
state.next_hunk();
state.prev_hunk();

// Change navigation (jump to next/prev addition or deletion)
state.next_change();
state.prev_change();

// Search within diff
state.start_search();
state.search.query = "println".to_string();
state.update_search();
state.next_match();

// Create viewer
let viewer = DiffViewer::new(&state)
    .title("Code Changes")
    .show_stats(true); // Shows +/- counts in title

// Handle keyboard input (in your event loop)
// handle_diff_viewer_key(&mut state, &key_event);

// Handle mouse scroll
// handle_diff_viewer_mouse(&mut state, &mouse_event);

Key bindings:

  • j/k or ↑/↓: Scroll up/down
  • h/l or ←/→: Scroll left/right
  • g/G or Home/End: Go to top/bottom
  • ]/[: Next/previous hunk
  • n/N: Next/previous change (or search match)
  • v or m: Toggle view mode (unified/side-by-side)
  • /: Start search
  • PgUp/PgDn or Ctrl+U/D: Page navigation

Style presets:

  • DiffViewerStyle::default() - Green additions, red deletions with dark backgrounds
  • DiffViewerStyle::high_contrast() - Brighter colors for better visibility
  • DiffViewerStyle::monochrome() - Bold/dim text without colors

Step Display

use ratatui_interact::components::{Step, StepDisplayState, StepDisplay, StepStatus};

let steps = vec![
    Step::new("Initialize").with_sub_steps(vec!["Load config", "Connect DB"]),
    Step::new("Process data"),
    Step::new("Finalize"),
];

let mut state = StepDisplayState::new(steps);

// Update progress
state.start_step(0);
state.start_sub_step(0, 0);
state.complete_sub_step(0, 0);
state.add_output(0, "Config loaded successfully");
state.complete_step(0);

let display = StepDisplay::new(&state);

File Explorer

use ratatui_interact::components::{FileExplorerState, FileExplorer};
use std::path::PathBuf;

let mut state = FileExplorerState::new(PathBuf::from("/home/user"));

// Navigate
state.cursor_down();
state.cursor_up();
state.toggle_selection(); // Multi-select
state.toggle_hidden(); // Show/hide hidden files

// Enter search mode
state.start_search();
state.search_push('r'); // Filter by 'r'

let explorer = FileExplorer::new(&state)
    .title("Select Files")
    .show_hidden(true);

Toast Notifications

Toast notifications provide transient feedback to users:

use ratatui_interact::components::{Toast, ToastState, ToastStyle};

struct App {
    toast_state: ToastState,
}

// Show a toast for 3 seconds
app.toast_state.show("File saved successfully!", 3000);

// In your render function:
fn render(app: &mut App, frame: &mut Frame, area: Rect) {
    // Draw your main content first...

    // Then draw toast on top if visible
    if let Some(message) = app.toast_state.get_message() {
        Toast::new(message)
            .style(ToastStyle::Success)
            .render_with_clear(area, frame.buffer_mut());
    }
}

// In your event loop, periodically clear expired toasts
app.toast_state.clear_if_expired();

Toast styles are auto-detected from message content, or can be set explicitly:

  • ToastStyle::Info (cyan) - default
  • ToastStyle::Success (green) - messages containing "success", "saved", "done"
  • ToastStyle::Warning (yellow) - messages containing "warning", "warn"
  • ToastStyle::Error (red) - messages containing "error", "fail"

Toast Stack

ToastStack manages multiple simultaneous toasts with configurable placement and dismiss policies:

use ratatui_interact::components::{
    ToastDismissPolicy, ToastPlacement, ToastStack, ToastStackLayout, ToastStackState, ToastStyle,
};

struct App {
    toasts: ToastStackState,
}

// Push toasts with different dismiss policies
app.toasts.push("Saved!", ToastStyle::Success, ToastDismissPolicy::Auto { duration_ms: 3000 });
app.toasts.push("Background task running...", ToastStyle::Info, ToastDismissPolicy::Manual);
app.toasts.push("Warning: disk almost full", ToastStyle::Warning,
    ToastDismissPolicy::ManualOrTimeout { duration_ms: 10000 });

// Dismiss by id (returned from push)
let id = app.toasts.push("Error occurred", ToastStyle::Error, ToastDismissPolicy::Manual);
app.toasts.dismiss(id);

// In your event loop — expire auto-dismiss toasts
let now_ms = std::time::SystemTime::now()
    .duration_since(std::time::UNIX_EPOCH).unwrap().as_millis() as i64;
app.toasts.tick(now_ms);

// In your render function (render last so toasts appear on top)
let layout = ToastStackLayout {
    placement: ToastPlacement::TopRight,
    ..ToastStackLayout::default()
};
ToastStack::new(&app.toasts, layout).render(area, frame.buffer_mut());

Run the demo: cargo run --example toast_stack_demo


Theme System

Theme provides a centralized ColorPalette with 30 semantic color roles that every widget style can be derived from:

use ratatui_interact::theme::Theme;
use ratatui_interact::components::{Button, ButtonState, ButtonStyle};

// Choose a preset
let theme = Theme::dark();   // or Theme::light()

// Derive any component style from the theme
let button_style: ButtonStyle = theme.style();

// Or use the .theme() builder shortcut on any widget
let button = Button::new("OK", &ButtonState::enabled()).theme(&theme);

// Access the palette directly for custom rendering
let fg = theme.palette.text;
let bg = theme.palette.surface;
let focused_border = theme.palette.border_focused;

Available palette roles: primary, secondary, text, text_dim, text_disabled, text_placeholder, text_muted, bg, surface, surface_raised, border_focused, border, border_disabled, border_accent, separator, highlight_fg, highlight_bg, success, warning, error, info, diff_add_fg/bg, diff_del_fg/bg

Enable serde support with the theme-serde feature:

ratatui-interact = { version = "0.5", features = ["theme-serde"] }

Run the demo: cargo run --example theme_demo


Mouse Click Handling

Buttons support mouse clicks through click regions. Use render_with_registry() for the simplest pattern:

use ratatui_interact::components::{Button, ButtonState};
use ratatui_interact::traits::ClickRegionRegistry;

struct App {
    click_regions: ClickRegionRegistry<usize>,
    // ... other fields
}

// In your render function:
fn render(app: &mut App, frame: &mut Frame) {
    // Clear at start of each frame
    app.click_regions.clear();

    let state = ButtonState::enabled();
    let button = Button::new("OK", &state);

    // Render and register in one call
    button.render_with_registry(area, frame.buffer_mut(), &mut app.click_regions, 0);
}

// In your event handler:
fn handle_mouse(app: &App, mouse: MouseEvent) {
    if is_left_click(&mouse) {
        if let Some(&idx) = app.click_regions.handle_click(mouse.column, mouse.row) {
            // Button at index `idx` was clicked
        }
    }
}

For more control, use the two-step pattern with render_stateful():

let region = button.render_stateful(area, buf);
registry.register(region.area, my_custom_action);