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

widgets/tooltip.rs

//! The tooltip: a float below a hovered node, and the same fact told to
//! assistive technology. Three spellings of one thing:
//!
//! - `widgets::tooltip(ui, text)` inside a node the view knows is hovered
//!   (`hoverable` + `is_hovered`) — the float, drawn by the view when the
//!   view decides, which is also how a tooltip shows on a click or a
//!   first frame;
//! - `NodeSpec::tooltip(hint)`: the prop — the node tracks hover, the
//!   hint is its accessible `description` (what a screen reader says
//!   after the name), and the core floats it while hovered. On any node;
//!   `apply_tooltip` is the first two alone, for a view that floats its
//!   own;
//! - `tooltip_with`: the same chrome around anything — a legend, a
//!   shortcut hint with its own layout.
//!
//! A tooltip is a float with `fit`: near the window's bottom edge it
//! mirrors above the node instead of hanging off the frame.
//!
//! Run: cargo run -p kui-native --example tooltip [-- --headless]

use kui_devtools::{Drive, Example};
use kui_native::widgets;
use kui_native::{Align, App, Core, NodeSpec, TextStyle, Ui, UiEvent};

#[derive(Default)]
struct Tooltip {
    clicks: u32,
    /// The badge's tooltip is pinned open until the first click, so it can
    /// be seen without a pointer.
    pinned: bool,
}

impl App for Tooltip {
    fn view(&mut self, ui: &mut Ui<'_>) {
        let t = ui.theme();
        ui.with(
            NodeSpec::column()
                .fill()
                .pad(24.0)
                .gap(20.0)
                .cross_align(Align::Start),
            |ui| {
                ui.text("the view's: `hoverable`, `is_hovered`, `widgets::tooltip`", TextStyle::new(12.0).color(t.muted));
                let badge = ui.child_key("badge");
                let show = ui.is_hovered(badge) || !self.pinned;
                ui.with_keyed(
                    "badge",
                    NodeSpec::row()
                        .pad_xy(12.0, 6.0)
                        .radius(10.0)
                        .bg(t.raised)
                        .border(1.0, t.border)
                        .hoverable(),
                    |ui| {
                        ui.text("?", TextStyle::new(13.0));
                        if show {
                            widgets::tooltip(ui, "a float: out of flow, on top, unclipped — pinned until the first click");
                        }
                    },
                );

                ui.text("the prop's: `tooltip` — hover tracking, the accessible description and the float in one", TextStyle::new(12.0).color(t.muted));
                ui.with(NodeSpec::row().gap(10.0), |ui| {
                    widgets::button_with(
                        ui,
                        "save",
                        "save",
                        widgets::button_spec(&ui.theme(), &ui.metrics()).on_click("save").tooltip("⌘S · write the file"),
                        None,
                    );
                    widgets::button_with(
                        ui,
                        "run",
                        "run",
                        widgets::button_spec(&ui.theme(), &ui.metrics()).accent().on_click("run").tooltip("⌘R · run the current file"),
                        None,
                    );
                });

                ui.text("`tooltip_with`: the chrome around anything", TextStyle::new(12.0).color(t.muted));
                let legend = ui.child_key("legend");
                let over = ui.is_hovered(legend);
                ui.with_keyed(
                    "legend",
                    NodeSpec::row()
                        .pad_xy(12.0, 6.0)
                        .radius(8.0)
                        .bg(t.surface)
                        .border(1.0, t.border)
                        .hoverable(),
                    |ui| {
                        ui.text("hover for the shortcuts", TextStyle::new(13.0));
                        if over {
                            widgets::tooltip_with(ui, |ui| {
                                ui.with(NodeSpec::column().gap(4.0), |ui| {
                                    for (k, what) in [("⌘S", "save"), ("⌘R", "run"), ("⌘-Shift-P", "the palette")] {
                                        ui.with(NodeSpec::row().gap(10.0), |ui| {
                                            ui.text_in(NodeSpec::row().width(80.0), k, TextStyle::new(12.0).color(t.accent).mono());
                                            ui.text(what, TextStyle::new(12.0).color(t.muted));
                                        });
                                    }
                                });
                            });
                        }
                    },
                );
                ui.leaf(NodeSpec::column().grow_height());
                let low = ui.child_key("low");
                let over_low = ui.is_hovered(low);
                ui.with_keyed(
                    "low",
                    NodeSpec::row()
                        .pad_xy(12.0, 6.0)
                        .radius(8.0)
                        .bg(t.surface)
                        .border(1.0, t.border)
                        .hoverable(),
                    |ui| {
                        ui.text("near the bottom: the float mirrors above", TextStyle::new(13.0));
                        if over_low {
                            widgets::tooltip(ui, "fit: mirrored back across the node, not off the frame");
                        }
                    },
                );
                ui.text(&format!("{} clicks", self.clicks), TextStyle::new(12.0).color(t.faint));
            },
        );
    }

    fn on_event(&mut self, ev: UiEvent) {
        if ev.payload.as_str().is_some() {
            self.clicks += 1;
            self.pinned = true;
        }
    }
}

impl Example for Tooltip {
    fn window(&self) -> kui_devtools::Window {
        kui_devtools::Window::default().size(520.0, 360.0)
    }

    /// The badge's tooltip is in the frame until a click, then only while
    /// hovered; a button's hint is its accessible description whether or
    /// not it is hovered.
    fn headless(&mut self, core: &mut Core) -> Result<(), String> {
        let mut d = Drive::new(core, 520.0, 360.0);
        d.frame(self);
        let has_text = |d: &mut Drive<'_>, s: &str| {
            d.core
                .access_tree()
                .nodes
                .iter()
                .any(|n| n.name.as_deref().is_some_and(|x| x.contains(s)))
        };
        let shown = has_text(&mut d, "a float: out of flow");
        d.check(shown, "the badge's tooltip shows before any click")?;
        let save = d.key_of("save").ok_or("no save button")?;
        d.click_key(self, save);
        d.frame(self);
        let shown = has_text(&mut d, "a float: out of flow");
        d.check(!shown, "and hides after the first click")?;
        let badge = d.key_of("badge").ok_or("no badge")?;
        d.hover(self, badge);
        d.frame(self);
        let shown = has_text(&mut d, "a float: out of flow");
        d.check(shown, "hovering the badge shows it again")?;

        let desc = d
            .core
            .access_tree()
            .nodes
            .iter()
            .find(|n| n.name.as_deref() == Some("save"))
            .and_then(|n| n.description.clone());
        d.check(
            desc.as_deref() == Some("⌘S · write the file"),
            "a button's hint is its accessible description, hovered or not",
        )
    }
}

kui_devtools::main!(Tooltip::default());