From 61e4e65e99d9219df314627cacfead1a9af3cd94 Mon Sep 17 00:00:00 2001 From: Olivier FAURE Date: Mon, 13 Apr 2026 21:41:22 +0200 Subject: [PATCH 1/4] Have TestHarness methods use WidgetTag instead of WidgetId This is part of an effort to make WidgetTag the main primitive used in tests. --- masonry/src/doc/color_rectangle.rs | 8 +- masonry/src/doc/testing_widget.md | 8 +- masonry/src/tests/accessibility.rs | 5 +- masonry/src/tests/action.rs | 14 ++- masonry/src/tests/event.rs | 55 +++++------ masonry/src/tests/update.rs | 63 +++++-------- masonry/src/widgets/badge.rs | 4 +- masonry/src/widgets/button.rs | 38 ++++---- masonry/src/widgets/checkbox.rs | 13 +-- masonry/src/widgets/portal.rs | 17 ++-- masonry/src/widgets/radio_button.rs | 12 +-- masonry/src/widgets/scroll_bar.rs | 14 +-- masonry/src/widgets/slider.rs | 3 +- masonry/src/widgets/split.rs | 4 +- masonry/src/widgets/switch.rs | 15 +-- masonry/src/widgets/text_area.rs | 3 +- masonry/src/widgets/text_input.rs | 15 ++- masonry/src/widgets/virtual_scroll.rs | 18 ++-- masonry_core/src/app/render_root.rs | 32 +++++++ masonry_core/src/core/widget_tag.rs | 2 +- masonry_testing/src/harness.rs | 128 ++++++++++++++++++-------- 21 files changed, 270 insertions(+), 201 deletions(-) diff --git a/masonry/src/doc/color_rectangle.rs b/masonry/src/doc/color_rectangle.rs index 930ab9bff..7f339ec19 100644 --- a/masonry/src/doc/color_rectangle.rs +++ b/masonry/src/doc/color_rectangle.rs @@ -286,11 +286,11 @@ mod tests { .with_props(Dimensions::MIN); let mut harness = TestHarness::create(default_property_set(), widget); - let rect_id = harness.root_id(); + let rect_tag = harness.root_tag(); // Computes the rect's layout and sends an PointerEvent // placing the mouse at its center. - harness.mouse_move_to(rect_id); + harness.mouse_move_to(rect_tag); assert_render_snapshot!(harness, "rect_hovered_rectangle"); } @@ -322,9 +322,9 @@ mod tests { .with_props(Dimensions::MIN); let mut harness = TestHarness::create(default_property_set(), widget); - let rect_id = harness.root_id(); + let rect_tag = harness.root_tag(); - harness.mouse_click_on(rect_id, None); + harness.mouse_click_on(rect_tag, None); assert!(matches!( harness.pop_action::(), Some((ColorRectanglePress, _)) diff --git a/masonry/src/doc/testing_widget.md b/masonry/src/doc/testing_widget.md index 25dd2d33e..cf92c0457 100644 --- a/masonry/src/doc/testing_widget.md +++ b/masonry/src/doc/testing_widget.md @@ -122,11 +122,11 @@ Let's create another snapshot test to check that our widget correctly changes co .with_props(Dimensions::MIN); let mut harness = TestHarness::create(default_property_set(), widget); - let rect_id = harness.root_id(); + let rect_tag = harness.root_tag(); // Computes the rect's layout and sends an PointerEvent // placing the mouse at its center. - harness.mouse_move_to(rect_id); + harness.mouse_move_to(rect_tag); assert_render_snapshot!(harness, "rect_hovered_rectangle"); } ``` @@ -187,9 +187,9 @@ The `TestHarness` is also capable of reading actions emitted by our widget with .with_props(Dimensions::MIN); let mut harness = TestHarness::create(default_property_set(), widget); - let rect_id = harness.root_id(); + let rect_tag = harness.root_tag(); - harness.mouse_click_on(rect_id, None); + harness.mouse_click_on(rect_tag, None); assert!(matches!( harness.pop_action::(), Some((ColorRectanglePress, _)) diff --git a/masonry/src/tests/accessibility.rs b/masonry/src/tests/accessibility.rs index 5ecc12070..60a834780 100644 --- a/masonry/src/tests/accessibility.rs +++ b/masonry/src/tests/accessibility.rs @@ -59,12 +59,11 @@ fn access_node_children() { let _ = harness.render(); let parent_ref = harness.get_widget(parent_tag); - let parent_node_id = parent_ref.id(); let [id_1, id_2, id_3] = parent_ref.inner().children_ids()[..] else { unreachable!() }; - let parent_node = harness.access_node(parent_node_id).unwrap(); + let parent_node = harness.access_node(parent_tag).unwrap(); assert_eq!( Vec::::from_iter(parent_node.child_ids().map(node_local_id_to_u64)), vec![id_1.to_raw(), id_2.to_raw(), id_3.to_raw()] @@ -78,7 +77,7 @@ fn access_node_children() { let _ = harness.render(); // Stash child is not included - let parent_node = harness.access_node(parent_node_id).unwrap(); + let parent_node = harness.access_node(parent_tag).unwrap(); assert_eq!( Vec::::from_iter(parent_node.child_ids().map(node_local_id_to_u64)), vec![id_1.to_raw(), id_3.to_raw()] diff --git a/masonry/src/tests/action.rs b/masonry/src/tests/action.rs index 0c3cd1544..f438b2e21 100644 --- a/masonry/src/tests/action.rs +++ b/masonry/src/tests/action.rs @@ -6,7 +6,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use assert_matches::assert_matches; -use crate::core::{ChildrenIds, Widget}; +use crate::core::{ChildrenIds, Widget, WidgetTag}; use crate::kurbo::Point; use crate::layout::{AsUnit, LayoutSize}; use crate::properties::Dimensions; @@ -25,6 +25,7 @@ fn action_source_removed() { #[derive(Debug)] struct ArbitraryAction; + let action_source_tag = WidgetTag::named("action_source"); let action_source = ModularWidget::new(ok.clone()) .pointer_event_fn(|ok, ctx, _, _| { // Send an action but crucially don't mark the pointer event as handled, @@ -34,8 +35,8 @@ fn action_source_removed() { }) .prepare() .with_props(Dimensions::fixed(50.px(), 50.px())) + .with_tag(action_source_tag) .to_pod(); - let action_source_id = action_source.id(); let parent = ModularWidget::new(Some(action_source)) .pointer_event_fn(|child, ctx, _, _| { @@ -81,7 +82,7 @@ fn action_source_removed() { let mut harness = TestHarness::create(test_property_set(), parent); - harness.mouse_move_to(action_source_id); + harness.mouse_move_to(action_source_tag); // We don't expect the action to make it to the app driver, // because we deleted the child before it got there. @@ -95,7 +96,10 @@ fn action_propagation() { #[derive(Debug)] struct TranslatedAction; - let button = Button::with_text("Click me!").prepare(); + let button_tag = WidgetTag::named("button"); + let button = Button::with_text("Click me!") + .prepare() + .with_tag(button_tag); let button_id = button.id(); let parent1 = ModularWidget::new_parent(button) @@ -129,7 +133,7 @@ fn action_propagation() { let mut harness = TestHarness::create(test_property_set(), parent3); - harness.mouse_click_on(button_id, None); + harness.mouse_click_on(button_tag, None); // Only the translated action should reach the app driver assert_matches!( diff --git a/masonry/src/tests/event.rs b/masonry/src/tests/event.rs index 1afdae6e0..605b44113 100644 --- a/masonry/src/tests/event.rs +++ b/masonry/src/tests/event.rs @@ -38,10 +38,9 @@ fn pointer_event() { let button = NewWidget::new(Button::with_text("button").record()).with_tag(button_tag); let mut harness = TestHarness::create(test_property_set(), button); - let button_id = harness.get_widget(button_tag).id(); harness.flush_records_of(button_tag); - harness.mouse_move_to(button_id); + harness.mouse_move_to(button_tag); let records = harness.take_records_of(button_tag); assert_any(records, |r| { @@ -61,10 +60,9 @@ fn pointer_event_bubbling() { NewWidget::new(ModularWidget::new_parent(parent).record()).with_tag(grandparent_tag); let mut harness = TestHarness::create(test_property_set(), grandparent); - let button_id = harness.get_widget(button_tag).id(); harness.flush_records_of(button_tag); - harness.mouse_click_on(button_id, None); + harness.mouse_click_on(button_tag, None); fn is_pointer_down(record: Record) -> bool { matches!(record, Record::PointerEvent(PointerEvent::Down { .. })) @@ -86,7 +84,7 @@ fn pointer_capture_and_cancel() { let target_id = harness.get_widget(target_tag).id(); - harness.mouse_move_to(target_id); + harness.mouse_move_to(target_tag); harness.mouse_button_press(None); assert_eq!(harness.pointer_capture_target_id(), Some(target_id)); @@ -109,7 +107,7 @@ fn synthetic_cancel() { let target_id = harness.get_widget(target_tag).id(); - harness.mouse_move_to(target_id); + harness.mouse_move_to(target_tag); harness.mouse_button_press(None); assert_eq!(harness.pointer_capture_target_id(), Some(target_id)); @@ -143,15 +141,14 @@ fn pointer_capture_suppresses_neighbors() { harness.flush_records_of(other_tag); let target_id = harness.get_widget(target_tag).id(); - let other_id = harness.get_widget(other_tag).id(); - harness.mouse_move_to(target_id); + harness.mouse_move_to(target_tag); harness.mouse_button_press(None); assert_eq!(harness.pointer_capture_target_id(), Some(target_id)); // As long as 'target' is captured, 'other' doesn't get pointer events, even when the cursor is on it. - harness.mouse_move_to(other_id); + harness.mouse_move_to(other_tag); assert_matches!(harness.take_records_of(other_tag)[..], []); // 'other' is not considered hovered either. @@ -191,8 +188,7 @@ fn try_capture_pointer_on_text_event() { .prepare(); let mut harness = TestHarness::create(test_property_set(), widget); - let id = harness.root_id(); - harness.focus_on(Some(id)); + harness.focus_on(harness.root_tag()); assert_debug_panics!( harness.keyboard_type_chars("a"), @@ -211,7 +207,7 @@ fn pointer_cancel_on_window_blur() { let target_id = harness.get_widget(target_tag).id(); - harness.mouse_move_to(target_id); + harness.mouse_move_to(target_tag); harness.mouse_button_press(None); assert_eq!(harness.pointer_capture_target_id(), Some(target_id)); harness.flush_records_of(target_tag); @@ -241,13 +237,11 @@ fn click_anchors_focus() { let mut harness = TestHarness::create(test_property_set(), parent); - let child_3_id = harness.get_widget(child_3).id(); let child_4_id = harness.get_widget(child_4).id(); - let other_id = harness.get_widget(other).id(); // Clicking a disabled button doesn't focus it. harness.set_disabled(child_3, true); - harness.mouse_click_on(child_3_id, None); + harness.mouse_click_on(child_3, None); assert_eq!(harness.focused_widget_id(), None); // But the next tab event focuses its neighbor. @@ -259,7 +253,7 @@ fn click_anchors_focus() { // is resolved. // Clicking another non-focusable widget clears focus. - harness.mouse_move_to_unchecked(other_id); + harness.mouse_move_to_unchecked(other); harness.mouse_button_press(None); harness.mouse_button_release(None); assert_eq!(harness.focused_widget_id(), None); @@ -433,7 +427,7 @@ fn multi_pointers_capture() { // Move mouse to button 1, mouse press // Check mouse is captured, button 1 is active - harness.mouse_move_to(button_1_id); + harness.mouse_move_to(button_1_tag); harness.mouse_button_press(None); assert_captured_by(&harness, PointerId::PRIMARY, button_1_id); @@ -484,7 +478,6 @@ fn text_event() { let target = NewWidget::new(TextArea::new_editable("").record()).with_tag(target_tag); let mut harness = TestHarness::create(test_property_set(), target); - let target_id = harness.get_widget(target_tag).id(); harness.flush_records_of(target_tag); // The widget isn't focused, it doesn't get text events. @@ -492,7 +485,7 @@ fn text_event() { assert_matches!(harness.take_records_of(target_tag)[..], []); // We focus on the widget, now it gets text events. - harness.focus_on(Some(target_id)); + harness.focus_on(target_tag); harness.keyboard_type_chars("A"); let records = harness.take_records_of(target_tag); assert_any(records, |r| matches!(r, Record::TextEvent(_))); @@ -511,9 +504,8 @@ fn text_event_bubbling() { NewWidget::new(ModularWidget::new_parent(parent).record()).with_tag(grandparent_tag); let mut harness = TestHarness::create(test_property_set(), grandparent); - let target_id = harness.get_widget(target_tag).id(); - harness.focus_on(Some(target_id)); + harness.focus_on(target_tag); harness.process_text_event(TextEvent::key_down(Key::Character("A".into()))); fn is_keyboard_event(record: Record) -> bool { @@ -535,16 +527,14 @@ fn text_event_fallback() { let parent = Flex::row().with_fixed(target).with_fixed(other).prepare(); let mut harness = TestHarness::create(test_property_set(), parent); - let target_id = harness.get_widget(target_tag).id(); - let other_id = harness.get_widget(other_tag).id(); harness.flush_records_of(target_tag); - harness.set_focus_fallback(Some(target_id)); + harness.set_focus_fallback(target_tag); - harness.focus_on(Some(other_id)); + harness.focus_on(other_tag); assert_matches!(harness.take_records_of(target_tag)[..], []); // If a widget is set as focus fallback, that widget gets text events when no widget is focused. - harness.focus_on(None); + harness.clear_focus(); harness.keyboard_type_chars("A"); let records = harness.take_records_of(target_tag); assert_any(records, |r| matches!(r, Record::TextEvent(_))); @@ -575,30 +565,28 @@ fn tab_focus() { let mut harness = TestHarness::create(test_property_set(), parent); let child_1_id = harness.get_widget(child_1).id(); - let child_2_id = harness.get_widget(child_2).id(); let child_3_id = harness.get_widget(child_3).id(); - let child_4_id = harness.get_widget(child_4).id(); let child_5_id = harness.get_widget(child_5).id(); assert_eq!(harness.focused_widget_id(), None); // Tab moves focus to the next focusable widget in the tree. - harness.focus_on(Some(child_2_id)); + harness.focus_on(child_2); harness.press_tab_key(false); assert_eq!(harness.focused_widget_id(), Some(child_3_id)); // Shift+Tab moves focus to the previous focusable widget in the tree. - harness.focus_on(Some(child_4_id)); + harness.focus_on(child_4); harness.press_tab_key(true); assert_eq!(harness.focused_widget_id(), Some(child_3_id)); // When nothing is focused, Tab focuses the first focusable widget in the tree. - harness.focus_on(None); + harness.clear_focus(); harness.press_tab_key(false); assert_eq!(harness.focused_widget_id(), Some(child_1_id)); // When nothing is focused, Shift+Tab focuses the last focusable widget in the tree. - harness.focus_on(None); + harness.clear_focus(); harness.press_tab_key(true); assert_eq!(harness.focused_widget_id(), Some(child_5_id)); } @@ -700,9 +688,8 @@ fn downcast_untyped_action() { let widget = NewWidget::new(arbitrary_submitter).with_tag(target_tag); let mut harness = TestHarness::create(test_property_set(), widget); - let target_id = harness.get_widget(target_tag).id(); - harness.mouse_move_to(target_id); + harness.mouse_move_to(target_tag); assert_matches!( harness.pop_action::(), diff --git a/masonry/src/tests/update.rs b/masonry/src/tests/update.rs index 4b5d8c460..64a8e01bb 100644 --- a/masonry/src/tests/update.rs +++ b/masonry/src/tests/update.rs @@ -104,8 +104,7 @@ fn disabled_widget_gets_no_event() { let parent = NewWidget::new(ModularWidget::new_parent(child)).with_tag(parent_tag); let mut harness = TestHarness::create(test_property_set(), parent); - let button_id = harness.get_widget(button_tag).id(); - harness.focus_on(Some(button_id)); + harness.focus_on(button_tag); harness.flush_records_of(button_tag); harness.set_disabled(button_tag, true); @@ -118,7 +117,7 @@ fn disabled_widget_gets_no_event() { ] ); - harness.mouse_click_on(button_id, None); + harness.mouse_click_on(button_tag, None); assert_matches!(harness.take_records_of(button_tag)[..], []); assert_matches!(harness.focused_widget_id(), None); @@ -179,8 +178,7 @@ fn stashed_widget_loses_focus() { let parent = NewWidget::new(ModularWidget::new_parent(child)).with_tag(parent_tag); let mut harness = TestHarness::create(test_property_set(), parent); - let button_id = harness.get_widget(button_tag).id(); - harness.focus_on(Some(button_id)); + harness.focus_on(button_tag); harness.flush_records_of(button_tag); harness.edit_widget(parent_tag, |mut widget| { @@ -334,7 +332,8 @@ fn focus_order() { next_focusable_widgets.push(*focusable_widgets.first().unwrap()); for (&id, &next_id) in std::iter::zip(&focusable_widgets, &next_focusable_widgets) { - harness.focus_on(Some(id)); + let tag = harness.make_dyn_tag_for_widget(id); + harness.focus_on(tag); harness.press_tab_key(false); let focused = harness.focused_widget_id(); assert_eq!( @@ -356,7 +355,7 @@ fn focus_order() { assert_eq!(focused, Some(id)); } - harness.focus_on(None); + harness.clear_focus(); harness.press_tab_key(false); let focused = harness.focused_widget_id(); assert_eq!( @@ -365,7 +364,7 @@ fn focus_order() { ); assert_eq!(focused, focusable_widgets.first().copied()); - harness.focus_on(None); + harness.clear_focus(); harness.press_tab_key(true); let focused = harness.focused_widget_id(); assert_eq!( @@ -392,14 +391,13 @@ fn disable_focusable() { let mut harness = TestHarness::create(test_property_set(), parent); let button1_id = harness.get_widget(button1_tag).id(); - let button2_id = harness.get_widget(button2_tag).id(); let button3_id = harness.get_widget(button3_tag).id(); - harness.focus_on(Some(button2_id)); + harness.focus_on(button2_tag); harness.set_disabled(button2_tag, true); // We skip button2 and jump from button1 to button3. - harness.focus_on(Some(button1_id)); + harness.focus_on(button1_tag); harness.press_tab_key(false); assert_eq!(harness.focused_widget_id(), Some(button3_id)); @@ -425,17 +423,16 @@ fn stash_focusable() { let mut harness = TestHarness::create(test_property_set(), parent); let button1_id = harness.get_widget(button1_tag).id(); - let button2_id = harness.get_widget(button2_tag).id(); let button3_id = harness.get_widget(button3_tag).id(); - harness.focus_on(Some(button2_id)); + harness.focus_on(button2_tag); harness.edit_root_widget(|mut parent| { parent.ctx.set_stashed(&mut parent.widget.state[1], true); }); // We skip button2 and jump from button1 to button3. - harness.focus_on(Some(button1_id)); + harness.focus_on(button1_tag); harness.press_tab_key(false); assert_eq!(harness.focused_widget_id(), Some(button3_id)); @@ -461,17 +458,16 @@ fn remove_focusable() { let mut harness = TestHarness::create(test_property_set(), parent); let button1_id = harness.get_widget(button1_tag).id(); - let button2_id = harness.get_widget(button2_tag).id(); let button3_id = harness.get_widget(button3_tag).id(); - harness.focus_on(Some(button2_id)); + harness.focus_on(button2_tag); harness.edit_root_widget(|mut parent| { let child = parent.widget.state.remove(1); parent.ctx.remove_child(child); }); // We go from button1 to button3. - harness.focus_on(Some(button1_id)); + harness.focus_on(button1_tag); harness.press_tab_key(false); assert_eq!(harness.focused_widget_id(), Some(button3_id)); @@ -488,9 +484,8 @@ fn ime_commit() { let textbox = NewWidget::new(TextArea::new_editable("")).with_tag(textbox_tag); let mut harness = TestHarness::create(test_property_set(), textbox); - let textbox_id = harness.get_widget(textbox_tag).id(); - harness.focus_on(Some(textbox_id)); + harness.focus_on(textbox_tag); harness.process_text_event(TextEvent::Ime(Ime::Commit("New Text".to_string()))); assert_eq!(harness.get_widget(textbox_tag).text(), "New Text"); @@ -509,9 +504,8 @@ fn ime_removed() { let parent = NewWidget::new(SizedBox::new(textbox)); let mut harness = TestHarness::create(test_property_set(), parent); - let textbox_id = harness.get_widget(textbox_tag).id(); - harness.focus_on(Some(textbox_id)); + harness.focus_on(textbox_tag); harness.edit_root_widget(|mut sized_box| { SizedBox::remove_child(&mut sized_box); @@ -528,9 +522,8 @@ fn ime_start_stop() { let parent = NewWidget::new(ModularWidget::new_parent(textbox)); let mut harness = TestHarness::create(test_property_set(), parent); - let textbox_id = harness.get_widget(textbox_tag).id(); - harness.focus_on(Some(textbox_id)); + harness.focus_on(textbox_tag); assert!(harness.has_ime_session()); @@ -568,11 +561,10 @@ fn cursor_icon() { let parent = NewWidget::new(Flex::row().with_fixed(label).with_fixed(icon_widget)); let mut harness = TestHarness::create(test_property_set(), parent); - let icon_id = harness.get_widget(icon_tag).id(); assert_eq!(harness.cursor_icon(), CursorIcon::Default); - harness.mouse_move_to(icon_id); + harness.mouse_move_to(icon_tag); assert_eq!(harness.cursor_icon(), CursorIcon::Crosshair); } @@ -585,15 +577,13 @@ fn pointer_capture_affects_pointer_icon() { let parent = NewWidget::new(Flex::row().with_fixed(label).with_fixed(icon_widget)); let mut harness = TestHarness::create(test_property_set(), parent); - let icon_id = harness.get_widget(icon_tag).id(); - let label_id = harness.get_widget(label_tag).id(); - harness.mouse_move_to(icon_id); + harness.mouse_move_to(icon_tag); harness.mouse_button_press(None); assert_eq!(harness.cursor_icon(), CursorIcon::Crosshair); // We keep the Crosshair icon as long as the pointer stays captured. - harness.mouse_move_to(label_id); + harness.mouse_move_to(label_tag); assert_eq!(harness.cursor_icon(), CursorIcon::Crosshair); harness.mouse_button_release(None); @@ -607,10 +597,9 @@ fn lose_hovered_on_pointer_leave_or_cancel() { let button = NewWidget::new(Button::with_text("button").record()).with_tag(button_tag); let mut harness = TestHarness::create(test_property_set(), button); - let button_id = harness.get_widget(button_tag).id(); // Hover button - harness.mouse_move_to(button_id); + harness.mouse_move_to(button_tag); assert!(harness.get_widget(button_tag).ctx().is_hovered()); // POINTER LEAVE @@ -625,7 +614,7 @@ fn lose_hovered_on_pointer_leave_or_cancel() { }); // Hover button again - harness.mouse_move_to(button_id); + harness.mouse_move_to(button_tag); assert!(harness.get_widget(button_tag).ctx().is_hovered()); // POINTER CANCEL @@ -656,9 +645,8 @@ fn change_hovered_when_widget_changes() { .with_tag(parent_tag); let mut harness = TestHarness::create(test_property_set(), parent); - let child_id = harness.get_widget(child_tag).id(); - harness.mouse_move_to(child_id); + harness.mouse_move_to(child_tag); assert!(harness.get_widget(child_tag).ctx().is_hovered()); assert!(!harness.get_widget(parent_tag).ctx().is_hovered()); @@ -718,11 +706,10 @@ fn status_flag_update_order() { let parent3 = NewWidget::new(make_reporter_parent(parent2, sender3, 3)); let mut harness = TestHarness::create(test_property_set(), parent3); - let parent1_id = harness.get_widget(parent1_tag).id(); // Flush initial events let _ = receiver.try_iter().count(); - harness.mouse_move_to(parent1_id); + harness.mouse_move_to(parent1_tag); let events: Vec<_> = receiver.try_iter().collect(); assert_eq!( events, @@ -778,7 +765,7 @@ fn status_flag_update_order() { assert!(!harness.get_widget(parent1_tag).ctx().is_hovered()); assert!(!harness.get_widget(parent1_tag).ctx().has_hovered()); - harness.focus_on(Some(parent1_id)); + harness.focus_on(parent1_tag); let events: Vec<_> = receiver.try_iter().collect(); assert_eq!( events, @@ -792,7 +779,7 @@ fn status_flag_update_order() { assert!(harness.get_widget(parent1_tag).ctx().is_focus_target()); assert!(harness.get_widget(parent1_tag).ctx().has_focus_target()); - harness.focus_on(None); + harness.clear_focus(); let events: Vec<_> = receiver.try_iter().collect(); assert_eq!( events, diff --git a/masonry/src/widgets/badge.rs b/masonry/src/widgets/badge.rs index 1d15b8b59..559f95cc0 100644 --- a/masonry/src/widgets/badge.rs +++ b/masonry/src/widgets/badge.rs @@ -233,9 +233,9 @@ mod tests { fn badge_is_non_interactive() { let widget = Badge::new(Label::new("New").prepare()).prepare(); let mut harness = TestHarness::create_with_size(test_property_set(), widget, (80, 40)); - let badge_id = harness.root_id(); + let badge_tag = harness.root_tag(); - harness.mouse_click_on(badge_id, None); + harness.mouse_click_on(badge_tag, None); assert!(harness.pop_action_erased().is_none()); assert!(harness.focused_widget().is_none()); } diff --git a/masonry/src/widgets/button.rs b/masonry/src/widgets/button.rs index b1ed5da9f..9659318eb 100644 --- a/masonry/src/widgets/button.rs +++ b/masonry/src/widgets/button.rs @@ -264,16 +264,16 @@ impl Widget for Button { #[cfg(test)] mod tests { use assert_matches::assert_matches; - use masonry_core::core::WidgetTag; - use masonry_testing::{TestHarnessParams, assert_failing_render_snapshot}; use super::*; - use crate::core::{CollectionWidget, PointerButton, PropertySet, StyleProperty}; + use crate::core::{CollectionWidget, PointerButton, PropertySet, StyleProperty, WidgetTag}; use crate::layout::AsUnit; use crate::properties::{ BorderColor, BorderWidth, BoxShadow, ContentColor, CornerRadius, Gap, Padding, }; - use crate::testing::{TestHarness, assert_render_snapshot}; + use crate::testing::{ + TestHarness, TestHarnessParams, assert_failing_render_snapshot, assert_render_snapshot, + }; use crate::theme::{ACCENT_COLOR, test_property_set}; use crate::widgets::{Flex, Grid, GridParams, Label, SizedBox}; @@ -284,13 +284,14 @@ mod tests { let params = TestHarnessParams::size_and_padding((100, 40), TestHarnessParams::ROOT_PADDING); let mut harness = TestHarness::create_with(test_property_set(), widget, params); + let button_tag = harness.root_tag(); let button_id = harness.root_id(); assert_render_snapshot!(harness, "button_hello"); assert!(harness.pop_action_erased().is_none()); - harness.mouse_click_on(button_id, Some(PointerButton::Primary)); + harness.mouse_click_on(button_tag, Some(PointerButton::Primary)); assert_eq!( harness.pop_action::(), Some(( @@ -302,7 +303,7 @@ mod tests { ); // Check that Tab focuses on the widget - harness.focus_on(None); + harness.clear_focus(); harness.press_tab_key(false); assert_eq!(harness.focused_widget().map(|w| w.id()), Some(button_id)); @@ -318,10 +319,11 @@ mod tests { fn mouse_down_requests_focus() { let widget = NewWidget::new(Button::with_text("Hello")); let mut harness = TestHarness::create(test_property_set(), widget); + let button_tag = harness.root_tag(); let button_id = harness.root_id(); - harness.focus_on(None); - harness.mouse_move_to(button_id); + harness.clear_focus(); + harness.mouse_move_to(button_tag); harness.mouse_button_press(None); assert_eq!(harness.focused_widget_id(), Some(button_id)); @@ -462,20 +464,23 @@ mod tests { /// /// We validate that each of these actually are correctly supported. fn validate_noninteractive_child(child: NewWidget) { - let child_id = child.id(); + let child_tag = WidgetTag::unique(); + let child = child.with_tag(child_tag); + let mut button = Button::new(child).prepare(); button.properties.insert(Padding::all(10.)); - let button_id = button.id(); let mut harness = TestHarness::create(test_property_set(), button); + let button_tag = harness.root_tag(); + let button_id = harness.root_id(); - harness.mouse_move_to_unchecked(child_id); - let button = harness.get_widget_with_id(button_id); + harness.mouse_move_to_unchecked(child_tag); + let button = harness.get_widget(button_tag); assert!( button.ctx().is_hovered(), "The child shouldn't prevent hover." ); harness.mouse_button_press(None); - let button = harness.get_widget_with_id(button_id); + let button = harness.get_widget(button_tag); assert!( button.ctx().is_pointer_capture_target(), "A non-interactive child shouldn't prevent pointer capture." @@ -518,18 +523,17 @@ mod tests { // for the button center. // See https://github.com/linebender/xilem/issues/1756 fn textless_button() { - let tag = WidgetTag::unique(); - let button = Button::with_text("").prepare().with_tag(tag); + let button_tag = WidgetTag::unique(); + let button = Button::with_text("").prepare().with_tag(button_tag); let parent = Flex::row().with(button, 0.).prepare(); let params = TestHarnessParams::size_and_padding((100, 40), TestHarnessParams::ROOT_PADDING); let mut harness = TestHarness::create_with(test_property_set(), parent, params); - let button_id = harness.get_widget(tag).id(); assert_render_snapshot!(harness, "button_no_text"); - harness.mouse_click_on(button_id, Some(PointerButton::Primary)); + harness.mouse_click_on(button_tag, Some(PointerButton::Primary)); assert_matches!( harness.pop_action::(), Some((ButtonPress { .. }, ..)) diff --git a/masonry/src/widgets/checkbox.rs b/masonry/src/widgets/checkbox.rs index 6254e7440..42ce2150c 100644 --- a/masonry/src/widgets/checkbox.rs +++ b/masonry/src/widgets/checkbox.rs @@ -376,7 +376,7 @@ impl Widget for Checkbox { #[cfg(test)] mod tests { use super::*; - use crate::core::{PropertySet, StyleProperty}; + use crate::core::{PropertySet, StyleProperty, WidgetTag}; use crate::properties::ContentColor; use crate::testing::{TestHarness, assert_render_snapshot}; use crate::theme::{ACCENT_COLOR, test_property_set}; @@ -387,13 +387,14 @@ mod tests { let widget = NewWidget::new(Checkbox::new(false, "Hello")); let mut harness = TestHarness::create_with_size(test_property_set(), widget, (100, 40)); + let checkbox_tag = harness.root_tag(); let checkbox_id = harness.root_id(); assert_render_snapshot!(harness, "checkbox_hello_unchecked"); assert!(harness.pop_action_erased().is_none()); - harness.mouse_click_on(checkbox_id, None); + harness.mouse_click_on(checkbox_tag, None); assert_eq!( harness.pop_action::(), Some((CheckboxToggled(true), checkbox_id)) @@ -405,7 +406,7 @@ mod tests { assert_render_snapshot!(harness, "checkbox_hello_checked"); - harness.focus_on(None); + harness.clear_focus(); harness.press_tab_key(false); assert_eq!(harness.focused_widget().map(|w| w.id()), Some(checkbox_id)); @@ -421,8 +422,8 @@ mod tests { fn checkbox_focus_indicator() { use crate::properties::types::MainAxisAlignment; - let checkbox = NewWidget::new(Checkbox::new(true, "Focus test")); - let checkbox_id = checkbox.id(); + let checkbox_tag = WidgetTag::named("checkbox"); + let checkbox = NewWidget::new(Checkbox::new(true, "Focus test")).with_tag(checkbox_tag); let root = NewWidget::new( Flex::row() @@ -431,7 +432,7 @@ mod tests { ); let mut harness = TestHarness::create_with_size(test_property_set(), root, (120, 40)); - harness.focus_on(Some(checkbox_id)); + harness.focus_on(checkbox_tag); assert_render_snapshot!(harness, "checkbox_focus_focused"); } #[test] diff --git a/masonry/src/widgets/portal.rs b/masonry/src/widgets/portal.rs index 9a894d90a..ede21acec 100644 --- a/masonry/src/widgets/portal.rs +++ b/masonry/src/widgets/portal.rs @@ -933,10 +933,10 @@ mod tests { assert_render_snapshot!(harness, "portal_button_list_scrolled"); - harness.scroll_into_view(harness.get_widget(button_3).id()); + harness.scroll_into_view(button_3); assert_render_snapshot!(harness, "portal_button_list_scroll_to_item_3"); - harness.scroll_into_view(harness.get_widget(button_13).id()); + harness.scroll_into_view(button_13); assert_render_snapshot!(harness, "portal_button_list_scroll_to_item_13"); } @@ -954,9 +954,8 @@ mod tests { .prepare(); let mut harness = TestHarness::create_with_size(test_property_set(), widget, (200, 200)); - let button_id = harness.get_widget(button_tag).id(); - harness.scroll_into_view(button_id); + harness.scroll_into_view(button_tag); assert_render_snapshot!(harness, "portal_scrolled_button_into_view"); } @@ -969,8 +968,7 @@ mod tests { let mut harness = TestHarness::create_with_size(test_property_set(), portal, (100, 100)); let _ = harness.render(); - let portal_id = harness.get_widget(portal_tag).id(); - let node = harness.access_node(portal_id).unwrap(); + let node = harness.access_node(portal_tag).unwrap(); assert_eq!(node.data().role(), Role::ScrollView); assert!(node.data().supports_action(accesskit::Action::ScrollDown)); @@ -988,7 +986,7 @@ mod tests { }); harness.render(); - let node = harness.access_node(portal_id).unwrap(); + let node = harness.access_node(portal_tag).unwrap(); assert!(!node.data().supports_action(accesskit::Action::ScrollDown)); assert!(node.data().supports_action(accesskit::Action::ScrollUp)); } @@ -1002,13 +1000,12 @@ mod tests { let mut harness = TestHarness::create_with_size(test_property_set(), portal, (100, 100)); let _ = harness.render(); - let portal_id = harness.get_widget(portal_tag).id(); - harness.focus_on(Some(portal_id)); + harness.focus_on(portal_tag); harness.process_text_event(TextEvent::key_down(Key::Named(NamedKey::PageDown))); let _ = harness.render(); - let node = harness.access_node(portal_id).unwrap(); + let node = harness.access_node(portal_tag).unwrap(); assert!(node.data().scroll_y().unwrap_or(0.0) > 0.0); } diff --git a/masonry/src/widgets/radio_button.rs b/masonry/src/widgets/radio_button.rs index 1587b9516..81230c750 100644 --- a/masonry/src/widgets/radio_button.rs +++ b/masonry/src/widgets/radio_button.rs @@ -431,11 +431,11 @@ mod tests { assert!(harness.pop_action_erased().is_none()); - harness.mouse_move_to(radio_id); + harness.mouse_move_to(radio_tag); assert_render_snapshot!(harness, "radio_button_hello_hovered"); - harness.mouse_click_on(radio_id, None); + harness.mouse_click_on(radio_tag, None); assert_eq!( harness.pop_action::(), Some((RadioButtonSelected, radio_id)) @@ -452,7 +452,7 @@ mod tests { // The radio button should be unchecked again, but still hovered assert_render_snapshot!(harness, "radio_button_hello_hovered"); - harness.focus_on(None); + harness.clear_focus(); harness.press_tab_key(false); assert_eq!(harness.focused_widget().map(|w| w.id()), Some(radio_id)); } @@ -461,8 +461,8 @@ mod tests { fn radio_button_focus_indicator() { use crate::properties::types::MainAxisAlignment; - let radio = NewWidget::new(RadioButton::new(true, "Focus test")); - let radio_id = radio.id(); + let radio_tag = WidgetTag::unique(); + let radio = NewWidget::new(RadioButton::new(true, "Focus test")).with_tag(radio_tag); let group = NewWidget::new(RadioGroup::new(radio)); let root = NewWidget::new( @@ -472,7 +472,7 @@ mod tests { ); let mut harness = TestHarness::create_with_size(default_property_set(), root, (120, 40)); - harness.focus_on(Some(radio_id)); + harness.focus_on(radio_tag); assert_render_snapshot!(harness, "radio_button_focus_focused"); } diff --git a/masonry/src/widgets/scroll_bar.rs b/masonry/src/widgets/scroll_bar.rs index f6b3f86a3..cd588c3d3 100644 --- a/masonry/src/widgets/scroll_bar.rs +++ b/masonry/src/widgets/scroll_bar.rs @@ -477,13 +477,13 @@ mod tests { .with_props(Dimensions::FIT); let mut harness = TestHarness::create_with_size(test_property_set(), widget, (50, 200)); - let scrollbar_id = harness.root_id(); + let scrollbar_tag = harness.root_tag(); assert_render_snapshot!(harness, "scrollbar_default"); assert!(harness.pop_action_erased().is_none()); - harness.mouse_click_on(scrollbar_id, None); + harness.mouse_click_on(scrollbar_tag, None); // TODO - Scroll action? assert!(harness.pop_action_erased().is_none()); @@ -504,13 +504,13 @@ mod tests { .with_props(Dimensions::FIT); let mut harness = TestHarness::create_with_size(test_property_set(), widget, (200, 50)); - let scrollbar_id = harness.root_id(); + let scrollbar_tag = harness.root_tag(); assert_render_snapshot!(harness, "scrollbar_horizontal"); assert!(harness.pop_action_erased().is_none()); - harness.mouse_click_on(scrollbar_id, None); + harness.mouse_click_on(scrollbar_tag, None); // TODO - Scroll action? assert!(harness.pop_action_erased().is_none()); @@ -524,13 +524,13 @@ mod tests { let mut harness = TestHarness::create_with_size(test_property_set(), widget, (50, 200)); let _ = harness.render(); - let scrollbar_id = harness.root_id(); - harness.focus_on(Some(scrollbar_id)); + let scrollbar_tag = harness.root_tag(); + harness.focus_on(scrollbar_tag); harness.process_text_event(TextEvent::key_down(Key::Named(NamedKey::ArrowDown))); let _ = harness.render(); - let node = harness.access_node(scrollbar_id).unwrap(); + let node = harness.access_node(scrollbar_tag).unwrap(); assert!(node.data().scroll_y().unwrap_or(0.0) > 0.0); } diff --git a/masonry/src/widgets/slider.rs b/masonry/src/widgets/slider.rs index 109b2fe17..5d61aa677 100644 --- a/masonry/src/widgets/slider.rs +++ b/masonry/src/widgets/slider.rs @@ -560,8 +560,9 @@ mod tests { let widget = Slider::new(0.0, 100.0, 50.0).with_step(10.0).prepare(); let mut harness = TestHarness::create_with_size(test_property_set(), widget, (200, 32)); let slider_id = harness.root_id(); + let slider_tag = harness.root_tag(); - harness.focus_on(Some(slider_id)); + harness.focus_on(slider_tag); assert_render_snapshot!(harness, "slider_keyboard_focused"); harness.process_text_event(TextEvent::key_down(Key::Named(NamedKey::ArrowRight))); diff --git a/masonry/src/widgets/split.rs b/masonry/src/widgets/split.rs index 5c3443684..e1d095a63 100644 --- a/masonry/src/widgets/split.rs +++ b/masonry/src/widgets/split.rs @@ -936,8 +936,8 @@ mod tests { let mut harness = TestHarness::create_with_size(test_property_set(), widget, (150, 100)); - let root_id = harness.root_id(); - harness.focus_on(Some(root_id)); + let root_tag = harness.root_tag(); + harness.focus_on(root_tag); let child1_initial_width = { let root = harness.root_widget(); diff --git a/masonry/src/widgets/switch.rs b/masonry/src/widgets/switch.rs index 5fd3524d4..5ebd4d362 100644 --- a/masonry/src/widgets/switch.rs +++ b/masonry/src/widgets/switch.rs @@ -348,6 +348,7 @@ mod tests { let widget = Switch::new(false).prepare(); let mut harness = TestHarness::create_with_size(test_property_set(), widget, (60, 40)); let switch_id = harness.root_id(); + let switch_tag = harness.root_tag(); // Initially not focused, and no actions assert!(harness.focused_widget().is_none()); @@ -355,7 +356,7 @@ mod tests { assert!(harness.pop_action_erased().is_none()); // Click on switch (off -> wants to be on) - harness.mouse_click_on(switch_id, None); + harness.mouse_click_on(switch_tag, None); assert_eq!(harness.focused_widget().map(|w| w.id()), Some(switch_id)); assert_eq!( harness.pop_action::(), @@ -366,7 +367,7 @@ mod tests { harness.edit_root_widget(|mut switch| Switch::set_on(&mut switch, true)); // Click again (on -> wants to be off) - harness.mouse_click_on(switch_id, None); + harness.mouse_click_on(switch_tag, None); assert_eq!( harness.pop_action::(), Some((SwitchToggled(false), switch_id)) @@ -380,7 +381,7 @@ mod tests { let switch_id = harness.root_id(); // Focus via tab - harness.focus_on(None); + harness.clear_focus(); harness.press_tab_key(false); assert_eq!(harness.focused_widget().map(|w| w.id()), Some(switch_id)); @@ -447,17 +448,18 @@ mod tests { let mut harness = TestHarness::create_with_size(test_property_set(), widget, (60, 40)); let switch_id = harness.root_id(); + let switch_tag = harness.root_tag(); assert_render_snapshot!(harness, "switch_off"); assert!(harness.pop_action_erased().is_none()); // Hover without clicking to show hovered state (no focus) - harness.mouse_move_to(switch_id); + harness.mouse_move_to(switch_tag); assert_render_snapshot!(harness, "switch_off_hovered"); // Now click to switch - harness.mouse_click_on(switch_id, None); + harness.mouse_click_on(switch_tag, None); assert_eq!( harness.pop_action::(), Some((SwitchToggled(true), switch_id)) @@ -474,10 +476,11 @@ mod tests { let widget = Switch::new(false).prepare(); let mut harness = TestHarness::create_with_size(test_property_set(), widget, (60, 40)); + let switch_tag = harness.root_tag(); let switch_id = harness.root_id(); // Focus directly (not via click) to get focused-but-not-hovered state - harness.focus_on(Some(switch_id)); + harness.focus_on(switch_tag); assert_eq!(harness.focused_widget().map(|w| w.id()), Some(switch_id)); assert_render_snapshot!(harness, "switch_focused"); diff --git a/masonry/src/widgets/text_area.rs b/masonry/src/widgets/text_area.rs index 5c870daa9..cc892ccf3 100644 --- a/masonry/src/widgets/text_area.rs +++ b/masonry/src/widgets/text_area.rs @@ -1263,9 +1263,10 @@ mod tests { ); let mut harness = TestHarness::create(test_property_set(), area); + let text_tag = harness.root_tag(); let text_id = harness.root_id(); - harness.focus_on(Some(text_id)); + harness.focus_on(text_tag); harness.process_text_event(TextEvent::Keyboard(KeyboardEvent { key: scenario.key, modifiers: scenario.modifiers, diff --git a/masonry/src/widgets/text_input.rs b/masonry/src/widgets/text_input.rs index 481f03991..edcde442f 100644 --- a/masonry/src/widgets/text_input.rs +++ b/masonry/src/widgets/text_input.rs @@ -373,7 +373,7 @@ mod tests { use masonry_testing::TestHarnessParams; use super::*; - use crate::core::{StyleProperty, TextEvent}; + use crate::core::{StyleProperty, TextEvent, WidgetTag}; use crate::dpi::PhysicalSize; use crate::testing::{TestHarness, assert_render_snapshot}; use crate::theme::test_property_set; @@ -388,23 +388,22 @@ mod tests { #[test] fn text_input_outline() { + let text_area_tag = WidgetTag::named("text_area"); let text_input = NewWidget::new(TextInput::from_text_area( TextArea::new_editable("TextInput contents") .with_style(StyleProperty::FontSize(14.0)) - .prepare(), + .prepare() + .with_tag(text_area_tag), )); let mut harness = TestHarness::create_with(test_property_set(), text_input, HARNESS_PARAMS); assert_render_snapshot!(harness, "text_input_outline"); - let mut text_area_id = None; harness.edit_root_widget(|mut text_input| { - let mut text_input = TextInput::text_mut(&mut text_input); - text_area_id = Some(text_input.ctx.widget_id()); - - TextArea::select_text(&mut text_input, "contents"); + let mut text_area = TextInput::text_mut(&mut text_input); + TextArea::select_text(&mut text_area, "contents"); }); - harness.focus_on(text_area_id); + harness.focus_on(text_area_tag); assert_render_snapshot!(harness, "text_input_selection"); diff --git a/masonry/src/widgets/virtual_scroll.rs b/masonry/src/widgets/virtual_scroll.rs index 9d15376a7..11800d97b 100644 --- a/masonry/src/widgets/virtual_scroll.rs +++ b/masonry/src/widgets/virtual_scroll.rs @@ -1125,6 +1125,7 @@ mod tests { let widget = VirtualScroll::new(0).prepare(); let mut harness = TestHarness::create_with_size(test_property_set(), widget, (100, 200)); + let virtual_scroll_tag = harness.root_tag(); let virtual_scroll_id = harness.root_id(); fn driver(action: VirtualScrollAction, mut scroll: WidgetMut<'_, VirtualScroll>) { VirtualScroll::will_handle_action(&mut scroll, &action); @@ -1154,7 +1155,7 @@ mod tests { }); drive_to_fixpoint(&mut harness, virtual_scroll_id, driver); assert_render_snapshot!(harness, "virtual_scroll_moved"); - harness.mouse_move_to(virtual_scroll_id); + harness.mouse_move_to(virtual_scroll_tag); harness.mouse_wheel(Vec2 { x: 0., y: 25. }); drive_to_fixpoint(&mut harness, virtual_scroll_id, driver); assert_render_snapshot!(harness, "virtual_scroll_scrolled"); @@ -1167,6 +1168,7 @@ mod tests { let widget = VirtualScroll::new(0).prepare(); let mut harness = TestHarness::create_with_size(test_property_set(), widget, (100, 200)); + let virtual_scroll_tag = harness.root_tag(); let virtual_scroll_id = harness.root_id(); fn driver(action: VirtualScrollAction, mut scroll: WidgetMut<'_, VirtualScroll>) { VirtualScroll::will_handle_action(&mut scroll, &action); @@ -1194,7 +1196,7 @@ mod tests { VirtualScroll::overwrite_anchor(&mut scroll, 100); }); drive_to_fixpoint(&mut harness, virtual_scroll_id, driver); - harness.mouse_move_to(virtual_scroll_id); + harness.mouse_move_to(virtual_scroll_tag); harness.mouse_wheel(Vec2 { x: 0., y: 200. }); drive_to_fixpoint(&mut harness, virtual_scroll_id, driver); } @@ -1206,6 +1208,7 @@ mod tests { let widget = VirtualScroll::new(0).prepare(); let mut harness = TestHarness::create_with_size(test_property_set(), widget, (100, 200)); + let virtual_scroll_tag = harness.root_tag(); let virtual_scroll_id = harness.root_id(); fn driver(action: VirtualScrollAction, mut scroll: WidgetMut<'_, VirtualScroll>) { VirtualScroll::will_handle_action(&mut scroll, &action); @@ -1233,7 +1236,7 @@ mod tests { VirtualScroll::overwrite_anchor(&mut scroll, 200); }); drive_to_fixpoint(&mut harness, virtual_scroll_id, driver); - harness.mouse_move_to(virtual_scroll_id); + harness.mouse_move_to(virtual_scroll_tag); harness.mouse_wheel(Vec2 { x: 0., y: 200. }); drive_to_fixpoint(&mut harness, virtual_scroll_id, driver); } @@ -1245,6 +1248,7 @@ mod tests { let widget = VirtualScroll::new(0).prepare(); let mut harness = TestHarness::create_with_size(test_property_set(), widget, (100, 200)); + let virtual_scroll_tag = harness.root_tag(); let virtual_scroll_id = harness.root_id(); fn driver(action: VirtualScrollAction, mut scroll: WidgetMut<'_, VirtualScroll>) { VirtualScroll::will_handle_action(&mut scroll, &action); @@ -1272,7 +1276,7 @@ mod tests { VirtualScroll::overwrite_anchor(&mut scroll, 200); }); drive_to_fixpoint(&mut harness, virtual_scroll_id, driver); - harness.mouse_move_to(virtual_scroll_id); + harness.mouse_move_to(virtual_scroll_tag); harness.mouse_wheel(Vec2 { x: 0., y: 200. }); drive_to_fixpoint(&mut harness, virtual_scroll_id, driver); } @@ -1286,6 +1290,7 @@ mod tests { .prepare(); let mut harness = TestHarness::create_with_size(test_property_set(), widget, (100, 200)); + let virtual_scroll_tag = harness.root_tag(); let virtual_scroll_id = harness.root_id(); fn driver(action: VirtualScrollAction, mut scroll: WidgetMut<'_, VirtualScroll>) { VirtualScroll::will_handle_action(&mut scroll, &action); @@ -1326,7 +1331,7 @@ mod tests { ); original_range = widget.active_range.clone(); } - harness.mouse_move_to(virtual_scroll_id); + harness.mouse_move_to(virtual_scroll_tag); harness.mouse_wheel(Vec2 { x: 0., y: -50. }); drive_to_fixpoint(&mut harness, virtual_scroll_id, driver); { @@ -1352,6 +1357,7 @@ mod tests { .prepare(); let mut harness = TestHarness::create_with_size(test_property_set(), widget, (100, 200)); + let virtual_scroll_tag = harness.root_tag(); let virtual_scroll_id = harness.root_id(); fn driver(action: VirtualScrollAction, mut scroll: WidgetMut<'_, VirtualScroll>) { VirtualScroll::will_handle_action(&mut scroll, &action); @@ -1394,7 +1400,7 @@ mod tests { original_range = widget.active_range.clone(); assert_render_snapshot!(harness, "virtual_scroll_limited_up_bottom"); } - harness.mouse_move_to(virtual_scroll_id); + harness.mouse_move_to(virtual_scroll_tag); harness.mouse_wheel(Vec2 { x: 0., y: 5. }); drive_to_fixpoint(&mut harness, virtual_scroll_id, driver); { diff --git a/masonry_core/src/app/render_root.rs b/masonry_core/src/app/render_root.rs index 729bb4ef5..76647546a 100644 --- a/masonry_core/src/app/render_root.rs +++ b/masonry_core/src/app/render_root.rs @@ -645,6 +645,38 @@ impl RenderRoot { self.widget_arena.has(id) } + /// Creates a unique tag for the widget with the given id and returns it. + pub fn make_tag_for_widget(&mut self, id: WidgetId) -> WidgetTag { + let Some(node_ref) = self.widget_arena.nodes.find(id) else { + panic!("Could not find widget {id} in tree."); + }; + + let widget = &*node_ref.item.widget; + if widget.type_id() != TypeId::of::() { + panic!( + "Widget {id} does not have type {}.", + std::any::type_name::() + ); + } + + let new_tag = WidgetTag::unique(); + self.global_state.widget_tags.insert(new_tag.inner, id); + + new_tag + } + + /// Creates a unique type-erased tag for the widget with the given id and returns it. + pub fn make_dyn_tag_for_widget(&mut self, id: WidgetId) -> WidgetTag { + let Some(_) = self.widget_arena.nodes.find(id) else { + panic!("Could not find widget {id} in tree."); + }; + + let new_tag = WidgetTag::unique(); + self.global_state.widget_tags.insert(new_tag.inner, id); + + new_tag + } + /// Returns a [`WidgetMut`] to the root widget of the [base layer](crate::doc::masonry_concepts#layers). /// /// Because of how `WidgetMut` works, it can only be passed to a user-provided callback. diff --git a/masonry_core/src/core/widget_tag.rs b/masonry_core/src/core/widget_tag.rs index 0863df196..b23fd8b0f 100644 --- a/masonry_core/src/core/widget_tag.rs +++ b/masonry_core/src/core/widget_tag.rs @@ -30,7 +30,7 @@ pub(crate) struct WidgetTagInner { pub(crate) name: &'static str, } -impl WidgetTag { +impl WidgetTag { /// Creates a new tag with the given name. /// /// Calling this method twice with the same string will return the same tag. diff --git a/masonry_testing/src/harness.rs b/masonry_testing/src/harness.rs index 97e8770f3..8b8d59b7d 100644 --- a/masonry_testing/src/harness.rs +++ b/masonry_testing/src/harness.rs @@ -146,6 +146,7 @@ pub const PRIMARY_MOUSE: PointerInfo = PointerInfo { pub struct TestHarness { signal_receiver: mpsc::Receiver, render_root: RenderRoot, + root_tag: WidgetTag, access_tree: accesskit_consumer::Tree, renderer: Option, mouse_state: PointerState, @@ -394,20 +395,26 @@ impl TestHarness { }), focus: 0.into(), }; + + let mut render_root = RenderRoot::new( + root_widget, + move |signal| signal_sender.send(signal).unwrap(), + RenderRootOptions { + default_properties: Arc::new(default_props), + use_system_fonts: false, + size_policy: WindowSizePolicy::User, + size: window_size, + scale_factor: params.scale_factor, + test_font: Some(data), + }, + ); + let root_id = render_root.get_layer_root(0).id(); + let root_tag = render_root.make_tag_for_widget(root_id); + let mut harness = Self { signal_receiver, - render_root: RenderRoot::new( - root_widget, - move |signal| signal_sender.send(signal).unwrap(), - RenderRootOptions { - default_properties: Arc::new(default_props), - use_system_fonts: false, - size_policy: WindowSizePolicy::User, - size: window_size, - scale_factor: params.scale_factor, - test_font: Some(data), - }, - ), + render_root, + root_tag, access_tree: accesskit_consumer::Tree::new(dummy_tree_update, false), renderer: None, mouse_state, @@ -586,7 +593,11 @@ impl TestHarness { } /// Returns a reference to the current value of a node of the accessibility tree. - pub fn access_node(&self, id: WidgetId) -> Option> { + pub fn access_node( + &self, + tag: WidgetTag, + ) -> Option> { + let id = self.get_widget(tag).id(); let mut node_id = self.access_tree.state().root_id(); #[expect( unsafe_code, @@ -665,8 +676,12 @@ impl TestHarness { /// - If the widget doesn't accept pointer events. /// - If the widget is scrolled out of view. #[track_caller] - pub fn mouse_click_on(&mut self, id: WidgetId, button: Option) { - self.mouse_move_to(id); + pub fn mouse_click_on( + &mut self, + tag: WidgetTag, + button: Option, + ) { + self.mouse_move_to(tag); self.mouse_button_press(button); self.mouse_button_release(button); } @@ -680,8 +695,9 @@ impl TestHarness { /// - If the widget doesn't accept pointer events. /// - If the widget is scrolled out of view. #[track_caller] - pub fn mouse_move_to(&mut self, id: WidgetId) { - let widget = self.get_widget_with_id(id); + pub fn mouse_move_to(&mut self, tag: WidgetTag) { + let widget = self.get_widget(tag); + let id = widget.id(); let local_widget_center = (widget.ctx().border_box_size() / 2.0).to_vec2().to_point(); let widget_center = widget.ctx().window_transform() * local_widget_center; @@ -718,8 +734,12 @@ impl TestHarness { /// - If the widget is not found in the tree. /// - If the widget is stashed. #[track_caller] - pub fn mouse_move_to_unchecked(&mut self, id: WidgetId) { - let widget = self.get_widget_with_id(id); + pub fn mouse_move_to_unchecked( + &mut self, + tag: WidgetTag, + ) { + let widget = self.get_widget(tag); + let id = widget.id(); let local_widget_center = (widget.ctx().border_box_size() / 2.0).to_vec2().to_point(); let widget_center = widget.ctx().window_transform() * local_widget_center; @@ -739,7 +759,8 @@ impl TestHarness { /// [`RequestPanToChild`]: masonry_core::core::Update::RequestPanToChild /// [`ScrollIntoView`]: masonry_core::accesskit::Action::ScrollIntoView #[track_caller] - pub fn scroll_into_view(&mut self, id: WidgetId) { + pub fn scroll_into_view(&mut self, tag: WidgetTag) { + let id = self.get_widget(tag).id(); self.render_root.handle_access_event(ActionRequest { action: Action::ScrollIntoView, target_tree: TreeId::ROOT, @@ -762,7 +783,11 @@ impl TestHarness { /// /// [`Click`]: masonry_core::accesskit::Action::Click #[track_caller] - pub fn accessibility_click_on(&mut self, id: WidgetId) { + pub fn accessibility_click_on( + &mut self, + tag: WidgetTag, + ) { + let id = self.get_widget(tag).id(); self.render_root.handle_access_event(ActionRequest { action: Action::Click, target_tree: TreeId::ROOT, @@ -808,19 +833,18 @@ impl TestHarness { /// /// If the widget is not found in the tree or can't be focused. #[track_caller] - pub fn focus_on(&mut self, id: Option) { - if let Some(id) = id { - let Some(widget) = self.render_root.get_widget(id) else { - panic!("Cannot focus widget {id}: widget not found in tree"); - }; - if widget.ctx().is_stashed() { - panic!("Cannot focus widget {id}: widget is stashed"); - } - if widget.ctx().is_disabled() { - panic!("Cannot focus widget {id}: widget is disabled"); - } + pub fn focus_on(&mut self, tag: WidgetTag) { + let widget = self.get_widget(tag); + let id = widget.id(); + + if widget.ctx().is_stashed() { + panic!("Cannot focus widget {id}: widget is stashed"); + } + if widget.ctx().is_disabled() { + panic!("Cannot focus widget {id}: widget is disabled"); } - let succeeded = self.render_root.focus_on(id); + + let succeeded = self.render_root.focus_on(Some(id)); assert!( succeeded, "RenderRoot::focus_on refused a widget which TestHarness::focus_on accepted." @@ -828,14 +852,21 @@ impl TestHarness { self.process_signals(); } + /// Sets the [focused widget](masonry_core::doc::masonry_concepts#text-focus) to `None`. + pub fn clear_focus(&mut self) { + let _ = self.render_root.focus_on(None); + self.process_signals(); + } + /// Sets the [focus fallback](masonry_core::doc::masonry_concepts#focus-fallback). - pub fn set_focus_fallback(&mut self, id: Option) { - if let Some(id) = id { - let Some(_) = self.render_root.get_widget(id) else { - panic!("Cannot set widget {id} as focus fallback: widget not found in tree"); - }; - } - let _ = self.render_root.set_focus_fallback(id); + pub fn set_focus_fallback(&mut self, tag: WidgetTag) { + let id = self.get_widget(tag).id(); + let _ = self.render_root.set_focus_fallback(Some(id)); + } + + /// Sets the [focus fallback](masonry_core::doc::masonry_concepts#focus-fallback) to `None`. + pub fn clear_focus_fallback(&mut self) { + let _ = self.render_root.set_focus_fallback(None); } /// Runs an animation pass on the widget tree. @@ -864,6 +895,11 @@ impl TestHarness { self.render_root.get_layer_root(0).id() } + /// Returns a [`WidgetTag`] associated with the root widget. + pub fn root_tag(&self) -> WidgetTag { + self.root_tag + } + /// Returns a [`WidgetRef`] to the widget with the given id. /// /// # Panics @@ -891,6 +927,18 @@ impl TestHarness { .unwrap_or_else(|| panic!("could not find widget '{tag}'")) } + /// Creates a unique tag for the widget with the given id and returns it. + #[track_caller] + pub fn make_tag_for_widget(&mut self, id: WidgetId) -> WidgetTag { + self.render_root.make_tag_for_widget(id) + } + + /// Creates a unique type-erased tag for the widget with the given id and returns it. + #[track_caller] + pub fn make_dyn_tag_for_widget(&mut self, id: WidgetId) -> WidgetTag { + self.render_root.make_dyn_tag_for_widget(id) + } + /// Drains the events recorded by the [`Recorder`] widget with the given tag. /// /// # Panics From f778c9566e4969574200b6caf574f0293a60ffe5 Mon Sep 17 00:00:00 2001 From: Olivier FAURE Date: Sun, 19 Apr 2026 17:21:18 +0200 Subject: [PATCH 2/4] Fix code typo --- masonry_testing/src/harness.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masonry_testing/src/harness.rs b/masonry_testing/src/harness.rs index 8b8d59b7d..b5c2d02da 100644 --- a/masonry_testing/src/harness.rs +++ b/masonry_testing/src/harness.rs @@ -929,7 +929,7 @@ impl TestHarness { /// Creates a unique tag for the widget with the given id and returns it. #[track_caller] - pub fn make_tag_for_widget(&mut self, id: WidgetId) -> WidgetTag { + pub fn make_tag_for_widget(&mut self, id: WidgetId) -> WidgetTag { self.render_root.make_tag_for_widget(id) } From 52bd68e8d0bc620218017efa9bf92a4332772843 Mon Sep 17 00:00:00 2001 From: Olivier FAURE Date: Sun, 19 Apr 2026 17:59:26 +0200 Subject: [PATCH 3/4] Document `RenderRootState::widget_tags` --- masonry_core/src/app/render_root.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/masonry_core/src/app/render_root.rs b/masonry_core/src/app/render_root.rs index 76647546a..939348063 100644 --- a/masonry_core/src/app/render_root.rs +++ b/masonry_core/src/app/render_root.rs @@ -149,6 +149,8 @@ pub(crate) struct RenderRootState { /// Scene cache for the widget tree. pub(crate) scene_cache: HashMap, + // TODO - Add garbage collection. + /// Map from widget tags to widget ids. pub(crate) widget_tags: HashMap, /// Map of layers attached to widgets, keyed by the attached widget id, then the type of the layer root. From 2857c1e8f34d9dab7a41727ca05a11f2a847bba5 Mon Sep 17 00:00:00 2001 From: Olivier FAURE Date: Sun, 19 Apr 2026 18:01:25 +0200 Subject: [PATCH 4/4] Tweak make_tag_for_widget doc --- masonry_core/src/app/render_root.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/masonry_core/src/app/render_root.rs b/masonry_core/src/app/render_root.rs index 939348063..6789f3169 100644 --- a/masonry_core/src/app/render_root.rs +++ b/masonry_core/src/app/render_root.rs @@ -648,6 +648,8 @@ impl RenderRoot { } /// Creates a unique tag for the widget with the given id and returns it. + /// + /// This will return a unique tag even if called multiple times with the same id. pub fn make_tag_for_widget(&mut self, id: WidgetId) -> WidgetTag { let Some(node_ref) = self.widget_arena.nodes.find(id) else { panic!("Could not find widget {id} in tree."); @@ -668,6 +670,8 @@ impl RenderRoot { } /// Creates a unique type-erased tag for the widget with the given id and returns it. + /// + /// This will return a unique tag even if called multiple times with the same id. pub fn make_dyn_tag_for_widget(&mut self, id: WidgetId) -> WidgetTag { let Some(_) = self.widget_arena.nodes.find(id) else { panic!("Could not find widget {id} in tree.");