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

//! The stock button, in every state it has. `widgets::button(ui, text,
//! payload)` is the whole of it for most uses: a keyed row with the
//! theme's three backgrounds (rest, hover, pressed), a label readable on
//! whichever it is, a click payload, and a `button` role named by its
//! text. `button_with` hands the spec back to the caller for the rest —
//! `accent` (a no-op on the stock spec since backlog AR41, which paints
//! every plain button from the theme's accent trio; it still recolours a
//! spec of the app's own), `disabled` (dimmed, inert, out of the Tab
//! ring), a `label` when the text is not the name, a `description`, a
//! `tooltip` hint — and
//! `button_spec` / `button_palette` / `readable_on` are the pieces for a
//! button of the app's own colour that still reads as the same control.
//!
//! Every binding's `button` lowers to this one function, so none of them
//! can end up with a button of its own; the access rows it admits are
//! `schema::BUTTON_ROWS_JSX`.
//!
//! Run: cargo run -p kui-native --example button [-- --headless]

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

#[derive(Default)]
struct Buttons {
    last: Option<String>,
    presses: u32,
}

impl App for Buttons {
    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("`widgets::button` · the theme's accent trio (rest, hover, pressed; `accent` adds nothing to a stock spec), the ring on Tab, disabled", TextStyle::new(12.0).color(t.muted));
                ui.with(NodeSpec::row().gap(10.0), |ui| {
                    widgets::button(ui, "plain", Value::str("plain"));
                    widgets::button_with(
                        ui,
                        "accent",
                        "accent",
                        widgets::button_spec(&ui.theme(), &ui.metrics()).accent().on_click("accent"),
                        None,
                    );
                    widgets::button_with(
                        ui,
                        "disabled",
                        "disabled",
                        widgets::button_spec(&ui.theme(), &ui.metrics()).disabled(true).on_click("disabled"),
                        None,
                    );
                });

                ui.text("`button_with` · the access rows: a label, a description, a hint", TextStyle::new(12.0).color(t.muted));
                ui.with(NodeSpec::row().gap(10.0), |ui| {
                    // The text is a glyph; the name is what a reader says.
                    widgets::button_with(
                        ui,
                        "add",
                        "+",
                        widgets::button_spec(&ui.theme(), &ui.metrics()).on_click("add").label("add a row"),
                        None,
                    );
                    // A `description` is what a reader says after the
                    // name; a tooltip hint *is* one, so a button says it
                    // once (`tooltip`) and both the float and the reader
                    // have it.
                    widgets::button_with(
                        ui,
                        "delete",
                        "delete",
                        widgets::button_spec(&ui.theme(), &ui.metrics())
                            .on_click("delete")
                            .tooltip("removes the row for good — no undo"),
                        None,
                    );
                });

                ui.text("`button_spec` + `button_palette` · a button in the app's own colour, still the same control", TextStyle::new(12.0).color(t.muted));
                ui.with(NodeSpec::row().gap(10.0), |ui| {
                    for (name, base) in [
                        ("forest", Color::hex(0x2f7d4fff)),
                        ("plum", Color::hex(0x7d2f6bff)),
                        ("sand", Color::hex(0xe0c070ff)),
                    ] {
                        let (rest, hover, pressed) = widgets::button_palette(base);
                        let spec = widgets::button_spec(&ui.theme(), &ui.metrics())
                            .bg(rest)
                            .hover_bg(hover)
                            .pressed_bg(pressed)
                            .on_click(Value::str(name));
                        // Readable on the base, whichever it is — the rule
                        // the stock button applies to its own.
                        let fg = widgets::readable_on(rest);
                        ui.text_in_keyed(name, spec, name, TextStyle::new(widgets::BUTTON_TEXT).color(fg));
                    }
                });

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

    fn on_event(&mut self, ev: UiEvent) {
        if let Some(s) = ev.payload.as_str() {
            self.presses += 1;
            self.last = Some(s.to_string());
        }
    }
}

impl Example for Buttons {
    const KEYS: &'static [(&'static str, &'static str)] =
        &[("Tab", "the ring"), ("Enter / Space", "press")];

    fn window(&self) -> kui_devtools::Window {
        kui_devtools::Window::default().size(560.0, 320.0)
    }

    /// A click by label presses; a disabled button does not, and is not
    /// in the ring; the access rows come back as declared.
    fn headless(&mut self, core: &mut Core) -> Result<(), String> {
        let mut d = Drive::new(core, 560.0, 320.0);
        d.frame(self);
        let plain = d.key_of("plain").ok_or("no plain button")?;
        d.click_key(self, plain);
        d.check(
            self.last.as_deref() == Some("plain") && self.presses == 1,
            "a click presses",
        )?;
        let disabled = d.key_of("disabled").ok_or("no disabled button")?;
        d.click_key(self, disabled);
        d.check(self.presses == 1, "a disabled button is inert")?;
        let mut ring = Vec::new();
        for _ in 0..8 {
            d.input(
                self,
                kui_native::InputEvent::Key(kui_native::EditKey::Tab, Default::default()),
            );
            if let Some(k) = d.core.focus() {
                ring.push(d.core.label_of(k).unwrap_or("?").to_string());
            }
        }
        d.check(!ring.iter().any(|n| n == "disabled"), "and not in the ring")?;
        d.check(
            ring.iter().any(|n| n == "forest"),
            "the app-coloured button is a button like the rest",
        )?;

        let node = |d: &mut Drive<'_>, name: &str| {
            d.core
                .access_tree()
                .nodes
                .iter()
                .find(|n| n.name.as_deref() == Some(name))
                .map(|n| (n.role, n.description.clone()))
        };
        let add = node(&mut d, "add a row");
        d.check(
            add.is_some(),
            "a `label` is the name a reader says, not the glyph",
        )?;
        let delete = node(&mut d, "delete");
        d.check(
            delete.and_then(|(_, desc)| desc).as_deref()
                == Some("removes the row for good — no undo"),
            "and a tooltip hint is the `description` a reader says after the name",
        )
    }
}

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