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

Float

Group: Chrome

Code

const HINT_ID: &str = "gallery:float:hint";
const MENU_ID: &str = "gallery:float:menu";

const COMMANDS: &[&str] = &["Open File", "Save", "Rename", "Duplicate", "Close"];
const MENU_ITEMS: &[&str] = &["Run now", "Run later", "Cancel"];

pub struct FloatDemo {
    selected: usize,
    hint_open: bool,
    menu_open: bool,
    menu_selected: usize,
    last_action: String,
}

impl FloatDemo {
    pub fn new() -> Self {
        Self {
            selected: 0,
            hint_open: false,
            menu_open: false,
            menu_selected: 0,
            last_action: String::new(),
        }
    }

    fn row_rect(&self, area: Rect, backend: &dyn Backend, idx: usize) -> Rect {
        let lh = backend.line_height();
        Rect::new(area.x, area.y + idx as f32 * lh, area.width, lh)
    }

    fn menu_anchor(&self, variant: usize, area: Rect, backend: &dyn Backend) -> Anchor {
        let side = if variant == 0 {
            Side::Bottom
        } else {
            Side::Top
        };
        Anchor::new(self.row_rect(area, backend, self.selected), side)
    }

    fn menu_float(&self, variant: usize, area: Rect, backend: &dyn Backend) -> Float {
        Float::new(
            WidgetId::new(MENU_ID),
            self.menu_anchor(variant, area, backend),
        )
    }

    fn menu_layout(&self, variant: usize, area: Rect, backend: &dyn Backend) -> FloatLayout {
        let cw = backend.char_width();
        let lh = backend.line_height();
        let float = self.menu_float(variant, area, backend);
        let measure = FloatMeasure::new(cw * 16.0, lh * (MENU_ITEMS.len() as f32 + 2.0));
        float.layout(area, measure)
    }

    fn hint_anchor(&self, area: Rect, backend: &dyn Backend) -> Anchor {
        let lh = backend.line_height();
        let cw = backend.char_width();
        let w = cw * 7.0;
        Anchor::new(
            Rect::new(area.x + area.width - w, area.y + area.height - lh, w, lh),
            Side::Top,
        )
    }

    fn hint_float(&self, area: Rect, backend: &dyn Backend) -> Float {
        let mut f = Float::new(WidgetId::new(HINT_ID), self.hint_anchor(area, backend));
        f.focusable = false;
        f
    }

    fn hint_layout(&self, area: Rect, backend: &dyn Backend) -> FloatLayout {
        let cw = backend.char_width();
        let lh = backend.line_height();
        let float = self.hint_float(area, backend);
        let measure = FloatMeasure::new(cw * 22.0, lh * 5.0);
        float.layout(area, measure)
    }

    fn line_at(backend: &mut dyn Backend, row: Rect, id: &str, text: String, selected: bool) {
        let bg = if selected {
            Color::rgb(60, 60, 90)
        } else {
            Color::rgb(24, 24, 32)
        };
        let bar = StatusBar {
            id: WidgetId::new(id),
            left_segments: vec![StatusBarSegment {
                text,
                fg: Color::rgb(220, 220, 220),
                bg,
                bold: selected,
                action_id: None,
            }],
            right_segments: vec![],
        };
        let _ = backend.draw_status_bar_interactive(row, &bar, &InteractionState::new());
    }

    fn paint_menu_content(&self, backend: &mut dyn Backend, layout: &FloatLayout) {
        let lh = backend.line_height();
        let cb = layout.content_bounds;
        for (i, item) in MENU_ITEMS.iter().enumerate() {
            let y = cb.y + i as f32 * lh;
            if y + lh > cb.y + cb.height {
                break;
            }
            Self::line_at(
                backend,
                Rect::new(cb.x, y, cb.width, lh),
                &format!("gallery:float:menu-item:{i}"),
                format!(" {item} "),
                i == self.menu_selected,
            );
        }
    }

    fn paint_hint_content(&self, backend: &mut dyn Backend, layout: &FloatLayout) {
        let lh = backend.line_height();
        let cb = layout.content_bounds;
        let lines = ["Up/Down move", "Enter runs", "m = menu"];
        for (i, text) in lines.iter().enumerate() {
            let y = cb.y + i as f32 * lh;
            if y + lh > cb.y + cb.height {
                break;
            }
            Self::line_at(
                backend,
                Rect::new(cb.x, y, cb.width, lh),
                &format!("gallery:float:hint-line:{i}"),
                format!(" {text} "),
                false,
            );
        }
    }

    fn open_menu(&mut self, variant: usize, area: Rect, backend: &mut dyn Backend) {
        self.menu_open = true;
        self.menu_selected = 0;
        let bounds = self.menu_layout(variant, area, backend).bounds;
        let float = self.menu_float(variant, area, backend);
        backend
            .modal_stack_handle()
            .borrow_mut()
            .push_float(&float, bounds);
    }

    fn close_menu(&mut self, backend: &mut dyn Backend) {
        self.menu_open = false;
        backend
            .modal_stack_handle()
            .borrow_mut()
            .pop(&WidgetId::new(MENU_ID));
    }

    fn toggle_hint(&mut self, area: Rect, backend: &mut dyn Backend) {
        if self.hint_open {
            self.hint_open = false;
            backend
                .modal_stack_handle()
                .borrow_mut()
                .pop(&WidgetId::new(HINT_ID));
        } else {
            self.hint_open = true;
            let bounds = self.hint_layout(area, backend).bounds;
            let float = self.hint_float(area, backend);
            backend
                .modal_stack_handle()
                .borrow_mut()
                .push_float(&float, bounds);
        }
    }
}

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

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

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

    fn variants(&self) -> &'static [&'static str] {
        &["Menu below row", "Menu above row"]
    }

    fn render(&self, variant: usize, backend: &mut dyn Backend, area: Rect) {
        for (i, cmd) in COMMANDS.iter().enumerate() {
            let row = self.row_rect(area, backend, i);
            Self::line_at(
                backend,
                row,
                &format!("gallery:float:cmd:{i}"),
                format!(" {cmd} "),
                i == self.selected,
            );
        }

        if self.menu_open {
            let layout = self.menu_layout(variant, area, backend);
            let float = self.menu_float(variant, area, backend);
            backend.draw_float(&float, &layout);
            self.paint_menu_content(backend, &layout);
        }
        if self.hint_open {
            let layout = self.hint_layout(area, backend);
            let float = self.hint_float(area, backend);
            backend.draw_float(&float, &layout);
            self.paint_hint_content(backend, &layout);
        }
    }

    fn handle(
        &mut self,
        variant: usize,
        event: &UiEvent,
        backend: &mut dyn Backend,
        area: Rect,
    ) -> Reaction {
        match event {
            UiEvent::KeyPressed {
                key: Key::Char('?'),
                ..
            } => {
                self.toggle_hint(area, backend);
                Reaction::Redraw
            }
            UiEvent::KeyPressed {
                key: Key::Char('m'),
                ..
            } => {
                if self.menu_open {
                    self.close_menu(backend);
                } else {
                    self.open_menu(variant, area, backend);
                }
                Reaction::Redraw
            }
            UiEvent::KeyPressed {
                key: Key::Named(NamedKey::Escape),
                ..
            } if self.menu_open => {
                self.close_menu(backend);
                Reaction::Redraw
            }
            UiEvent::KeyPressed {
                key: Key::Named(NamedKey::Escape),
                ..
            } if self.hint_open => {
                self.toggle_hint(area, backend);
                Reaction::Redraw
            }
            UiEvent::KeyPressed {
                key: Key::Named(NamedKey::Up),
                ..
            } => {
                let focused = backend
                    .modal_stack_handle()
                    .borrow()
                    .top_focusable()
                    .map(|e| e.id);
                if focused == Some(WidgetId::new(MENU_ID)) {
                    self.menu_selected = self.menu_selected.saturating_sub(1);
                } else {
                    self.selected = self.selected.saturating_sub(1);
                }
                Reaction::Redraw
            }
            UiEvent::KeyPressed {
                key: Key::Named(NamedKey::Down),
                ..
            } => {
                let focused = backend
                    .modal_stack_handle()
                    .borrow()
                    .top_focusable()
                    .map(|e| e.id);
                if focused == Some(WidgetId::new(MENU_ID)) {
                    self.menu_selected = (self.menu_selected + 1).min(MENU_ITEMS.len() - 1);
                } else {
                    self.selected = (self.selected + 1).min(COMMANDS.len() - 1);
                }
                Reaction::Redraw
            }
            UiEvent::KeyPressed {
                key: Key::Named(NamedKey::Enter),
                ..
            } => {
                let focused = backend
                    .modal_stack_handle()
                    .borrow()
                    .top_focusable()
                    .map(|e| e.id);
                if focused == Some(WidgetId::new(MENU_ID)) {
                    self.last_action = format!(
                        "{} / {}",
                        COMMANDS[self.selected], MENU_ITEMS[self.menu_selected]
                    );
                    self.close_menu(backend);
                } else {
                    self.last_action = COMMANDS[self.selected].to_string();
                }
                Reaction::Redraw
            }
            UiEvent::MouseDown {
                widget: Some(ref id),
                position,
                ..
            } if id.as_str() == MENU_ID => {
                let layout = self.menu_layout(variant, area, backend);
                let lh = backend.line_height();
                let rel_y = position.y - layout.content_bounds.y;
                if rel_y >= 0.0 {
                    let idx = (rel_y / lh).floor() as usize;
                    if idx < MENU_ITEMS.len() {
                        self.last_action =
                            format!("{} / {}", COMMANDS[self.selected], MENU_ITEMS[idx]);
                        self.close_menu(backend);
                    }
                }
                Reaction::Redraw
            }
            UiEvent::MouseDown {
                widget: Some(ref id),
                ..
            } if id.as_str() == HINT_ID => {
                self.toggle_hint(area, backend);
                Reaction::Redraw
            }
            UiEvent::MouseDown {
                widget: None,
                position,
                ..
            } => {
                let lh = backend.line_height();
                let idx = ((position.y - area.y) / lh).floor() as usize;
                if idx < COMMANDS.len() {
                    self.selected = idx;
                }
                Reaction::Redraw
            }
            _ => Reaction::Continue,
        }
    }

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

    fn data(&self, _variant: usize) -> serde_json::Value {
        serde_json::json!({
            "selected": COMMANDS[self.selected],
            "menu_open": self.menu_open,
            "hint_open": self.hint_open,
            "last_action": self.last_action,
        })
    }

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

Backends

VariantTUIGTKmacOSWindows
Menu below row
Menu above row