Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Theme a widget

Every widget in retroglyph-ui picks its colors from whichever style knobs you set on it directly (Button::style/hovered_style, Panel::border_style/fill_style, and so on), independent of any other widget on screen. That’s fine for one widget; for an app with several, .theme() replaces the per-widget, per-state style calls with one shared palette.

Theme

Theme is a plain struct of named color roles (fg, panel_bg, hover_bg, press_bg, accent, and the rest); build one, or start from Theme::DARK or Theme::LIGHT. Every widget that draws (Panel, Tabs, List, Button, ProgressBar, and the rest of the widget module) has a .theme(theme) builder method that derives every style it needs from that one Theme instead of the widget’s own per-state defaults:

    let panel = Panel::new()
        .title(if *dark { "Theme: Dark" } else { "Theme: Light" })
        .theme(theme);
    ui.draw(panel_area, &panel);

    // Panel's own interior inset -- one cell in from the border on every side, the same math
    // `Modal::render` uses to hand back its inner content rect.
    let inner = Rect::new(
        panel_area.left() + 1,
        panel_area.top() + 1,
        panel_area.width() - 2,
        panel_area.height() - 2,
    );

    let tabs = Tabs::new(&TABS).select(Some(selected_tab)).theme(theme);
    ui.draw(
        Rect::new(inner.left(), inner.top(), inner.width(), 1),
        &tabs,
    );

    let list_area = Rect::new(inner.left(), inner.top() + 2, inner.width(), 4);
    let list = List::new(&ITEMS).theme(theme);
    ui.draw_stateful(list_area, &list, list_state);

    draw_toggle_button(
        ui,
        Rect::new(inner.left(), inner.top() + 7, 20, 1),
        theme,
        dark,
    );

    let progress_area = Rect::new(inner.left(), inner.top() + 9, inner.width(), 1);
    ui.draw(progress_area, &ProgressBar::new(7, 10).theme(theme));

Theme carries no reference to “the active theme”: nothing here is global or thread-local. Each draw call is handed whichever Theme value the app currently considers active, picked however the app likes (a config setting, a t keypress, matching the terminal’s own light/dark preference), and every widget re-derives its colors from it fresh every frame.

Switching at runtime

Because .theme() takes a plain value with no persistent state of its own, switching themes is just picking a different Theme before the next frame’s draw calls, including from inside a widget the theme itself affects, like the toggle button below:

fn draw_toggle_button(ui: &mut Ui<'_, '_, WidgetId>, rect: Rect, theme: Theme, dark: &mut bool) {
    let label = if *dark {
        "Switch to Light"
    } else {
        "Switch to Dark"
    };
    let button = Button::new(label).theme(theme);
    if ui.show(rect, WidgetId::ToggleButton, &button).clicked() {
        *dark = !*dark;
    }
}

Running it

cargo run --example 17_theme_switch --features crossterm
cargo run --example 17_theme_switch --features software
cargo run --example 17_theme_switch  # headless fallback, prints a few frames to stdout

See also

  • Draw a panel and Handle a click, for widgets typically themed together.
  • examples/examples/09_widgets_dashboard.rs/examples/examples/10_widgets_interaction.rs for the hand-threaded theme.* style calls .theme() replaces.