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

features/hover.rs

//! Hover, declared rather than tracked. Four ways a view says what the
//! pointer's arrival means, and none of them is state the app keeps:
//!
//! - `hover_bg`: the background while the pointer is over the node — the
//!   core swaps it, the view declares both colours once;
//! - `hoverable` + `Ui::is_hovered`: a node the core tracks so the *view*
//!   can branch on it — a tooltip, a reveal, a badge that reads its own
//!   hover on the next frame;
//! - `hover_group`: siblings that light together — a row's icon and its
//!   label are one hover, whichever of them the pointer is on, asked by
//!   `is_group_hovered`;
//! - `on_hover`: the arrival and the leaving as events, `enter` and
//!   `leave`, per node, for a model that wants to know — the count below.
//!
//! The pointer leaving the window leaves everything; a hover survives a
//! rebuild because the node's key does.
//!
//! Run: cargo run -p kui-native --example hover [-- --headless]

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

const ROWS: [(&str, &str); 3] = [("◆", "inbox"), ("●", "drafts"), ("▲", "sent")];

#[derive(Default)]
struct Hover {
    enters: u32,
    leaves: u32,
    /// Which row the pointer is over, from `on_hover`.
    over: Option<String>,
}

impl App for Hover {
    fn view(&mut self, ui: &mut Ui<'_>) {
        let t = ui.theme();
        ui.with(
            NodeSpec::column()
                .fill()
                .pad(24.0)
                .gap(18.0)
                .cross_align(Align::Start),
            |ui| {
                ui.text(
                    "hover_bg · the core swaps the background",
                    TextStyle::new(12.0).color(t.muted),
                );
                ui.with(NodeSpec::row().gap(10.0), |ui| {
                    for (name, bg, hover) in [
                        ("surface", t.surface, t.hover),
                        ("accent", t.accent, t.accent_hover),
                        ("danger", t.danger, t.danger.with_alpha(0.7)),
                    ] {
                        ui.text_in_keyed(
                            name,
                            NodeSpec::row()
                                .pad_xy(16.0, 10.0)
                                .radius(8.0)
                                .bg(bg)
                                .hover_bg(hover)
                                .border(1.0, t.border),
                            name,
                            TextStyle::new(13.0).color(kui_native::widgets::readable_on(bg)),
                        );
                    }
                });

                ui.text(
                    "hoverable + is_hovered · the view branches on its own hover",
                    TextStyle::new(12.0).color(t.muted),
                );
                let badge = ui.child_key("badge");
                let hovered = ui.is_hovered(badge);
                ui.text_in_keyed(
                    "badge",
                    NodeSpec::row()
                        .pad_xy(14.0, 8.0)
                        .radius(8.0)
                        .bg(t.raised)
                        .border(1.0, if hovered { t.accent } else { t.border })
                        .hoverable(),
                    if hovered {
                        "hovered — the border is the view's doing"
                    } else {
                        "hover me"
                    },
                    TextStyle::new(13.0),
                );

                ui.text(
                    "hover_group · an icon and its label are one hover",
                    TextStyle::new(12.0).color(t.muted),
                );
                ui.with(
                    NodeSpec::column()
                        .width(260.0)
                        .gap(2.0)
                        .pad(6.0)
                        .bg(t.surface)
                        .radius(8.0)
                        .border(1.0, t.border),
                    |ui| {
                        for (icon, label) in ROWS {
                            let lit = ui.is_group_hovered(NodeSpec::hover_group_id(label));
                            let bg = if lit { t.hover } else { t.surface };
                            // Two nodes, one group: whichever the pointer is
                            // on, both read `lit`. `on_hover` stays per node
                            // — the group is who lights together, not who
                            // reports — so the count below moves as the
                            // pointer crosses from the icon to the label.
                            ui.with(NodeSpec::row().grow_width().gap(0.0), |ui| {
                                ui.text_in_keyed(
                                    &format!("{label}-icon"),
                                    NodeSpec::row()
                                        .width(36.0)
                                        .pad_xy(0.0, 6.0)
                                        .main_align(Align::Center)
                                        .bg(bg)
                                        .radius(4.0)
                                        .hover_group(label)
                                        .on_hover(Value::str(label)),
                                    icon,
                                    TextStyle::new(13.0).color(t.accent),
                                );
                                ui.text_in_keyed(
                                    &format!("{label}-text"),
                                    NodeSpec::row()
                                        .grow_width()
                                        .pad_xy(8.0, 6.0)
                                        .bg(bg)
                                        .radius(4.0)
                                        .hover_group(label)
                                        .on_hover(Value::str(label)),
                                    label,
                                    TextStyle::new(13.0),
                                );
                            });
                        }
                    },
                );

                ui.text(
                    &format!(
                        "on_hover · {} enters, {} leaves · over: {}",
                        self.enters,
                        self.leaves,
                        self.over.as_deref().unwrap_or("nothing")
                    ),
                    TextStyle::new(12.0).color(t.faint),
                );
            },
        );
    }

    fn on_event(&mut self, ev: UiEvent) {
        if ev.kind() != Some("hover") {
            return;
        }
        let tag = ev.payload.get_str("tag").map(str::to_string);
        match ev.payload.get_str("phase") {
            Some("enter") => {
                self.enters += 1;
                self.over = tag;
            }
            Some("leave") => {
                self.leaves += 1;
                self.over = None;
            }
            _ => {}
        }
    }
}

impl Example for Hover {
    fn window(&self) -> kui_devtools::Window {
        kui_devtools::Window::default().size(460.0, 420.0)
    }

    /// The pointer moves onto a badge, a group, and away: what the frame
    /// reports and what the events say agree.
    fn headless(&mut self, core: &mut Core) -> Result<(), String> {
        let mut d = Drive::new(core, 460.0, 420.0);
        d.frame(self);
        let badge = d.key_of("badge").ok_or("no badge")?;
        d.hover(self, badge);
        d.check(
            d.core.is_hovered(badge),
            "a hoverable node reports the pointer",
        )?;
        d.frame(self);

        let icon = d.key_of("drafts-icon").ok_or("no drafts icon")?;
        let text = d.key_of("drafts-text").ok_or("no drafts label")?;
        d.hover(self, icon);
        d.check(
            d.core.is_group_hovered(NodeSpec::hover_group_id("drafts")),
            "the pointer on the icon lights the group",
        )?;
        d.check(
            self.over.as_deref() == Some("drafts") && self.enters == 1,
            "and `on_hover` reports one enter",
        )?;
        d.hover(self, text);
        d.check(
            self.enters == 2 && self.leaves == 1 && self.over.as_deref() == Some("drafts"),
            "moving to the label leaves the icon and enters the label: on_hover is per node",
        )?;
        d.check(
            d.core.is_group_hovered(NodeSpec::hover_group_id("drafts")),
            "and the group stays lit across the move",
        )?;
        d.input(self, kui_native::InputEvent::CursorLeft);
        d.check(
            self.leaves == 2 && self.over.is_none(),
            "the pointer leaving the window leaves the group",
        )?;
        d.frame(self);
        d.check(!d.core.is_hovered(badge), "and nothing is hovered")
    }
}

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