From 1923d1db1eb1726b24ba1b240fea57843288f8cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 23 Aug 2025 06:37:02 +0200 Subject: [PATCH] Try to find text targets in `tester::recorder` --- devtools/src/tester.rs | 1 + devtools/src/tester/recorder.rs | 259 +++++++++++++++++++++++++------- selector/src/lib.rs | 16 ++ selector/src/target.rs | 15 +- test/src/instruction.rs | 13 ++ 5 files changed, 245 insertions(+), 59 deletions(-) diff --git a/devtools/src/tester.rs b/devtools/src/tester.rs index 96715a4d..fda621c6 100644 --- a/devtools/src/tester.rs +++ b/devtools/src/tester.rs @@ -546,6 +546,7 @@ impl Tester

{ column(self.instructions.iter().enumerate().map( |(i, instruction)| { monospace(instruction.to_string()) + .wrapping(text::Wrapping::None) // TODO: Ellipsize? .size(10) .style(move |theme: &Theme| text::Style { color: match &self.state { diff --git a/devtools/src/tester/recorder.rs b/devtools/src/tester/recorder.rs index e1aa5655..a15e460c 100644 --- a/devtools/src/tester/recorder.rs +++ b/devtools/src/tester/recorder.rs @@ -3,26 +3,29 @@ use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; use crate::core::widget; +use crate::core::widget::operation; use crate::core::widget::tree; use crate::core::{ self, Clipboard, Element, Event, Layout, Length, Point, Rectangle, Shell, - Size, Vector, Widget, + Size, Theme, Vector, Widget, }; -use crate::test::instruction::Interaction; +use crate::test::Selector; +use crate::test::instruction::{Interaction, Mouse, Target}; +use crate::test::selector::target; -pub fn recorder<'a, Message, Theme, Renderer>( +pub fn recorder<'a, Message, Renderer>( content: impl Into>, -) -> Recorder<'a, Message, Theme, Renderer> { +) -> Recorder<'a, Message, Renderer> { Recorder::new(content) } #[allow(missing_debug_implementations)] -pub struct Recorder<'a, Message, Theme, Renderer> { +pub struct Recorder<'a, Message, Renderer> { content: Element<'a, Message, Theme, Renderer>, on_record: Option Message + 'a>>, } -impl<'a, Message, Theme, Renderer> Recorder<'a, Message, Theme, Renderer> { +impl<'a, Message, Renderer> Recorder<'a, Message, Renderer> { pub fn new( content: impl Into>, ) -> Self { @@ -41,14 +44,46 @@ impl<'a, Message, Theme, Renderer> Recorder<'a, Message, Theme, Renderer> { } } -impl Widget - for Recorder<'_, Message, Theme, Renderer> +struct State { + last_hovered: Option, + last_hovered_overlay: Option, +} + +impl Widget + for Recorder<'_, Message, Renderer> where Renderer: core::Renderer, { + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State { + last_hovered: None, + last_hovered_overlay: None, + }) + } + + fn children(&self) -> Vec { + vec![widget::Tree::new(&self.content)] + } + + fn diff(&self, tree: &mut tree::Tree) { + tree.diff_children(std::slice::from_ref(&self.content)); + } + + fn size(&self) -> Size { + self.content.as_widget().size() + } + + fn size_hint(&self) -> Size { + self.content.as_widget().size_hint() + } + fn update( &mut self, - state: &mut widget::Tree, + tree: &mut widget::Tree, event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, @@ -61,37 +96,43 @@ where return; } - self.content.as_widget_mut().update( - state, event, layout, cursor, renderer, clipboard, shell, viewport, - ); - if let Some(on_record) = &self.on_record { - record(event, cursor, shell, layout.bounds(), on_record); + let state = tree.state.downcast_mut::(); + + record( + event, + cursor, + shell, + layout.bounds(), + &mut state.last_hovered, + on_record, + |position| { + use widget::Operation; + + let mut selector = position.find_all(); + + self.content.as_widget_mut().operate( + &mut tree.children[0], + layout, + renderer, + &mut operation::black_box(&mut selector), + ); + + selector.finish() + }, + ); } - } - fn tag(&self) -> tree::Tag { - self.content.as_widget().tag() - } - - fn state(&self) -> tree::State { - self.content.as_widget().state() - } - - fn children(&self) -> Vec { - self.content.as_widget().children() - } - - fn diff(&self, tree: &mut tree::Tree) { - self.content.as_widget().diff(tree); - } - - fn size(&self) -> Size { - self.content.as_widget().size() - } - - fn size_hint(&self) -> Size { - self.content.as_widget().size_hint() + self.content.as_widget_mut().update( + &mut tree.children[0], + event, + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ); } fn layout( @@ -100,7 +141,9 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - self.content.as_widget().layout(tree, renderer, limits) + self.content + .as_widget() + .layout(&mut tree.children[0], renderer, limits) } fn draw( @@ -113,39 +156,70 @@ where cursor: mouse::Cursor, viewport: &Rectangle, ) { - self.content - .as_widget() - .draw(tree, renderer, theme, style, layout, cursor, viewport); + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + layout, + cursor, + viewport, + ); + + let state = tree.state.downcast_ref::(); + + let Some(last_hovered) = &state.last_hovered else { + return; + }; + + let palette = theme.palette(); + + renderer.with_layer(*viewport, |renderer| { + renderer.fill_quad( + renderer::Quad { + bounds: *last_hovered, + ..renderer::Quad::default() + }, + palette.primary.scale_alpha(0.5), + ); + }); } fn mouse_interaction( &self, - state: &widget::Tree, + tree: &widget::Tree, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - self.content - .as_widget() - .mouse_interaction(state, layout, cursor, viewport, renderer) + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor, + viewport, + renderer, + ) } fn operate( &self, - state: &mut widget::Tree, + tree: &mut widget::Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn widget::Operation, ) { - self.content - .as_widget() - .operate(state, layout, renderer, operation); + self.content.as_widget().operate( + &mut tree.children[0], + layout, + renderer, + operation, + ); } fn overlay<'a>( &'a mut self, - state: &'a mut widget::Tree, + tree: &'a mut widget::Tree, layout: Layout<'a>, renderer: &Renderer, _viewport: &Rectangle, @@ -153,25 +227,34 @@ where ) -> Option> { self.content .as_widget_mut() - .overlay(state, layout, renderer, &layout.bounds(), translation) + .overlay( + &mut tree.children[0], + layout, + renderer, + &layout.bounds(), + translation, + ) .map(|raw| { + let state = tree.state.downcast_mut::(); + overlay::Element::new(Box::new(Overlay { raw, bounds: layout.bounds(), + last_hovered: &mut state.last_hovered_overlay, on_record: self.on_record.as_deref(), })) }) } } -impl<'a, Message, Theme, Renderer> From> +impl<'a, Message, Renderer> From> for Element<'a, Message, Theme, Renderer> where Message: 'a, Theme: 'a, Renderer: core::Renderer + 'a, { - fn from(recorder: Recorder<'a, Message, Theme, Renderer>) -> Self { + fn from(recorder: Recorder<'a, Message, Renderer>) -> Self { Element::new(recorder) } } @@ -179,6 +262,7 @@ where struct Overlay<'a, Message, Theme, Renderer> { raw: overlay::Element<'a, Message, Theme, Renderer>, bounds: Rectangle, + last_hovered: &'a mut Option, on_record: Option<&'a dyn Fn(Interaction) -> Message>, } @@ -230,13 +314,33 @@ where return; } + if let Some(on_event) = &self.on_record { + record( + event, + cursor, + shell, + self.bounds, + self.last_hovered, + on_event, + |position| { + use widget::Operation; + + let mut selector = position.find_all(); + + self.raw.as_overlay_mut().operate( + layout, + renderer, + &mut operation::black_box(&mut selector), + ); + + selector.finish() + }, + ); + } + self.raw .as_overlay_mut() .update(event, layout, cursor, renderer, clipboard, shell); - - if let Some(on_event) = &self.on_record { - record(event, cursor, shell, self.bounds, on_event); - } } fn mouse_interaction( @@ -262,6 +366,7 @@ where overlay::Element::new(Box::new(Overlay { raw, bounds: self.bounds, + last_hovered: self.last_hovered, on_record: self.on_record, })) }) @@ -277,7 +382,9 @@ fn record( cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, bounds: Rectangle, + last_hovered: &mut Option, on_record: impl Fn(Interaction) -> Message, + find: impl FnOnce(Point) -> operation::Outcome>, ) { if let Event::Mouse(_) = event && !cursor.is_over(bounds) @@ -294,7 +401,43 @@ fn record( Interaction::from_event(event) }; - if let Some(interaction) = interaction { + if let Some(mut interaction) = interaction { + if let Interaction::Mouse( + Mouse::Move(at) + | Mouse::Press { at: Some(at), .. } + | Mouse::Release { at: Some(at), .. } + | Mouse::Click { at: Some(at), .. }, + ) = &mut interaction + { + if let Target::Point(position) = *at + && let operation::Outcome::Some(targets) = + find(position + (bounds.position() - Point::ORIGIN)) + && let Some((content, visible_bounds)) = + targets.into_iter().rev().find_map(|target| { + if let target::Match::Text { + content, + visible_bounds, + .. + } + | target::Match::TextInput { + content, + visible_bounds, + .. + } = target + { + Some((content, visible_bounds)) + } else { + None + } + }) + { + *at = Target::Text(content); + *last_hovered = visible_bounds; + } else { + *last_hovered = None; + } + } + shell.publish(on_record(interaction)); } } diff --git a/selector/src/lib.rs b/selector/src/lib.rs index c32d065f..18142882 100644 --- a/selector/src/lib.rs +++ b/selector/src/lib.rs @@ -8,6 +8,7 @@ mod find; pub use find::{Find, FindAll}; pub use target::Target; +use crate::core::Point; use crate::core::widget::Id; pub trait Selector { @@ -116,6 +117,21 @@ impl Selector for Id { } } +impl Selector for Point { + type Output = target::Match; + + fn select(&mut self, target: Target<'_>) -> Option { + target + .visible_bounds() + .is_some_and(|visible_bounds| visible_bounds.contains(*self)) + .then(|| target::Match::from_target(target)) + } + + fn description(&self) -> String { + format!("bounds contains {:?}", self) + } +} + impl Selector for F where F: FnMut(Target<'_>) -> Option, diff --git a/selector/src/target.rs b/selector/src/target.rs index fb2e6d0f..0ae0a50f 100644 --- a/selector/src/target.rs +++ b/selector/src/target.rs @@ -68,6 +68,17 @@ impl<'a> Target<'a> { | Target::Custom { bounds, .. } => *bounds, } } + + pub fn visible_bounds(&self) -> Option { + match self { + Target::Container { visible_bounds, .. } + | Target::Focusable { visible_bounds, .. } + | Target::Scrollable { visible_bounds, .. } + | Target::TextInput { visible_bounds, .. } + | Target::Text { visible_bounds, .. } + | Target::Custom { visible_bounds, .. } => *visible_bounds, + } + } } #[derive(Debug, Clone, PartialEq)] @@ -93,6 +104,7 @@ pub enum Match { id: Option, bounds: Rectangle, visible_bounds: Option, + content: String, }, Text { id: Option, @@ -147,11 +159,12 @@ impl Match { id, bounds, visible_bounds, - .. + state, } => Self::TextInput { id: id.cloned(), bounds, visible_bounds, + content: state.text().to_owned(), }, Target::Text { id, diff --git a/test/src/instruction.rs b/test/src/instruction.rs index 1d3dd025..c7945e83 100644 --- a/test/src/instruction.rs +++ b/test/src/instruction.rs @@ -129,6 +129,19 @@ impl Interaction { None, ) } + ( + Mouse::Click { + button, + at: Some(click_at), + }, + Mouse::Move(move_at), + ) if click_at == move_at => ( + Self::Mouse(Mouse::Click { + button, + at: Some(click_at), + }), + None, + ), (current, next) => { (Self::Mouse(current), Some(Self::Mouse(next))) }