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>>; 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>> = 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::( 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::(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::(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::(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 { 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>> = 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"]); }