tests.rs raw

use std::cell::RefCell;
use std::rc::Rc;

use guiduck_scene::geom::{Point, Size};
use guiduck_scene::paint::color::palette::css;
use taffy::prelude::{length, percent};

use super::*;
use crate::clipboard::NoClipboard;
use crate::text::TextContext;
use crate::widget::{Button, Container, Text, WidgetTree};

type Log = Rc<RefCell<Vec<String>>>;

fn log_handler(log: &Log, name: &str) -> impl FnMut(&mut EventCtx, &EventData) + 'static {
    let log = log.clone();
    let name = name.to_owned();
    move |_ctx, _ev| log.borrow_mut().push(name.clone())
}

/// Root (400x300) containing a 100x100 box at (50, 50) containing a 40x40
/// box at its (10, 10).
fn fixture() -> (WidgetTree, WidgetId, WidgetId, WidgetId) {
    let mut tree = WidgetTree::with_text_context(TextContext::hermetic([]));
    let root = tree.insert(
        Container::new(),
        taffy::Style {
            size: taffy::Size {
                width: percent(1.0_f32),
                height: percent(1.0_f32),
            },
            padding: taffy::Rect::length(50.0_f32),
            ..Default::default()
        },
        None,
    );
    let outer = tree.insert(
        Container::new(),
        taffy::Style {
            size: taffy::Size {
                width: length(100.0_f32),
                height: length(100.0_f32),
            },
            padding: taffy::Rect::length(10.0_f32),
            ..Default::default()
        },
        Some(root),
    );
    let inner = tree.insert(
        Container::new(),
        taffy::Style {
            size: taffy::Size {
                width: length(40.0_f32),
                height: length(40.0_f32),
            },
            ..Default::default()
        },
        Some(outer),
    );
    tree.compute_layout(Size::new(400.0, 300.0));
    (tree, root, outer, inner)
}

#[test]
fn hit_test_finds_topmost_descendant() {
    let (tree, root, outer, inner) = fixture();
    assert_eq!(tree.hit_test(Point::new(65.0, 65.0)), Some(inner));
    assert_eq!(tree.hit_test(Point::new(140.0, 140.0)), Some(outer));
    assert_eq!(tree.hit_test(Point::new(5.0, 5.0)), Some(root));
    assert_eq!(tree.hit_test(Point::new(500.0, 500.0)), None);
}

#[test]
fn enter_and_leave_follow_the_hover_chain() {
    let (mut tree, root, outer, inner) = fixture();
    let log: Log = Log::default();
    for (id, name) in [(root, "root"), (outer, "outer"), (inner, "inner")] {
        tree.on_event(
            id,
            EventKind::PointerEnter,
            log_handler(&log, &format!("enter-{name}")),
        );
        tree.on_event(
            id,
            EventKind::PointerLeave,
            log_handler(&log, &format!("leave-{name}")),
        );
    }

    tree.dispatch_pointer(
        PointerInput::Moved(Point::new(65.0, 65.0)),
        &mut NoClipboard,
    );
    assert_eq!(
        log.borrow().as_slice(),
        ["enter-root", "enter-outer", "enter-inner"],
        "enters fire outermost-first"
    );

    log.borrow_mut().clear();
    // Move to a point inside outer but outside inner.
    tree.dispatch_pointer(
        PointerInput::Moved(Point::new(140.0, 140.0)),
        &mut NoClipboard,
    );
    assert_eq!(
        log.borrow().as_slice(),
        ["leave-inner"],
        "only the widget actually left fires"
    );

    log.borrow_mut().clear();
    tree.dispatch_pointer(PointerInput::Left, &mut NoClipboard);
    assert_eq!(
        log.borrow().as_slice(),
        ["leave-outer", "leave-root"],
        "leaves fire innermost-first"
    );
}

#[test]
fn capture_then_bubble_with_stop_propagation() {
    let (mut tree, root, outer, inner) = fixture();
    let log: Log = Log::default();
    for (id, name) in [(root, "root"), (outer, "outer"), (inner, "inner")] {
        tree.add_handler(
            id,
            EventKind::PointerDown,
            Phase::Capture,
            log_handler(&log, &format!("capture-{name}")),
        );
        tree.on_event(
            id,
            EventKind::PointerDown,
            log_handler(&log, &format!("bubble-{name}")),
        );
    }

    tree.dispatch_pointer(
        PointerInput::Down {
            time: std::time::Instant::now(),
            pos: Point::new(65.0, 65.0),
            button: PointerButton::Left,
        },
        &mut NoClipboard,
    );
    assert_eq!(
        log.borrow().as_slice(),
        [
            "capture-root",
            "capture-outer",
            "capture-inner",
            "bubble-inner",
            "bubble-outer",
            "bubble-root",
        ]
    );

    // Stopping in outer's bubble handler prevents root's bubble handler.
    log.borrow_mut().clear();
    tree.on_event(outer, EventKind::PointerUp, |ctx, _ev| {
        ctx.stop_propagation()
    });
    tree.on_event(
        root,
        EventKind::PointerUp,
        log_handler(&log, "bubble-root-up"),
    );
    tree.dispatch_pointer(
        PointerInput::Up {
            pos: Point::new(65.0, 65.0),
            button: PointerButton::Left,
        },
        &mut NoClipboard,
    );
    assert!(
        !log.borrow().iter().any(|e| e == "bubble-root-up"),
        "propagation stopped before the root"
    );
}

#[test]
fn click_requires_press_and_release_on_same_target() {
    let (mut tree, root, outer, inner) = fixture();
    let log: Log = Log::default();
    tree.on_event(inner, EventKind::Click, log_handler(&log, "click-inner"));
    tree.on_event(outer, EventKind::Click, log_handler(&log, "click-outer"));
    let _ = root;

    // Press and release on inner: click (which also bubbles through outer).
    tree.dispatch_pointer(
        PointerInput::Down {
            time: std::time::Instant::now(),
            pos: Point::new(65.0, 65.0),
            button: PointerButton::Left,
        },
        &mut NoClipboard,
    );
    tree.dispatch_pointer(
        PointerInput::Up {
            pos: Point::new(66.0, 66.0),
            button: PointerButton::Left,
        },
        &mut NoClipboard,
    );
    assert_eq!(log.borrow().as_slice(), ["click-inner", "click-outer"]);

    // Press on inner, drag out, release on outer: no click anywhere.
    log.borrow_mut().clear();
    tree.dispatch_pointer(
        PointerInput::Down {
            time: std::time::Instant::now(),
            pos: Point::new(65.0, 65.0),
            button: PointerButton::Left,
        },
        &mut NoClipboard,
    );
    tree.dispatch_pointer(
        PointerInput::Up {
            pos: Point::new(140.0, 140.0),
            button: PointerButton::Left,
        },
        &mut NoClipboard,
    );
    assert!(log.borrow().is_empty());
}

/// A CountedClick handler on the path holds the click until the burst window
/// closes, then delivers one event carrying the confirmed count.
#[test]
fn counted_click_resolves_after_the_window() {
    let (mut tree, _root, _outer, inner) = fixture();
    let log: Log = Log::default();
    {
        let log = log.clone();
        tree.on_event(inner, EventKind::CountedClick, move |_ctx, ev| {
            let count = ev.pointer().map_or(0, |p| p.click_count);
            log.borrow_mut().push(format!("counted-{count}"));
        });
    }
    let t0 = std::time::Instant::now();
    let ms = std::time::Duration::from_millis;

    tree.dispatch_pointer(
        PointerInput::Down {
            time: t0,
            pos: Point::new(65.0, 65.0),
            button: PointerButton::Left,
        },
        &mut NoClipboard,
    );
    tree.dispatch_pointer(
        PointerInput::Up {
            pos: Point::new(65.0, 65.0),
            button: PointerButton::Left,
        },
        &mut NoClipboard,
    );
    assert!(
        log.borrow().is_empty(),
        "CountedClick is held until the window closes"
    );

    let deadline = tree
        .next_wake(t0)
        .expect("a pending burst schedules a wake");
    tree.tick(t0 + ms(100));
    assert!(log.borrow().is_empty(), "still pending inside the window");

    tree.tick(deadline);
    assert_eq!(
        log.borrow().as_slice(),
        ["counted-1"],
        "one confirmed single"
    );
}

/// A double within the window resolves to a single CountedClick of count 2 —
/// never a count-1 event first — because the window is anchored at the first
/// press and the count is only reported once settled.
#[test]
fn counted_click_coalesces_a_double() {
    let (mut tree, _root, _outer, inner) = fixture();
    let log: Log = Log::default();
    {
        let log = log.clone();
        tree.on_event(inner, EventKind::CountedClick, move |_ctx, ev| {
            let count = ev.pointer().map_or(0, |p| p.click_count);
            log.borrow_mut().push(format!("counted-{count}"));
        });
    }
    let t0 = std::time::Instant::now();
    let ms = std::time::Duration::from_millis;

    for press in [0_u64, 80] {
        tree.dispatch_pointer(
            PointerInput::Down {
                time: t0 + ms(press),
                pos: Point::new(65.0, 65.0),
                button: PointerButton::Left,
            },
            &mut NoClipboard,
        );
        tree.dispatch_pointer(
            PointerInput::Up {
                pos: Point::new(65.0, 65.0),
                button: PointerButton::Left,
            },
            &mut NoClipboard,
        );
    }
    assert!(log.borrow().is_empty(), "held through the whole burst");

    // Anchored at the first down, the window closes at t0 + 200ms.
    tree.tick(t0 + ms(200));
    assert_eq!(
        log.borrow().as_slice(),
        ["counted-2"],
        "one event carrying the final count"
    );
}

/// A burst settles a whole window after the press that started it, and the
/// widget it landed on can be gone by then — an application whose handler
/// rebuilt the region the click was in. The resolution has no one to tell, and
/// says so rather than asking the tree about a widget it no longer has.
#[test]
fn a_counted_click_whose_target_was_removed_is_dropped() {
    let (mut tree, _root, _outer, inner) = fixture();
    let log: Log = Log::default();
    tree.on_event(inner, EventKind::CountedClick, log_handler(&log, "counted"));
    let t0 = std::time::Instant::now();
    let ms = std::time::Duration::from_millis;

    tree.dispatch_pointer(
        PointerInput::Down {
            time: t0,
            pos: Point::new(65.0, 65.0),
            button: PointerButton::Left,
        },
        &mut NoClipboard,
    );
    tree.dispatch_pointer(
        PointerInput::Up {
            pos: Point::new(65.0, 65.0),
            button: PointerButton::Left,
        },
        &mut NoClipboard,
    );

    // The click's own handling retired the widget it landed on, before the
    // window it is held for has closed.
    tree.remove(inner);

    tree.tick(t0 + ms(200));
    assert!(
        log.borrow().is_empty(),
        "a widget that is gone hears nothing"
    );
}

/// With nothing on the path distinguishing counts, a click is never held: the
/// Click fires immediately and no wake is scheduled.
#[test]
fn a_plain_click_path_is_never_deferred() {
    let (mut tree, _root, _outer, inner) = fixture();
    let log: Log = Log::default();
    tree.on_event(inner, EventKind::Click, log_handler(&log, "click"));
    let t0 = std::time::Instant::now();

    tree.dispatch_pointer(
        PointerInput::Down {
            time: t0,
            pos: Point::new(65.0, 65.0),
            button: PointerButton::Left,
        },
        &mut NoClipboard,
    );
    tree.dispatch_pointer(
        PointerInput::Up {
            pos: Point::new(65.0, 65.0),
            button: PointerButton::Left,
        },
        &mut NoClipboard,
    );
    assert_eq!(log.borrow().as_slice(), ["click"], "Click is immediate");
    assert_eq!(
        tree.next_wake(t0),
        None,
        "no pending burst, so no wake owed"
    );
}

/// A button (or checkbox) consumes its click: the widget's own handler runs,
/// but the click does not also bubble to an enclosing container's `:on-click`.
/// A click on the container alone still fires it.
#[test]
fn a_button_consumes_its_click() {
    let mut tree = WidgetTree::with_text_context(TextContext::hermetic([]));
    let root = tree.insert(
        Container::new(),
        taffy::Style {
            size: taffy::Size {
                width: percent(1.0_f32),
                height: percent(1.0_f32),
            },
            ..Default::default()
        },
        None,
    );
    let button = tree.insert(
        Button::new("go"),
        taffy::Style {
            size: taffy::Size {
                width: length(80.0_f32),
                height: length(30.0_f32),
            },
            ..Default::default()
        },
        Some(root),
    );
    tree.compute_layout(Size::new(200.0, 100.0));

    let log: Log = Log::default();
    tree.on_event(root, EventKind::Click, log_handler(&log, "container"));
    tree.on_event(button, EventKind::Click, log_handler(&log, "button"));

    let click = |tree: &mut WidgetTree, pos: Point| {
        tree.dispatch_pointer(
            PointerInput::Down {
                time: std::time::Instant::now(),
                pos,
                button: PointerButton::Left,
            },
            &mut NoClipboard,
        );
        tree.dispatch_pointer(
            PointerInput::Up {
                pos,
                button: PointerButton::Left,
            },
            &mut NoClipboard,
        );
    };

    // Click on the button (its top-left is the root's origin; center ~40,15).
    click(&mut tree, Point::new(40.0, 15.0));
    assert_eq!(
        log.borrow().as_slice(),
        ["button"],
        "the button's click is its own and did not bubble to the container",
    );

    // Click on the container, outside the button.
    log.borrow_mut().clear();
    click(&mut tree, Point::new(150.0, 90.0));
    assert_eq!(
        log.borrow().as_slice(),
        ["container"],
        "a click on the container alone still fires it",
    );
}

#[test]
fn local_coordinates_are_widget_relative() {
    let (mut tree, _root, _outer, inner) = fixture();
    let seen: Rc<RefCell<Option<PointerEvent>>> = Rc::default();
    let seen2 = seen.clone();
    tree.on_event(inner, EventKind::PointerDown, move |_ctx, ev| {
        *seen2.borrow_mut() = ev.pointer().copied();
    });
    tree.dispatch_pointer(
        PointerInput::Down {
            time: std::time::Instant::now(),
            pos: Point::new(65.0, 72.0),
            button: PointerButton::Left,
        },
        &mut NoClipboard,
    );
    let event = seen.borrow().expect("handler ran");
    // inner's absolute origin is (60, 60): root padding 50 + outer padding 10.
    assert_eq!(event.local, Point::new(5.0, 12.0));
    assert_eq!(event.window, Point::new(65.0, 72.0));
    assert_eq!(event.button, Some(PointerButton::Left));
}

#[test]
fn signal_binding_updates_widget_through_frames() {
    let mut tree = WidgetTree::with_text_context(TextContext::hermetic([sample_font()]));
    let root = tree.insert(
        Container::new(),
        taffy::Style {
            size: taffy::Size {
                width: percent(1.0_f32),
                height: percent(1.0_f32),
            },
            ..Default::default()
        },
        None,
    );
    let label = tree.insert(
        Text::new("initial").family("DejaVu Sans"),
        taffy::Style::default(),
        Some(root),
    );

    let count = guiduck_signals::Signal::new(0);
    tree.bind::<Text, _>(
        label,
        move || format!("count: {}", count.get()),
        |text, value| text.set_text(value),
    );

    assert!(tree.needs_frame(), "initial binding queued a command");
    tree.render_frame(Size::new(300.0, 200.0));
    assert_eq!(tree.widget::<Text>(label).unwrap().text(), "count: 0");
    assert!(!tree.needs_frame(), "quiescent after the frame");

    count.set(7);
    assert!(tree.needs_frame(), "signal write schedules a frame");
    tree.render_frame(Size::new(300.0, 200.0));
    assert_eq!(tree.widget::<Text>(label).unwrap().text(), "count: 7");
    assert!(!tree.needs_frame());

    count.set(7);
    assert!(!tree.needs_frame(), "equal write must not schedule a frame");
}

#[test]
fn handler_commands_mutate_widgets_next_frame() {
    let (mut tree, _root, outer, _inner) = fixture();
    tree.on_event(outer, EventKind::PointerEnter, move |ctx, _ev| {
        ctx.mutate::<Container>(ctx.current, |c| {
            c.set_background(css::ORANGE);
        });
    });

    tree.dispatch_pointer(
        PointerInput::Moved(Point::new(140.0, 140.0)),
        &mut NoClipboard,
    );
    assert!(tree.needs_frame(), "hover queued a mutation");
    tree.render_frame(Size::new(400.0, 300.0));
    assert!(!tree.needs_frame());
}

fn sample_font() -> guiduck_scene::paint::Blob<u8> {
    static FONT: &[u8] = include_bytes!("../../../guiduck-samples/fonts/DejaVuSans.ttf");
    guiduck_scene::paint::Blob::new(std::sync::Arc::new(FONT))
}

#[test]
fn disabled_widgets_are_inert_but_their_enabled_ancestors_react() {
    let (mut tree, _root, outer, inner) = fixture();
    let log: Log = Log::default();
    tree.on_event(inner, EventKind::Click, log_handler(&log, "inner"));
    tree.on_event(outer, EventKind::Click, log_handler(&log, "outer"));
    tree.set_enabled(inner, false);

    // A click landing on the disabled inner box: inner is inert, but the
    // press behaves as a click on outer (as if on its padding).
    let pos = Point::new(70.0, 70.0);
    tree.dispatch_pointer(
        PointerInput::Down {
            time: std::time::Instant::now(),
            pos,
            button: PointerButton::Left,
        },
        &mut NoClipboard,
    );
    tree.dispatch_pointer(
        PointerInput::Up {
            pos,
            button: PointerButton::Left,
        },
        &mut NoClipboard,
    );
    assert_eq!(log.borrow().as_slice(), ["outer"]);

    // Hover likewise: outer hovers, inner does not.
    tree.dispatch_pointer(PointerInput::Moved(pos), &mut NoClipboard);
    assert!(tree.interaction_state(outer).hover);
    assert!(!tree.interaction_state(inner).hover);
    assert!(tree.interaction_state(inner).disabled);
    assert!(!tree.interaction_state(outer).disabled);
}

#[test]
fn disabling_an_ancestor_disables_the_subtree() {
    let (mut tree, _root, outer, inner) = fixture();
    let log: Log = Log::default();
    tree.on_event(inner, EventKind::Click, log_handler(&log, "inner"));
    tree.set_enabled(outer, false);

    assert!(tree.effectively_disabled(inner), "inherited");
    assert!(tree.is_enabled(inner), "own flag untouched");

    let pos = Point::new(70.0, 70.0);
    tree.dispatch_pointer(
        PointerInput::Down {
            time: std::time::Instant::now(),
            pos,
            button: PointerButton::Left,
        },
        &mut NoClipboard,
    );
    tree.dispatch_pointer(
        PointerInput::Up {
            pos,
            button: PointerButton::Left,
        },
        &mut NoClipboard,
    );
    assert!(log.borrow().is_empty(), "the whole subtree is inert");
}

#[test]
fn disabling_the_focused_widget_moves_focus() {
    let mut tree = WidgetTree::with_text_context(TextContext::hermetic([sample_font()]));
    let root = tree.insert(
        Container::new(),
        taffy::Style {
            size: taffy::Size {
                width: percent(1.0_f32),
                height: percent(1.0_f32),
            },
            ..Default::default()
        },
        None,
    );
    let first = tree.insert(
        crate::widget::TextInput::new(13.0).family("DejaVu Sans"),
        taffy::Style::default(),
        Some(root),
    );
    let second = tree.insert(
        crate::widget::TextInput::new(13.0).family("DejaVu Sans"),
        taffy::Style::default(),
        Some(root),
    );
    tree.render_frame(Size::new(400.0, 300.0));
    assert_eq!(tree.focus(), Some(first));

    tree.set_enabled(first, false);
    tree.render_frame(Size::new(400.0, 300.0));
    assert_eq!(
        tree.focus(),
        Some(second),
        "focus leaves a disabled widget for the next focusable"
    );

    // Tab traversal skips the disabled one entirely.
    tree.focus_step(false);
    assert_eq!(tree.focus(), Some(second), "only one focusable remains");
}

#[test]
fn shortcuts_fire_only_when_the_focused_widget_declines() {
    let mut tree = WidgetTree::with_text_context(TextContext::hermetic([sample_font()]));
    let root = tree.insert(
        Container::new(),
        taffy::Style {
            size: taffy::Size {
                width: percent(1.0_f32),
                height: percent(1.0_f32),
            },
            ..Default::default()
        },
        None,
    );
    tree.insert(
        crate::widget::TextInput::new(13.0).family("DejaVu Sans"),
        taffy::Style {
            size: taffy::Size {
                width: length(100.0_f32),
                height: length(24.0_f32),
            },
            ..Default::default()
        },
        Some(root),
    );
    tree.render_frame(Size::new(400.0, 300.0));

    let fired = Rc::new(RefCell::new(0));
    {
        let fired = fired.clone();
        tree.on_shortcut(Keystroke::ctrl("s"), move || {
            *fired.borrow_mut() += 1;
        });
    }
    let bare = Rc::new(RefCell::new(0));
    {
        let bare = bare.clone();
        tree.on_shortcut(
            Keystroke {
                key: Key::Character("s".into()),
                modifiers: Modifiers::default(),
            },
            move || {
                *bare.borrow_mut() += 1;
            },
        );
    }

    // Ctrl+S: the text input declines it, the accelerator fires.
    let consumed = tree.dispatch_key(
        &KeyInput {
            key: Key::Character("s".into()),
            modifiers: Modifiers {
                ctrl: true,
                ..Default::default()
            },
            pressed: true,
        },
        &mut NoClipboard,
    );
    assert!(consumed);
    assert_eq!(*fired.borrow(), 1);

    // Bare "s": the focused text input types it; the bare shortcut must
    // not steal the keystroke.
    tree.dispatch_key(
        &KeyInput {
            key: Key::Character("s".into()),
            modifiers: Modifiers::default(),
            pressed: true,
        },
        &mut NoClipboard,
    );
    assert_eq!(*bare.borrow(), 0, "typing beats bare-letter shortcuts");
}

/// Every key an accelerator can *declare* spells the same as the key the
/// platform *sends*, so `:accel "Ctrl+S"` and `event.key` are one notation.
///
/// The two vocabularies are deliberately different sets — `Key` has
/// `Backspace`, `Tab`, and `Other`, which an accelerator may not name — but
/// where they overlap they must agree, and the match below is exhaustive, so
/// a new `AccelKey` variant does not compile until it has been given its
/// runtime counterpart here.
#[test]
fn an_accelerator_and_an_observed_key_spell_the_same() {
    use guiduck_component_core::accel::{AccelIr, AccelKey};

    let cases = [
        AccelKey::Character("S".into()),
        AccelKey::Enter,
        AccelKey::Delete,
        AccelKey::Home,
        AccelKey::End,
        AccelKey::Left,
        AccelKey::Right,
        AccelKey::Up,
        AccelKey::Down,
    ];
    for accel_key in cases {
        let key = match &accel_key {
            AccelKey::Character(c) => Key::Character(c.clone()),
            AccelKey::Enter => Key::Enter,
            AccelKey::Delete => Key::Delete,
            AccelKey::Home => Key::Home,
            AccelKey::End => Key::End,
            AccelKey::Left => Key::Left,
            AccelKey::Right => Key::Right,
            AccelKey::Up => Key::Up,
            AccelKey::Down => Key::Down,
        };
        // Bare, and under every modifier, so the join order is pinned too.
        for (ctrl, alt, shift, logo) in [
            (false, false, false, false),
            (true, false, false, false),
            (true, true, true, true),
            (false, false, true, false),
        ] {
            let declared = AccelIr {
                key: accel_key.clone(),
                ctrl,
                shift,
                alt,
                logo,
            };
            let observed = KeyInput {
                key: key.clone(),
                modifiers: Modifiers {
                    ctrl,
                    shift,
                    alt,
                    logo,
                },
                pressed: true,
            };
            assert_eq!(
                declared.display(),
                observed.spelling().expect("a named key"),
                "{accel_key:?} spells differently declared and observed"
            );
        }
    }
}

/// The key we did not model has no name, which is what stops `:on-key` from
/// reporting a keystroke an application could never match.
#[test]
fn an_unmodelled_key_has_no_spelling() {
    assert_eq!(Key::Other.name(), None);
    assert_eq!(
        KeyInput {
            key: Key::Other,
            modifiers: Modifiers {
                ctrl: true,
                ..Default::default()
            },
            pressed: true,
        }
        .spelling(),
        None,
    );
}

/// A tree with two buttons under one container, and the layout computed.
fn two_buttons() -> (WidgetTree, WidgetId, WidgetId, WidgetId) {
    let mut tree = WidgetTree::with_text_context(TextContext::hermetic([sample_font()]));
    let root = tree.insert(Container::new(), taffy::Style::default(), None);
    let first = tree.insert(Button::new("first"), taffy::Style::default(), Some(root));
    let second = tree.insert(Button::new("second"), taffy::Style::default(), Some(root));
    tree.compute_layout(Size::new(400.0, 300.0));
    (tree, root, first, second)
}

/// `:focused true` takes focus, and `false` gives it up.
#[test]
fn set_focused_drives_focus_both_ways() {
    let (mut tree, _root, first, _second) = two_buttons();

    tree.set_focused(first, true);
    assert_eq!(tree.focus(), Some(first));

    tree.set_focused(first, false);
    assert_eq!(tree.focus(), None);
}

/// The asymmetry that makes `:focused` order-independent: clearing a widget
/// that is *not* focused does nothing, so when focus moves between two widgets
/// driven by one piece of state, the falling and rising bindings can run in
/// either order and land in the same place.
#[test]
fn clearing_focused_on_an_unfocused_widget_does_nothing() {
    let (mut tree, _root, first, second) = two_buttons();

    // The order a reconciler happens to produce: the new widget rises first,
    // then the old one falls. The stale `false` must not undo the `true`.
    tree.set_focused(first, true);
    tree.set_focused(second, true);
    tree.set_focused(first, false);
    assert_eq!(tree.focus(), Some(second), "the stale clear was ignored");

    // And the other order, for the same end state.
    let (mut tree, _root, first, second) = two_buttons();
    tree.set_focused(first, true);
    tree.set_focused(first, false);
    tree.set_focused(second, true);
    assert_eq!(tree.focus(), Some(second));
}

/// Focus changes report to the widgets that gained and lost it — and to
/// nobody else, because "am I focused" is not a fact an ancestor can answer.
#[test]
fn focus_changes_report_to_the_widgets_themselves() {
    let (mut tree, root, first, second) = two_buttons();
    let log: Log = Rc::new(RefCell::new(Vec::new()));

    for (id, name) in [(root, "root"), (first, "first"), (second, "second")] {
        let log = log.clone();
        let name = name.to_owned();
        tree.on_event(id, EventKind::FocusChange, move |_ctx, ev| {
            let EventData::FocusChanged(focused) = ev else {
                panic!("focus wires receive FocusChanged");
            };
            log.borrow_mut().push(format!("{name}={focused}"));
        });
    }

    tree.set_focus(Some(first));
    assert_eq!(*log.borrow(), ["first=true"]);

    tree.set_focus(Some(second));
    assert_eq!(
        *log.borrow(),
        ["first=true", "first=false", "second=true"],
        "the loser hears first, and the container hears nothing"
    );
}

/// A handler that asks the tree during a focus report sees the settled
/// answer, not the transition it is being told about.
#[test]
fn a_focus_report_arrives_after_focus_has_moved() {
    let (mut tree, _root, first, _second) = two_buttons();
    let seen: Rc<RefCell<Option<bool>>> = Rc::new(RefCell::new(None));
    let commands = tree.commands();
    let seen_in_handler = seen.clone();
    let target = first;
    tree.on_event(first, EventKind::FocusChange, move |_ctx, _ev| {
        let seen = seen_in_handler.clone();
        commands.push(move |tree| *seen.borrow_mut() = Some(tree.focus() == Some(target)));
    });

    tree.set_focus(Some(first));
    tree.render_frame(Size::new(400.0, 300.0));
    assert_eq!(*seen.borrow(), Some(true));
}

/// A key the focused widget declines reaches its `:on-key` wire, and then its
/// ancestors' — outwards only, so the widget it was typed into always answers
/// first.
#[test]
fn an_unconsumed_key_bubbles_from_the_focused_widget() {
    let (mut tree, root, first, _second) = two_buttons();
    let log: Log = Rc::new(RefCell::new(Vec::new()));

    for (id, name) in [(root, "root"), (first, "button")] {
        let log = log.clone();
        let name = name.to_owned();
        tree.on_event(id, EventKind::Key, move |_ctx, ev| {
            let EventData::Key(spelling) = ev else {
                panic!("key wires receive Key");
            };
            log.borrow_mut().push(format!("{name}:{spelling}"));
        });
    }

    tree.set_focus(Some(first));
    let used = tree.dispatch_key(
        &KeyInput {
            key: Key::Escape,
            modifiers: Modifiers::default(),
            pressed: true,
        },
        &mut NoClipboard,
    );

    assert_eq!(*log.borrow(), ["button:Escape", "root:Escape"]);
    assert!(!used, "nobody consumed it, so the key is still unclaimed");
}

/// Consuming the event consumes the key: it stops bubbling, and the layers
/// below (overlay dismissal, shortcuts, Tab) never see it.
#[test]
fn consuming_a_key_event_consumes_the_key() {
    let (mut tree, root, first, _second) = two_buttons();
    let reached_root = Rc::new(RefCell::new(0));
    let shortcut_fired = Rc::new(RefCell::new(0));

    tree.on_event(first, EventKind::Key, |ctx, _ev| ctx.stop_propagation());
    let seen = reached_root.clone();
    tree.on_event(root, EventKind::Key, move |_ctx, _ev| {
        *seen.borrow_mut() += 1;
    });
    let fired = shortcut_fired.clone();
    tree.on_shortcut(
        Keystroke {
            key: Key::Escape,
            modifiers: Modifiers::default(),
        },
        move || *fired.borrow_mut() += 1,
    );

    tree.set_focus(Some(first));
    let used = tree.dispatch_key(
        &KeyInput {
            key: Key::Escape,
            modifiers: Modifiers::default(),
            pressed: true,
        },
        &mut NoClipboard,
    );

    assert!(used);
    assert_eq!(*reached_root.borrow(), 0, "consuming stopped the bubble");
    assert_eq!(*shortcut_fired.borrow(), 0, "and stopped the shortcut");
}

/// The widget's own handling still comes first: what a control consumes never
/// reaches a key wire, so typing into a field cannot trigger an app's
/// bare-letter action.
#[test]
fn a_key_the_focused_widget_consumes_never_reaches_a_wire() {
    let mut tree = WidgetTree::with_text_context(TextContext::hermetic([sample_font()]));
    let root = tree.insert(Container::new(), taffy::Style::default(), None);
    let field = tree.insert(
        crate::widget::TextInput::new(14.0),
        taffy::Style::default(),
        Some(root),
    );
    tree.compute_layout(Size::new(400.0, 300.0));

    let saw = Rc::new(RefCell::new(0));
    let seen = saw.clone();
    tree.on_event(root, EventKind::Key, move |_ctx, _ev| {
        *seen.borrow_mut() += 1;
    });

    tree.set_focus(Some(field));
    tree.dispatch_key(
        &KeyInput {
            key: Key::Character("a".into()),
            modifiers: Modifiers::default(),
            pressed: true,
        },
        &mut NoClipboard,
    );
    assert_eq!(*saw.borrow(), 0, "the field typed it");

    // Escape is not a text-input key, so that one does reach the wire.
    tree.dispatch_key(
        &KeyInput {
            key: Key::Escape,
            modifiers: Modifiers::default(),
            pressed: true,
        },
        &mut NoClipboard,
    );
    assert_eq!(*saw.borrow(), 1);
}

/// With no focusable widget anywhere, a key wire still fires — from the root.
/// The focus invariant covers every other case, so this is the one shape where
/// there is nothing focused to start from, and a wire that went silently dead
/// there would be a trap with no diagnostic behind it.
#[test]
fn a_key_wire_fires_from_the_root_when_nothing_is_focusable() {
    let mut tree = WidgetTree::with_text_context(TextContext::hermetic([]));
    let root = tree.insert(Container::new(), taffy::Style::default(), None);
    let child = tree.insert(Container::new(), taffy::Style::default(), Some(root));
    tree.compute_layout(Size::new(100.0, 100.0));
    assert_eq!(tree.focus(), None, "a container is not focusable");

    let log: Log = Rc::new(RefCell::new(Vec::new()));
    for (id, name) in [(root, "root"), (child, "child")] {
        let log = log.clone();
        let name = name.to_owned();
        tree.on_event(id, EventKind::Key, move |_ctx, ev| {
            let EventData::Key(spelling) = ev else {
                panic!("key wires receive Key");
            };
            log.borrow_mut().push(format!("{name}:{spelling}"));
        });
    }

    tree.dispatch_key(
        &KeyInput {
            key: Key::Character("q".into()),
            modifiers: Modifiers {
                ctrl: true,
                ..Default::default()
            },
            pressed: true,
        },
        &mut NoClipboard,
    );
    // From the root, so only the root's own wire — there is no focused widget
    // to have started deeper.
    assert_eq!(*log.borrow(), ["root:Ctrl+Q"]);
}