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/select.rs

//! The select: a field that shows the choice in force and, clicked, drops
//! the core's own menu of the choices with the current one checked
//! (`widgets::select`). The app holds no open state — the menu is the
//! same one a right-click opens, drawn in the frame or shown by the
//! platform, dismissed by Escape or a press outside, walked by the
//! arrows — and hears one event: the choice, as the `menu` event a menu
//! row posts, on the field's key. Drawing the field again with the new
//! `current` is the whole loop.
//!
//! Two fields: a `select` over plain labels, whose choice posts the label,
//! and a `select_items` over `MenuItem`s, whose choice posts each item's
//! `id` — here the size in points, so the app parses nothing.
//!
//! Run: cargo run -p kui-native --example select [-- --headless]

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

const LANGUAGES: [&str; 4] = ["English", "Deutsch", "Français", "日本語"];
const SIZES: [u32; 4] = [11, 13, 15, 18];

struct Select {
    language: usize,
    size: usize,
    /// What the last choice posted, verbatim: the whole of what an app sees.
    last: Option<Value>,
}

impl Default for Select {
    fn default() -> Self {
        Self {
            language: 0,
            size: 1,
            last: None,
        }
    }
}

impl App for Select {
    fn view(&mut self, ui: &mut Ui<'_>) {
        let t = ui.theme();
        let row = |ui: &mut Ui<'_>, label: &str, f: &mut dyn FnMut(&mut Ui<'_>)| {
            ui.with(
                NodeSpec::row()
                    .grow_width()
                    .gap(12.0)
                    .cross_align(Align::Center),
                |ui| {
                    ui.text_in(
                        NodeSpec::row().width(90.0),
                        label,
                        TextStyle::new(13.0).color(t.muted),
                    );
                    f(ui);
                },
            );
        };
        ui.with(
            NodeSpec::column()
                .fill()
                .pad(24.0)
                .gap(14.0)
                .cross_align(Align::Start),
            |ui| {
                ui.text(
                    "`select`: the choice posts its label",
                    TextStyle::new(12.0).color(t.muted),
                );
                row(ui, "language", &mut |ui| {
                    widgets::select(ui, "language", &LANGUAGES, Some(self.language));
                });
                ui.text(
                    "`select_items`: the choice posts the item's `id` — the size itself",
                    TextStyle::new(12.0).color(t.muted),
                );
                row(ui, "size", &mut |ui| {
                    let items: Vec<MenuItem> = SIZES
                        .iter()
                        .map(|s| MenuItem::new(format!("{s} pt")).id(Value::Int(i64::from(*s))))
                        .collect();
                    widgets::select_items(ui, "size", &items, Some(self.size));
                });
                ui.leaf(NodeSpec::column().height(10.0));
                ui.text(
                    &format!("{} at {} pt", LANGUAGES[self.language], SIZES[self.size]),
                    TextStyle::new(SIZES[self.size] as f32),
                );
                let last = match &self.last {
                    Some(v) => format!("last event: {v:?}"),
                    None => {
                        "no choice yet — click a field, or Tab to it and press Space".to_string()
                    }
                };
                ui.text(&last, TextStyle::new(12.0).color(t.faint).mono());
            },
        );
    }

    fn on_event(&mut self, ev: UiEvent) {
        // `{kind: "menu", role: "custom", item: …}` on the field's key.
        if ev.kind() != Some("menu") {
            return;
        }
        let Some(item) = ev.payload.get("item") else {
            return;
        };
        self.last = Some(item.clone());
        if let Some(name) = item.as_str()
            && let Some(i) = LANGUAGES.iter().position(|l| *l == name)
        {
            self.language = i;
        }
        if let Some(pt) = item.as_int()
            && let Some(i) = SIZES.iter().position(|s| i64::from(*s) == pt)
        {
            self.size = i;
        }
    }
}

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

    /// The drawn menu, so the rows are in the frame to click.
    fn native_menus(&self) -> Option<bool> {
        Some(false)
    }

    /// A click on the field opens its menu under it and reaches the app
    /// as nothing; a row chosen is one event naming the option, and the
    /// field then shows it.
    fn headless(&mut self, core: &mut Core) -> Result<(), String> {
        let mut d = Drive::new(core, 520.0, 300.0);
        d.frame(self);
        let field = d.key_of("language").ok_or("no language field")?;
        let rect = d.rect_of(field).ok_or("the field is not in the hit list")?;
        let evs = d.click_key(self, field);
        d.check(evs.is_empty(), "the field's click never reaches the app")?;
        let menu = d.core.menu().ok_or("no menu opened")?;
        d.check(menu.target == field, "the menu is about the field")?;
        d.check(
            menu.at.y == rect.y + rect.h && menu.at.x == rect.x,
            "and hangs under it",
        )?;
        d.check(menu.items[0].checked, "the current choice is checked")?;
        d.frame(self);
        let row = d
            .core
            .access_tree()
            .nodes
            .iter()
            .find(|n| n.role == kui_native::Role::MenuItem && n.name.as_deref() == Some("Deutsch"))
            .map(|n| n.rect)
            .ok_or("the rows are not drawn")?;
        let evs = d.click(self, row.x + row.w / 2.0, row.y + row.h / 2.0);
        d.check(
            evs.len() == 1 && evs[0].key == field,
            "one event, on the field",
        )?;
        d.check(self.language == 1, "the app took the choice")?;
        d.check(d.core.menu().is_none(), "and the menu closed")?;
        d.frame(self);
        let shown = d
            .core
            .access_tree()
            .nodes
            .iter()
            .find(|n| n.role == kui_native::Role::Button && n.name.as_deref() == Some("language"))
            .and_then(|n| n.description.clone());
        d.check(
            shown.as_deref() == Some("Deutsch"),
            "the field describes itself by the new choice",
        )?;
        // The item form: the id, not the label.
        let size = d.key_of("size").ok_or("no size field")?;
        d.click_key(self, size);
        d.frame(self);
        let row = d
            .core
            .access_tree()
            .nodes
            .iter()
            .find(|n| n.role == kui_native::Role::MenuItem && n.name.as_deref() == Some("18 pt"))
            .map(|n| n.rect)
            .ok_or("the size rows are not drawn")?;
        d.click(self, row.x + row.w / 2.0, row.y + row.h / 2.0);
        d.check(
            self.last == Some(Value::Int(18)),
            "the choice posted the item's id",
        )?;
        d.check(self.size == 3, "and the app took it")
    }
}

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