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

Toolbar

Group: Chrome

Code

pub struct ToolbarDemo {
    filter_active: bool,
    running: bool,
    last_message: String,
    interaction: InteractionState,
    focused_index: Option<usize>,
}

impl ToolbarDemo {
    pub fn new() -> Self {
        Self {
            filter_active: false,
            running: true,
            last_message: "Click, Tab to focus, Enter to activate".into(),
            interaction: InteractionState::new(),
            focused_index: None,
        }
    }

    /// `variant == 1` drops button labels and keeps only icon + tooltip —
    /// the compact style a space-constrained host toolbar would use.
    fn toolbar(&self, variant: usize) -> Toolbar {
        let labeled = variant == 0;
        let label = |text: &str| if labeled { text.into() } else { String::new() };
        Toolbar::new(WidgetId::new("gallery:toolbar"))
            .with_buttons(vec![
                ToolbarButton::Action {
                    id: WidgetId::new("gallery:toolbar:continue"),
                    label: label("Continue"),
                    icon: Some("▶".into()),
                    key_hint: Some("1".into()),
                    enabled: !self.running,
                    is_active: false,
                    tooltip: "Resume execution".into(),
                },
                ToolbarButton::Action {
                    id: WidgetId::new("gallery:toolbar:pause"),
                    label: label("Pause"),
                    icon: Some("⏸".into()),
                    key_hint: Some("2".into()),
                    enabled: self.running,
                    is_active: false,
                    tooltip: "Pause execution".into(),
                },
                ToolbarButton::Separator,
                ToolbarButton::Action {
                    id: WidgetId::new("gallery:toolbar:filter"),
                    label: label("Filter"),
                    icon: Some("⚙".into()),
                    key_hint: Some("3".into()),
                    enabled: true,
                    is_active: self.filter_active,
                    tooltip: "Toggle filter".into(),
                },
                ToolbarButton::Action {
                    id: WidgetId::new("gallery:toolbar:debug"),
                    label: label("Debug"),
                    // Every button carries an icon so the icon-only
                    // variant has something to paint for each one.
                    icon: Some("⚑".into()),
                    key_hint: None,
                    enabled: false,
                    is_active: false,
                    tooltip: "Disabled in this build".into(),
                },
                ToolbarButton::Label {
                    text: if self.running {
                        " running".into()
                    } else {
                        " paused".into()
                    },
                    fg: Some(if self.running {
                        Color::rgb(120, 200, 120)
                    } else {
                        Color::rgb(220, 180, 80)
                    }),
                },
            ])
            .with_focused_index(self.focused_index)
    }

    fn toolbar_rect(area: Rect, backend: &dyn Backend) -> Rect {
        Rect::new(area.x, area.y, area.width, backend.line_height())
    }

    fn hint_bar(&self) -> StatusBar {
        StatusBar {
            id: WidgetId::new("gallery:toolbar:hint"),
            left_segments: vec![StatusBarSegment {
                text: format!("  {} ", self.last_message),
                fg: Color::rgb(255, 255, 255),
                bg: Color::rgb(30, 30, 30),
                bold: false,
                action_id: None,
            }],
            right_segments: vec![],
        }
    }

    fn dispatch(&mut self, id: &WidgetId) {
        match id.as_str() {
            "gallery:toolbar:continue" => {
                self.running = true;
                self.last_message = "Continue".into();
            }
            "gallery:toolbar:pause" => {
                self.running = false;
                self.last_message = "Paused".into();
            }
            "gallery:toolbar:filter" => {
                self.filter_active = !self.filter_active;
                self.last_message =
                    format!("Filter {}", if self.filter_active { "on" } else { "off" });
            }
            other => {
                self.last_message = format!("Unknown action: {other}");
            }
        }
    }

    fn focusable_indices(&self, variant: usize) -> Vec<usize> {
        self.toolbar(variant)
            .buttons
            .into_iter()
            .enumerate()
            .filter_map(|(i, btn)| match btn {
                ToolbarButton::Action { enabled, .. } if enabled => Some(i),
                _ => None,
            })
            .collect()
    }

    fn advance_focus(&mut self, variant: usize, forward: bool) {
        let candidates = self.focusable_indices(variant);
        if candidates.is_empty() {
            return;
        }
        self.focused_index = Some(match self.focused_index {
            None => {
                if forward {
                    candidates[0]
                } else {
                    *candidates.last().unwrap()
                }
            }
            Some(current) => match candidates.iter().position(|&i| i == current) {
                None => candidates[0],
                Some(p) => {
                    let next = if forward {
                        (p + 1) % candidates.len()
                    } else {
                        (p + candidates.len() - 1) % candidates.len()
                    };
                    candidates[next]
                }
            },
        });
    }

    fn activate_focused(&mut self, variant: usize) -> bool {
        let Some(idx) = self.focused_index else {
            return false;
        };
        let bar = self.toolbar(variant);
        if let Some(ToolbarButton::Action { id, enabled, .. }) = bar.buttons.get(idx) {
            if *enabled {
                let id = *id;
                self.dispatch(&id);
                return true;
            }
        }
        false
    }
}

impl Default for ToolbarDemo {
    fn default() -> Self {
        Self::new()
    }
}

impl Demo for ToolbarDemo {
    fn name(&self) -> &'static str {
        "Toolbar"
    }

    fn group(&self) -> &'static str {
        "Chrome"
    }

    fn variants(&self) -> &'static [&'static str] {
        &["Labeled", "Icon-only"]
    }

    fn render(&self, variant: usize, backend: &mut dyn Backend, area: Rect) {
        let lh = backend.line_height();
        let rect = Self::toolbar_rect(area, backend);
        let bar = self.toolbar(variant);
        let _ = backend.draw_toolbar_interactive(rect, &bar, &self.interaction);

        let hint_rect = Rect::new(area.x, area.y + lh, area.width, lh);
        let _ = backend.draw_status_bar_interactive(
            hint_rect,
            &self.hint_bar(),
            &InteractionState::new(),
        );
    }

    fn handle(
        &mut self,
        variant: usize,
        event: &UiEvent,
        backend: &mut dyn Backend,
        area: Rect,
    ) -> Reaction {
        match event {
            UiEvent::KeyPressed {
                key: Key::Named(NamedKey::Tab),
                ..
            } => {
                self.advance_focus(variant, true);
                Reaction::Redraw
            }
            UiEvent::KeyPressed {
                key: Key::Named(NamedKey::BackTab),
                ..
            } => {
                self.advance_focus(variant, false);
                Reaction::Redraw
            }
            UiEvent::KeyPressed {
                key: Key::Named(NamedKey::Enter) | Key::Char(' '),
                ..
            } => {
                if self.activate_focused(variant) {
                    Reaction::Redraw
                } else {
                    Reaction::Continue
                }
            }
            UiEvent::KeyPressed {
                key: Key::Char(c @ '1'..='3'),
                ..
            } => {
                let id = match c {
                    '1' => "gallery:toolbar:continue",
                    '2' => "gallery:toolbar:pause",
                    _ => "gallery:toolbar:filter",
                };
                let bar = self.toolbar(variant);
                let allowed = bar.buttons.iter().any(|btn| {
                    matches!(btn, ToolbarButton::Action { id: bid, enabled, .. }
                        if bid.as_str() == id && *enabled)
                });
                if allowed {
                    self.dispatch(&WidgetId::new(id));
                    return Reaction::Redraw;
                }
                Reaction::Continue
            }
            UiEvent::MouseMoved { .. } | UiEvent::MouseDown { .. } | UiEvent::MouseUp { .. } => {
                let rect = Self::toolbar_rect(area, backend);
                let bar = self.toolbar(variant);
                let layout = backend.toolbar_layout(rect, &bar);
                let hit_test = |x: f32, y: f32| match layout.hit_test(x, y) {
                    ToolbarHit::Button(id) => Some(id),
                    ToolbarHit::Empty => None,
                };
                let was_pressed = self.interaction.pressed().cloned();
                let changed = self.interaction.handle_mouse(event, hit_test);

                if let UiEvent::MouseUp { position, .. } = event {
                    if let (Some(pressed_id), ToolbarHit::Button(release_id)) =
                        (was_pressed, layout.hit_test(position.x, position.y))
                    {
                        if pressed_id == release_id {
                            self.dispatch(&release_id);
                        }
                    }
                    return Reaction::Redraw;
                }
                if changed {
                    Reaction::Redraw
                } else {
                    Reaction::Continue
                }
            }
            _ => Reaction::Continue,
        }
    }

    fn source(&self) -> &'static str {
        extract_region(SOURCE)
    }

    fn data(&self, variant: usize) -> serde_json::Value {
        serde_json::to_value(self.toolbar(variant)).unwrap_or(serde_json::Value::Null)
    }

    fn caps_note(&self, _variant: usize, _caps: &BackendCaps) -> Option<String> {
        None
    }
}

Backends

VariantTUIGTKmacOSWindows
Labeled
Icon-only