kui-native 0.1.0-alpha.45

Batteries-included kui runner: winit + wgpu around a Core, App trait, widgets
Documentation
//! `kui_native::testing::Drive` (backlog DX11), driving an app the way an
//! app's own test would: an owned `Core`, gestures through `view` and
//! `on_event`, and the readings a test asserts on.

use kui_native::testing::Drive;
use kui_native::{App, Core, KeyPhase, NodeSpec, TextStyle, Ui, UiEvent, Vec2};

#[derive(Default)]
struct Pad {
    clicks: u32,
    typed: String,
    split: f32,
    drags: Vec<&'static str>,
}

impl App for Pad {
    fn view(&mut self, ui: &mut Ui<'_>) {
        ui.with(NodeSpec::column().fill(), |ui| {
            ui.text_in_keyed(
                "add",
                NodeSpec::row().size(80.0, 30.0).on_click("add"),
                &format!("clicked {}", self.clicks),
                TextStyle::new(12.0),
            );
            ui.leaf_keyed("sink", NodeSpec::row().size(80.0, 30.0).on_key("keys"));
            ui.with(NodeSpec::row().size(200.0, 40.0), |ui| {
                ui.leaf_keyed("bar", NodeSpec::row().size(4.0, 40.0).on_drag("bar"));
            });
        });
    }

    fn on_event(&mut self, ev: UiEvent) {
        if let Some(d) = ev.drag() {
            self.split = d.ratio().x;
            self.drags.push(match d.phase {
                kui_native::DragPhase::Start => "start",
                kui_native::DragPhase::Move => "move",
                kui_native::DragPhase::End => "end",
            });
        } else if let Some((KeyPhase::Down, k)) = ev.key_press() {
            self.typed.extend(k.text);
        } else if ev.payload.as_str() == Some("add") {
            self.clicks += 1;
        }
    }
}

#[test]
fn an_owned_drive_clicks_types_drags_and_reads_back() {
    let mut app = Pad::default();
    let mut d = Drive::new(Core::new(), 400.0, 300.0).framing();
    d.frame(&mut app);

    let add = d.key_of("add").expect("the button");
    d.click_key(&mut app, add);
    assert_eq!(app.clicks, 1);
    assert_eq!(
        d.texts_under("add"),
        ["clicked 1"],
        "framed after the click"
    );
    let r = d.rect_of(add).expect("a rect");
    d.click(&mut app, r.center().x, r.center().y);
    assert_eq!(app.clicks, 2, "a pointer click at its middle");

    let sink = d.key_of("sink").expect("the sink");
    d.focus(&mut app, sink);
    d.keys(&mut app, "ab c");
    assert_eq!(app.typed, "ab c", "each press carries its text, space too");

    let bar = d.key_of("bar").unwrap();
    let bar = d.rect_of(bar).unwrap();
    d.drag(&mut app, bar.center(), Vec2::new(150.0, bar.center().y));
    assert_eq!(app.drags.first(), Some(&"start"));
    assert_eq!(app.drags.last(), Some(&"end"));
    assert!((app.split - 0.75).abs() < 0.01, "150 of 200: {}", app.split);

    assert!(d.warnings().is_empty(), "{:?}", d.warnings());
    assert!(d.frames() > 5, "framing frames after each gesture");
    assert!(d.log().iter().any(|l| l.contains("add")), "{:?}", d.log());
}

#[test]
fn a_borrowing_drive_frames_only_when_asked() {
    let mut app = Pad::default();
    let mut core = Core::new();
    let mut d = Drive::new(&mut core, 400.0, 300.0);
    d.frame(&mut app);
    let add = d.key_of("add").unwrap();
    d.click_key(&mut app, add);
    assert_eq!(app.clicks, 1);
    assert_eq!(d.texts_under("add"), ["clicked 0"], "no frame since");
    d.frame(&mut app);
    assert_eq!(d.texts_under("add"), ["clicked 1"]);
    assert_eq!(d.frames(), 2);
}

/// ADR 0036: a handler lent its window's core copies and moves focus in
/// answer to the event, where kawoosh parked `clip_out` and
/// `reclaim_focus` in its model for the next `view`.
#[test]
fn a_handler_acts_on_the_core_its_event_came_from() {
    use kui_native::MenuAction;

    struct Copier;
    impl App for Copier {
        fn view(&mut self, ui: &mut Ui<'_>) {
            ui.with(NodeSpec::row(), |ui| {
                ui.leaf_keyed("copy", NodeSpec::row().size(80.0, 30.0).on_click("copy"));
                ui.leaf_keyed("sink", NodeSpec::row().size(80.0, 30.0).on_key("keys"));
            });
        }
        fn on_event_with(&mut self, ev: UiEvent, core: &mut Core) {
            if ev.payload.as_str() == Some("copy") {
                core.set_clipboard("copied", None);
                let sink = core.key_of("sink");
                core.set_key_focus(sink);
            }
        }
    }

    let mut app = Copier;
    let mut d = Drive::new(Core::new(), 400.0, 300.0);
    d.frame(&mut app);
    let copy = d.key_of("copy").unwrap();
    d.click_key(&mut app, copy);
    let actions = d.core.take_menu_actions();
    assert!(
        actions
            .iter()
            .any(|a| matches!(a, MenuAction::SetClipboard { text, .. } if text == "copied")),
        "the write goes out with this turn: {actions:?}"
    );
    d.frame(&mut app);
    let sink = d.key_of("sink");
    assert_eq!(
        d.core.key_focus(),
        sink,
        "the focus lands for the next frame"
    );
}

/// DX18 through ADR 0036's door: a focus move a handler makes is the
/// program's, even when the next thing the window hears is an input
/// rather than the frame the move asked for (the alpha.22 regression
/// pass found it reported with that input's `by`).
#[test]
fn a_handlers_focus_move_is_reported_as_the_programs() {
    struct Mover;
    impl App for Mover {
        fn view(&mut self, ui: &mut Ui<'_>) {
            ui.with(NodeSpec::row(), |ui| {
                ui.leaf_keyed("go", NodeSpec::row().size(80.0, 30.0).on_click("go"));
                ui.with_keyed("pane", NodeSpec::row().on_focus("pane"), |ui| {
                    ui.leaf_keyed("sink", NodeSpec::row().size(80.0, 30.0).on_key("keys"));
                });
            });
        }
        fn on_event_with(&mut self, ev: UiEvent, core: &mut Core) {
            if ev.payload.as_str() == Some("go") {
                let sink = core.key_of("sink");
                core.set_key_focus(sink);
            }
        }
    }

    let mut app = Mover;
    let mut d = Drive::new(Core::new(), 400.0, 300.0);
    d.frame(&mut app);
    d.frame(&mut app);
    d.click(&mut app, 40.0, 15.0);
    let evs = d.move_to(&mut app, 300.0, 200.0);
    let by: Vec<_> = evs
        .iter()
        .filter(|e| e.kind() == Some("focus"))
        .map(|e| {
            let phase = e.payload.get_str("phase").unwrap();
            format!("{phase}:{}", e.payload.get_str("by").unwrap())
        })
        .collect();
    assert_eq!(by, ["in:program"]);
}

/// An app that overrides only `on_event` hears everything as before.
#[test]
fn the_plain_handler_still_hears_everything() {
    let mut app = Pad::default();
    let mut d = Drive::new(Core::new(), 400.0, 300.0);
    d.frame(&mut app);
    let add = d.key_of("add").unwrap();
    d.click_key(&mut app, add);
    assert_eq!(app.clicks, 1);
}

/// RG75: a test drives the middle button and a latched scroll gesture
/// through the drive's own doors, where it had only `input` for them.
#[test]
fn a_drive_presses_the_middle_button_and_scrolls_a_gesture() {
    use kui_native::{Buttons, MouseButton};
    #[derive(Default)]
    struct Pane {
        heard: Vec<String>,
    }
    impl App for Pane {
        fn view(&mut self, ui: &mut Ui<'_>) {
            ui.with_keyed(
                "page",
                NodeSpec::column().size(200.0, 100.0).scroll_y(),
                |ui| {
                    ui.leaf_keyed(
                        "pane",
                        NodeSpec::row()
                            .size(200.0, 400.0)
                            .on_button("pane")
                            .buttons(Buttons::MIDDLE),
                    );
                },
            );
        }
        fn on_event(&mut self, ev: UiEvent) {
            if let Some(b) = ev.button() {
                self.heard.push(format!("{:?}", b.phase));
            }
        }
    }
    let mut app = Pane::default();
    let mut d = Drive::new(Core::new(), 400.0, 300.0).framing();
    d.frame(&mut app);
    d.frame(&mut app);
    d.button_click(&mut app, 50.0, 50.0, MouseButton::Middle);
    assert_eq!(app.heard, ["Press", "Release"]);
    d.scroll_gesture(&mut app, 50.0, 50.0, Vec2::new(0.0, -20.0), true);
    d.scroll_gesture(&mut app, 50.0, 50.0, Vec2::new(0.0, -20.0), false);
    let page = d.key_of("page").unwrap();
    assert_eq!(d.core.scroll_offset(page), Vec2::new(0.0, 40.0));
}

/// Why a frame runs, read where an app reads it — `ui.core()` in its
/// `view` (backlog F111): the input the frame answers, by kind, and who
/// held the frame the last one owed, down to the line of the app's own
/// `request_frame`.
#[test]
fn a_view_reads_why_its_frame_runs() {
    use kui_native::{FrameCause, KeyMods};

    #[derive(Default)]
    struct Ledger {
        ask: bool,
        asked_at: u32,
        seen: Vec<(FrameCause, Vec<(&'static str, u32)>)>,
    }
    impl App for Ledger {
        fn view(&mut self, ui: &mut Ui<'_>) {
            let core = ui.core();
            let asks = core
                .owed_by()
                .requests
                .iter()
                .map(|r| (r.at.file(), r.at.line()))
                .collect();
            self.seen.push((core.frame_cause(), asks));
            ui.leaf_keyed("sink", NodeSpec::row().size(80.0, 30.0).on_key("keys"));
            if std::mem::take(&mut self.ask) {
                self.asked_at = line!() + 1;
                ui.request_frame();
            }
        }
    }

    let mut app = Ledger::default();
    let mut d = Drive::new(Core::new(), 200.0, 100.0);
    d.core.set_frame_trace(true);
    d.frame(&mut app);
    let sink = d.key_of("sink").expect("the sink");
    d.focus(&mut app, sink);
    d.frame(&mut app);
    d.key(&mut app, "a", KeyMods::default());
    app.ask = true;
    d.frame(&mut app);
    d.frame(&mut app);

    let (cause, _) = &app.seen[2];
    assert!(cause.contains(FrameCause::KEY), "{cause:?}");
    let (cause, asks) = &app.seen[3];
    assert_eq!(*cause, FrameCause::OWED);
    assert_eq!(asks.len(), 1);
    assert!(asks[0].0.ends_with("drive.rs"), "{asks:?}");
    assert_eq!(asks[0].1, app.asked_at);
}