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

//! Where the free space goes and what lines up (backlog C13), and a box
//! that keeps its shape (C14). Three panels:
//!
//!   - **mainAlign**: a track of four chips of different widths, laid out
//!     by whichever alignment the buttons above it pick — the three that
//!     put the chips together, and the three spreads that deal the free
//!     space out between them (`space-between`, `-around`, `-evenly`).
//!   - **crossAlign: baseline**: a reading and its unit in three sizes,
//!     once aligned at the top and once on their baselines, where the
//!     small label and unit sit on the line the large number stands on.
//!   - **aspectRatio**: a `grow`-wide card at 16:9 that keeps its shape as
//!     the window is resized, and a row of squares sized from a fixed
//!     height alone.
//!
//! Run: cargo run -p kui-native --example align [-- --headless]

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

/// The alignments the buttons pick, in `schema::ALIGNS` order; `baseline`
/// is a cross-axis value and has no button here.
const MAIN: [Align; 6] = [
    Align::Start,
    Align::Center,
    Align::End,
    Align::SpaceBetween,
    Align::SpaceAround,
    Align::SpaceEvenly,
];

const CHIPS: [f32; 4] = [48.0, 72.0, 36.0, 60.0];

struct Page {
    main: Align,
}

impl Page {
    fn main_panel(&self, ui: &mut Ui<'_>) {
        let t = ui.theme();
        let m = ui.metrics();
        ui.text("mainAlign", TextStyle::new(15.0).color(t.fg));
        ui.with(NodeSpec::row().gap(6.0).wrap().cross_gap(6.0), |ui| {
            for a in MAIN {
                let spec = widgets::button_spec(&t, &m).on_click(Value::str(a.name()));
                let spec = if a == self.main {
                    spec.accent()
                } else {
                    spec.bg(t.surface).hover_bg(t.hover).pressed_bg(t.pressed)
                };
                widgets::button_with(ui, a.name(), a.name(), spec, None);
            }
        });
        ui.with_keyed(
            "track",
            NodeSpec::row()
                .grow_width()
                .pad(8.0)
                .gap(4.0)
                .radius(m.radius)
                .bg(t.sunken)
                .main_align(self.main),
            |ui| {
                for (i, w) in CHIPS.into_iter().enumerate() {
                    ui.leaf_keyed(
                        &format!("chip{i}"),
                        NodeSpec::column()
                            .size(w, 24.0)
                            .radius(m.radius_inner)
                            .bg(t.accent),
                    );
                }
            },
        );
    }

    fn baseline_panel(&self, ui: &mut Ui<'_>) {
        let t = ui.theme();
        ui.text(
            "crossAlign: start, then baseline",
            TextStyle::new(15.0).color(t.fg),
        );
        for (label, cross) in [("top", Align::Start), ("base", Align::Baseline)] {
            ui.with_keyed(
                label,
                NodeSpec::row()
                    .gap(6.0)
                    .pad(8.0)
                    .bg(t.surface)
                    .cross_align(cross),
                |ui| {
                    for (part, text, size) in [
                        ("label", "Frame time", 13.0),
                        ("value", "8.3", 36.0),
                        ("unit", "ms", 13.0),
                    ] {
                        ui.with_keyed(part, NodeSpec::column(), |ui| {
                            let colour = if part == "value" { t.fg } else { t.muted };
                            ui.text(text, TextStyle::new(size).color(colour));
                        });
                    }
                },
            );
        }
    }

    fn aspect_panel(&self, ui: &mut Ui<'_>) {
        let t = ui.theme();
        ui.text("aspectRatio", TextStyle::new(15.0).color(t.fg));
        ui.with(NodeSpec::row().grow_width().gap(12.0), |ui| {
            ui.text_in_keyed(
                "video",
                NodeSpec::column()
                    .grow_width()
                    .max_width(360.0)
                    .aspect_ratio(16.0 / 9.0)
                    .radius(ui.metrics().radius)
                    .bg(t.raised)
                    .center(),
                "16 : 9",
                TextStyle::new(14.0).color(t.muted),
            );
            ui.with(NodeSpec::row().gap(6.0), |ui| {
                for (i, c) in [0x3b5bd4ffu32, 0x73d98cff, 0xffcc00ff]
                    .into_iter()
                    .enumerate()
                {
                    ui.leaf_keyed(
                        &format!("square{i}"),
                        NodeSpec::column()
                            .height(40.0)
                            .aspect_ratio(1.0)
                            .bg(Color::hex(c)),
                    );
                }
            });
        });
    }
}

impl App for Page {
    fn view(&mut self, ui: &mut Ui<'_>) {
        let t = ui.theme();
        ui.with(
            NodeSpec::column()
                .fill()
                .pad(24.0)
                .gap(14.0)
                .scroll_y()
                .bg(t.bg),
            |ui| {
                self.main_panel(ui);
                self.baseline_panel(ui);
                self.aspect_panel(ui);
            },
        );
    }

    fn on_event(&mut self, ev: UiEvent) {
        if let Some(name) = ev.payload.as_str()
            && let Some(a) = MAIN.into_iter().find(|a| a.name() == name)
        {
            self.main = a;
        }
    }
}

impl Example for Page {
    fn headless(&mut self, core: &mut Core) -> Result<(), String> {
        core.set_inspect(true);
        let mut d = Drive::new(core, 800.0, 600.0);
        let rect = |d: &mut Drive<'_>, label: &str| {
            let key = d.key_of(label).ok_or(format!("no {label}"))?;
            d.core
                .nodes()
                .into_iter()
                .find(|n| n.key == key)
                .map(|n| n.rect)
                .ok_or(format!("{label} is not in the snapshot"))
        };
        d.frame(self);

        // mainAlign: spaceBetween puts the first chip at the track's
        // padding and the last against its far padding.
        let between = d.key_of("spaceBetween").ok_or("no spaceBetween button")?;
        d.click_key(self, between);
        d.frame(self);
        let track = rect(&mut d, "track")?;
        let (first, last) = (rect(&mut d, "chip0")?, rect(&mut d, "chip3")?);
        d.check(
            (first.x - (track.x + 8.0)).abs() < 0.5,
            "the first chip starts the track",
        )?;
        d.check(
            (last.x + last.w - (track.x + track.w - 8.0)).abs() < 0.5,
            "and the last ends it",
        )?;
        let evenly = d.key_of("spaceEvenly").ok_or("no spaceEvenly button")?;
        d.click_key(self, evenly);
        d.frame(self);
        let (first, last) = (rect(&mut d, "chip0")?, rect(&mut d, "chip3")?);
        let track = rect(&mut d, "track")?;
        let lead = first.x - (track.x + 8.0);
        let tail = track.x + track.w - 8.0 - (last.x + last.w);
        d.check(
            lead > 1.0 && (lead - tail).abs() < 0.5,
            "spaceEvenly leaves equal ends",
        )?;

        // crossAlign: at the top the three tops meet; on the baseline the
        // small label drops below the large value's top.
        let tops: Vec<f32> = d
            .core
            .nodes()
            .into_iter()
            .filter(|n| n.label.as_deref() == Some("label") || n.label.as_deref() == Some("value"))
            .map(|n| n.rect.y)
            .collect();
        d.check(tops.len() == 4, "two rows of a label and a value")?;
        d.check(
            (tops[0] - tops[1]).abs() < 0.5,
            "aligned at the top, the tops meet",
        )?;
        d.check(
            tops[2] > tops[3] + 4.0,
            "on the baseline, the small label sits lower",
        )?;

        // aspectRatio: the card is 16:9 whatever width it grew to, and a
        // square is as wide as its fixed height.
        let video = rect(&mut d, "video")?;
        d.check(
            (video.w / video.h - 16.0 / 9.0).abs() < 0.01,
            "the card keeps 16:9",
        )?;
        let sq = rect(&mut d, "square0")?;
        d.check(
            (sq.w - 40.0).abs() < 0.01 && (sq.h - 40.0).abs() < 0.01,
            "a 40 px square",
        )?;
        let warned = d.core.take_warnings();
        d.check(warned.is_empty(), "nothing warned")?;
        Ok(())
    }
}

kui_devtools::main!(Page { main: Align::Start });