From 63142d34fc440b18106aee8b45de195487d98c05 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 01:44:17 +0200 Subject: [PATCH] Introduce new `iced_selector` subcrate and refactor `Operation` --- Cargo.lock | 8 + Cargo.toml | 2 + core/src/overlay/group.rs | 2 +- core/src/widget/operation.rs | 132 ++++++------ core/src/widget/operation/focusable.rs | 60 ++---- core/src/widget/operation/scrollable.rs | 27 +-- core/src/widget/operation/text_input.rs | 36 +--- examples/toast/src/main.rs | 6 +- examples/todos/src/main.rs | 4 +- selector/Cargo.toml | 17 ++ selector/src/find.rs | 270 ++++++++++++++++++++++++ selector/src/lib.rs | 113 ++++++++++ selector/src/target.rs | 238 +++++++++++++++++++++ test/Cargo.toml | 1 + test/src/emulator.rs | 19 +- test/src/error.rs | 10 +- test/src/lib.rs | 3 +- test/src/selector.rs | 208 ------------------ test/src/simulator.rs | 44 ++-- widget/src/button.rs | 3 +- widget/src/column.rs | 3 +- widget/src/container.rs | 117 ++-------- widget/src/grid.rs | 3 +- widget/src/keyed/column.rs | 3 +- widget/src/lib.rs | 2 + widget/src/pane_grid.rs | 3 +- widget/src/row.rs | 3 +- widget/src/scrollable.rs | 20 +- widget/src/stack.rs | 3 +- 29 files changed, 839 insertions(+), 521 deletions(-) create mode 100644 selector/Cargo.toml create mode 100644 selector/src/find.rs create mode 100644 selector/src/lib.rs create mode 100644 selector/src/target.rs delete mode 100644 test/src/selector.rs diff --git a/Cargo.lock b/Cargo.lock index ecdcd3e0..cda9648a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2627,6 +2627,13 @@ dependencies = [ "thiserror 2.0.14", ] +[[package]] +name = "iced_selector" +version = "0.14.0-dev" +dependencies = [ + "iced_core", +] + [[package]] name = "iced_test" version = "0.14.0-dev" @@ -2634,6 +2641,7 @@ dependencies = [ "iced_program", "iced_renderer", "iced_runtime", + "iced_selector", "nom 8.0.0", "png", "sha2", diff --git a/Cargo.toml b/Cargo.toml index de46980a..432a9cd4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -132,6 +132,7 @@ members = [ "program", "renderer", "runtime", + "selector", "test", "tiny_skia", "wgpu", @@ -163,6 +164,7 @@ iced_highlighter = { version = "0.14.0-dev", path = "highlighter" } iced_program = { version = "0.14.0-dev", path = "program" } iced_renderer = { version = "0.14.0-dev", path = "renderer" } iced_runtime = { version = "0.14.0-dev", path = "runtime" } +iced_selector = { version = "0.14.0-dev", path = "selector" } iced_test = { version = "0.14.0-dev", path = "test" } iced_tiny_skia = { version = "0.14.0-dev", path = "tiny_skia" } iced_wgpu = { version = "0.14.0-dev", path = "wgpu" } diff --git a/core/src/overlay/group.rs b/core/src/overlay/group.rs index 145ee21d..cb734996 100644 --- a/core/src/overlay/group.rs +++ b/core/src/overlay/group.rs @@ -127,7 +127,7 @@ where renderer: &Renderer, operation: &mut dyn widget::Operation, ) { - operation.container(None, layout.bounds(), &mut |operation| { + operation.traverse(&mut |operation| { self.children.iter_mut().zip(layout.children()).for_each( |(child, layout)| { child.as_overlay_mut().operate(layout, renderer, operation); diff --git a/core/src/widget/operation.rs b/core/src/widget/operation.rs index 8fc627bf..070b369c 100644 --- a/core/src/widget/operation.rs +++ b/core/src/widget/operation.rs @@ -18,25 +18,16 @@ use std::sync::Arc; /// A piece of logic that can traverse the widget tree of an application in /// order to query or update some widget state. pub trait Operation: Send { - /// Operates on a widget that contains other widgets. + /// Requests further traversal of the widget tree to keep operating. /// - /// The `operate_on_children` function can be called to return control to - /// the widget tree and keep traversing it. - fn container( - &mut self, - id: Option<&Id>, - bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ); + /// The provided `operate` closure may be called by an [`Operation`] + /// to return control to the widget tree and keep traversing it. If + /// the closure is not called, the children of the widget asking for + /// traversal will be skipped. + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)); - /// Operates on a widget that can be focused. - fn focusable( - &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - _state: &mut dyn Focusable, - ) { - } + /// Operates on a widget that contains other widgets. + fn container(&mut self, _id: Option<&Id>, _bounds: Rectangle) {} /// Operates on a widget that can be scrolled. fn scrollable( @@ -49,6 +40,15 @@ pub trait Operation: Send { ) { } + /// Operates on a widget that can be focused. + fn focusable( + &mut self, + _id: Option<&Id>, + _bounds: Rectangle, + _state: &mut dyn Focusable, + ) { + } + /// Operates on a widget that has text input. fn text_input( &mut self, @@ -80,13 +80,12 @@ impl Operation for Box where T: Operation + ?Sized, { - fn container( - &mut self, - id: Option<&Id>, - bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - self.as_mut().container(id, bounds, operate_on_children); + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) { + self.as_mut().traverse(operate); + } + + fn container(&mut self, id: Option<&Id>, bounds: Rectangle) { + self.as_mut().container(id, bounds); } fn focusable( @@ -179,17 +178,19 @@ where } impl Operation for BlackBox<'_, T> { - fn container( - &mut self, - id: Option<&Id>, - bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - self.operation.container(id, bounds, &mut |operation| { - operate_on_children(&mut BlackBox { operation }); + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) + where + Self: Sized, + { + self.operation.traverse(&mut |operation| { + operate(&mut BlackBox { operation }); }); } + fn container(&mut self, id: Option<&Id>, bounds: Rectangle) { + self.operation.container(id, bounds); + } + fn focusable( &mut self, id: Option<&Id>, @@ -267,28 +268,25 @@ where A: 'static, B: 'static, { - fn container( - &mut self, - id: Option<&Id>, - bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) { struct MapRef<'a, A> { operation: &'a mut dyn Operation, } impl Operation for MapRef<'_, A> { - fn container( + fn traverse( &mut self, - id: Option<&Id>, - bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), + operate: &mut dyn FnMut(&mut dyn Operation), ) { + self.operation.traverse(&mut |operation| { + operate(&mut MapRef { operation }); + }); + } + + fn container(&mut self, id: Option<&Id>, bounds: Rectangle) { let Self { operation, .. } = self; - operation.container(id, bounds, &mut |operation| { - operate_on_children(&mut MapRef { operation }); - }); + operation.container(id, bounds); } fn scrollable( @@ -345,9 +343,13 @@ where } } - let Self { operation, .. } = self; + self.operation.traverse(&mut |operation| { + operate(&mut MapRef { operation }); + }); + } - MapRef { operation }.container(id, bounds, operate_on_children); + fn container(&mut self, id: Option<&Id>, bounds: Rectangle) { + self.operation.container(id, bounds); } fn focusable( @@ -444,17 +446,16 @@ where A: 'static, B: Send + 'static, { - fn container( - &mut self, - id: Option<&Id>, - bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - self.operation.container(id, bounds, &mut |operation| { - operate_on_children(&mut black_box(operation)); + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) { + self.operation.traverse(&mut |operation| { + operate(&mut black_box(operation)); }); } + fn container(&mut self, id: Option<&Id>, bounds: Rectangle) { + self.operation.container(id, bounds); + } + fn focusable( &mut self, id: Option<&Id>, @@ -531,21 +532,26 @@ pub fn scope( ) -> impl Operation { struct ScopedOperation { target: Id, + current: Option, operation: Box>, } impl Operation for ScopedOperation { - fn container( + fn traverse( &mut self, - id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), + operate: &mut dyn FnMut(&mut dyn Operation), ) { - if id == Some(&self.target) { - operate_on_children(self.operation.as_mut()); + if self.current.as_ref() == Some(&self.target) { + self.operation.as_mut().traverse(operate); } else { - operate_on_children(self); + operate(self); } + + self.current = None; + } + + fn container(&mut self, id: Option<&Id>, _bounds: Rectangle) { + self.current = id.cloned(); } fn finish(&self) -> Outcome { @@ -553,6 +559,7 @@ pub fn scope( Outcome::Chain(next) => { Outcome::Chain(Box::new(ScopedOperation { target: self.target.clone(), + current: None, operation: next, })) } @@ -563,6 +570,7 @@ pub fn scope( ScopedOperation { target, + current: None, operation: Box::new(operation), } } diff --git a/core/src/widget/operation/focusable.rs b/core/src/widget/operation/focusable.rs index 44c9d647..5b066f2d 100644 --- a/core/src/widget/operation/focusable.rs +++ b/core/src/widget/operation/focusable.rs @@ -48,13 +48,8 @@ pub fn focus(target: Id) -> impl Operation { } } - fn container( - &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self); + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) { + operate(self); } } @@ -75,13 +70,8 @@ pub fn unfocus() -> impl Operation { state.unfocus(); } - fn container( - &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self); + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) { + operate(self); } } @@ -109,13 +99,11 @@ pub fn count() -> impl Operation { self.count.total += 1; } - fn container( + fn traverse( &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), + operate: &mut dyn FnMut(&mut dyn Operation), ) { - operate_on_children(self); + operate(self); } fn finish(&self) -> Outcome { @@ -163,13 +151,8 @@ where self.current += 1; } - fn container( - &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self); + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) { + operate(self); } } @@ -205,13 +188,8 @@ where self.current += 1; } - fn container( - &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self); + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) { + operate(self); } } @@ -237,13 +215,11 @@ pub fn find_focused() -> impl Operation { } } - fn container( + fn traverse( &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), + operate: &mut dyn FnMut(&mut dyn Operation), ) { - operate_on_children(self); + operate(self); } fn finish(&self) -> Outcome { @@ -279,17 +255,15 @@ pub fn is_focused(target: Id) -> impl Operation { } } - fn container( + fn traverse( &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), + operate: &mut dyn FnMut(&mut dyn Operation), ) { if self.is_focused.is_some() { return; } - operate_on_children(self); + operate(self); } fn finish(&self) -> Outcome { diff --git a/core/src/widget/operation/scrollable.rs b/core/src/widget/operation/scrollable.rs index 7c78c087..25a03434 100644 --- a/core/src/widget/operation/scrollable.rs +++ b/core/src/widget/operation/scrollable.rs @@ -28,13 +28,8 @@ pub fn snap_to(target: Id, offset: RelativeOffset) -> impl Operation { } impl Operation for SnapTo { - fn container( - &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self); + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) { + operate(self); } fn scrollable( @@ -63,13 +58,8 @@ pub fn scroll_to(target: Id, offset: AbsoluteOffset) -> impl Operation { } impl Operation for ScrollTo { - fn container( - &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self); + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) { + operate(self); } fn scrollable( @@ -98,13 +88,8 @@ pub fn scroll_by(target: Id, offset: AbsoluteOffset) -> impl Operation { } impl Operation for ScrollBy { - fn container( - &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self); + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) { + operate(self); } fn scrollable( diff --git a/core/src/widget/operation/text_input.rs b/core/src/widget/operation/text_input.rs index 6bcae385..de2ac1a0 100644 --- a/core/src/widget/operation/text_input.rs +++ b/core/src/widget/operation/text_input.rs @@ -45,13 +45,8 @@ pub fn move_cursor_to_front(target: Id) -> impl Operation { } } - fn container( - &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self); + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) { + operate(self); } } @@ -80,13 +75,8 @@ pub fn move_cursor_to_end(target: Id) -> impl Operation { } } - fn container( - &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self); + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) { + operate(self); } } @@ -116,13 +106,8 @@ pub fn move_cursor_to(target: Id, position: usize) -> impl Operation { } } - fn container( - &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self); + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) { + operate(self); } } @@ -150,13 +135,8 @@ pub fn select_all(target: Id) -> impl Operation { } } - fn container( - &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self); + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) { + operate(self); } } diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index b4e93a32..e825f4f3 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -349,7 +349,8 @@ mod toast { renderer: &Renderer, operation: &mut dyn Operation, ) { - operation.container(None, layout.bounds(), &mut |operation| { + operation.container(None, layout.bounds()); + operation.traverse(&mut |operation| { self.content.as_widget().operate( &mut state.children[0], layout, @@ -580,7 +581,8 @@ mod toast { renderer: &Renderer, operation: &mut dyn widget::Operation, ) { - operation.container(None, layout.bounds(), &mut |operation| { + operation.container(None, layout.bounds()); + operation.traverse(&mut |operation| { self.toasts .iter() .zip(self.state.iter_mut()) diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index e0c60e94..5dab262c 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -613,8 +613,8 @@ fn presets() -> impl Iterator> mod tests { use super::*; + use iced::widget; use iced::{Settings, Theme}; - use iced_test::selector::id; use iced_test::{Error, Simulator}; fn simulator(todos: &Todos) -> Simulator<'_, Message> { @@ -633,7 +633,7 @@ mod tests { let _command = todos.update(Message::Loaded(Err(LoadError::File))); let mut ui = simulator(&todos); - let _input = ui.click(id("new-task"))?; + let _input = ui.click(widget::Id::new("new-task"))?; let _ = ui.typewrite("Create the universe"); let _ = ui.tap_key(keyboard::key::Named::Enter); diff --git a/selector/Cargo.toml b/selector/Cargo.toml new file mode 100644 index 00000000..67488d12 --- /dev/null +++ b/selector/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "iced_selector" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +categories.workspace = true +keywords.workspace = true +rust-version.workspace = true + +[dependencies] +iced_core.workspace = true + +[lints] +workspace = true diff --git a/selector/src/find.rs b/selector/src/find.rs new file mode 100644 index 00000000..15016933 --- /dev/null +++ b/selector/src/find.rs @@ -0,0 +1,270 @@ +use crate::core::widget::operation::{ + Focusable, Outcome, Scrollable, TextInput, +}; +use crate::core::widget::{Id, Operation}; +use crate::core::{Rectangle, Vector}; +use crate::{Selector, Target}; + +use std::any::Any; + +pub type Find = Finder>; +pub type FindAll = Finder>; + +#[derive(Debug)] +pub struct One +where + S: Selector, +{ + selector: S, + output: Option, +} + +impl One +where + S: Selector, +{ + pub fn new(selector: S) -> Self { + Self { + selector, + output: None, + } + } +} + +impl Strategy for One +where + S: Selector, + S::Output: Clone, +{ + type Output = Option; + + fn feed(&mut self, target: Target<'_>) { + if let Some(output) = self.selector.select(target) { + self.output = Some(output); + } + } + + fn is_done(&self) -> bool { + self.output.is_some() + } + + fn finish(&self) -> Self::Output { + self.output.clone() + } +} + +#[derive(Debug)] +pub struct All +where + S: Selector, +{ + selector: S, + outputs: Vec, +} + +impl All +where + S: Selector, +{ + pub fn new(selector: S) -> Self { + Self { + selector, + outputs: Vec::new(), + } + } +} + +impl Strategy for All +where + S: Selector, + S::Output: Clone, +{ + type Output = Vec; + + fn feed(&mut self, target: Target<'_>) { + if let Some(output) = self.selector.select(target) { + self.outputs.push(output); + } + } + + fn is_done(&self) -> bool { + false + } + + fn finish(&self) -> Self::Output { + self.outputs.clone() + } +} + +pub trait Strategy { + type Output; + + fn feed(&mut self, target: Target<'_>); + + fn is_done(&self) -> bool; + + fn finish(&self) -> Self::Output; +} + +#[derive(Debug)] +pub struct Finder { + strategy: S, + stack: Vec<(Rectangle, Vector)>, + viewport: Rectangle, + translation: Vector, +} + +impl Finder { + pub fn new(strategy: S) -> Self { + Self { + strategy, + stack: vec![(Rectangle::INFINITE, Vector::ZERO)], + viewport: Rectangle::INFINITE, + translation: Vector::ZERO, + } + } +} + +impl Operation for Finder +where + S: Strategy + Send, + S::Output: Send, +{ + fn traverse( + &mut self, + operate: &mut dyn FnMut(&mut dyn Operation), + ) { + if self.strategy.is_done() { + return; + } + + self.stack.push((self.viewport, self.translation)); + operate(self); + let _ = self.stack.pop(); + + let (viewport, translation) = self.stack.last().unwrap(); + self.viewport = *viewport; + self.translation = *translation; + } + + fn container(&mut self, id: Option<&Id>, bounds: Rectangle) { + if self.strategy.is_done() { + return; + } + + self.strategy.feed(Target::Container { + id, + bounds, + visible_bounds: self + .viewport + .intersection(&(bounds + self.translation)), + }); + } + + fn focusable( + &mut self, + id: Option<&Id>, + bounds: Rectangle, + state: &mut dyn Focusable, + ) { + if self.strategy.is_done() { + return; + } + + self.strategy.feed(Target::Focusable { + id, + bounds, + visible_bounds: self + .viewport + .intersection(&(bounds + self.translation)), + state, + }); + } + + fn scrollable( + &mut self, + id: Option<&Id>, + bounds: Rectangle, + content_bounds: Rectangle, + translation: Vector, + state: &mut dyn Scrollable, + ) { + if self.strategy.is_done() { + return; + } + + let visible_bounds = + self.viewport.intersection(&(bounds + self.translation)); + + self.strategy.feed(Target::Scrollable { + id, + bounds, + visible_bounds, + content_bounds, + translation, + state, + }); + + self.translation = self.translation - translation; + self.viewport = visible_bounds.unwrap_or_default(); + } + + fn text_input( + &mut self, + id: Option<&Id>, + bounds: Rectangle, + state: &mut dyn TextInput, + ) { + if self.strategy.is_done() { + return; + } + + self.strategy.feed(Target::TextInput { + id, + bounds, + visible_bounds: self + .viewport + .intersection(&(bounds + self.translation)), + state, + }); + } + + fn text(&mut self, id: Option<&Id>, bounds: Rectangle, text: &str) { + if self.strategy.is_done() { + return; + } + + self.strategy.feed(Target::Text { + id, + bounds, + visible_bounds: self + .viewport + .intersection(&(bounds + self.translation)), + content: text, + }); + } + + fn custom( + &mut self, + id: Option<&Id>, + bounds: Rectangle, + state: &mut dyn Any, + ) { + if self.strategy.is_done() { + return; + } + + self.strategy.feed(Target::Custom { + id, + bounds, + visible_bounds: self + .viewport + .intersection(&(bounds + self.translation)), + state, + }); + } + + fn finish(&self) -> Outcome { + Outcome::Some(self.strategy.finish()) + } +} diff --git a/selector/src/lib.rs b/selector/src/lib.rs new file mode 100644 index 00000000..2163c502 --- /dev/null +++ b/selector/src/lib.rs @@ -0,0 +1,113 @@ +#![allow(missing_docs)] +use iced_core as core; + +pub mod target; + +mod find; + +pub use find::{Find, FindAll}; +pub use target::Target; + +use crate::core::widget::Id; + +pub trait Selector { + type Output; + + fn select(&mut self, target: Target<'_>) -> Option; + + fn description(&self) -> String; + + fn find(self) -> Find + where + Self: Sized, + { + Find::new(find::One::new(self)) + } + + fn find_all(self) -> FindAll + where + Self: Sized, + { + FindAll::new(find::All::new(self)) + } +} + +impl Selector for &str { + type Output = target::Text; + + fn select(&mut self, target: Target<'_>) -> Option { + match target { + Target::TextInput { + id, + bounds, + visible_bounds, + state, + } if state.text() == *self => Some(target::Text::Input { + id: id.cloned(), + bounds, + visible_bounds, + }), + Target::Text { + id, + bounds, + visible_bounds, + content, + } if content == *self => Some(target::Text::Raw { + id: id.cloned(), + bounds, + visible_bounds, + }), + _ => None, + } + } + + fn description(&self) -> String { + format!("text == \"{}\"", self.escape_default()) + } +} + +impl Selector for Id { + type Output = target::Match; + + fn select(&mut self, target: Target<'_>) -> Option { + if target.id() != Some(self) { + return None; + } + + Some(target::Match::from_target(target)) + } + + fn description(&self) -> String { + format!("id == \"{:?}\"", self) + } +} + +impl Selector for F +where + F: FnMut(Target<'_>) -> Option, +{ + type Output = T; + + fn select(&mut self, target: Target<'_>) -> Option { + (self)(target) + } + + fn description(&self) -> String { + format!("custom selector: {}", std::any::type_name_of_val(self)) + } +} + +// pub fn inspect(position: Point) -> impl Selector { +// visible(move |target: Target<'_>, visible_bounds: Rectangle| { +// visible_bounds +// .contains(position) +// .then(|| Match::from_target(target)) +// }) +// } + +// pub fn visible( +// f: impl Fn(Target<'_>, Rectangle) -> Option, +// ) -> impl Selector { +// todo!() +// } +// diff --git a/selector/src/target.rs b/selector/src/target.rs new file mode 100644 index 00000000..fb2e6d0f --- /dev/null +++ b/selector/src/target.rs @@ -0,0 +1,238 @@ +use crate::core::widget::Id; +use crate::core::widget::operation::{Focusable, Scrollable, TextInput}; +use crate::core::{Rectangle, Vector}; + +use std::any::Any; + +#[derive(Clone)] +#[allow(missing_debug_implementations)] +pub enum Target<'a> { + Container { + id: Option<&'a Id>, + bounds: Rectangle, + visible_bounds: Option, + }, + Focusable { + id: Option<&'a Id>, + bounds: Rectangle, + visible_bounds: Option, + state: &'a dyn Focusable, + }, + Scrollable { + id: Option<&'a Id>, + bounds: Rectangle, + content_bounds: Rectangle, + visible_bounds: Option, + translation: Vector, + state: &'a dyn Scrollable, + }, + TextInput { + id: Option<&'a Id>, + bounds: Rectangle, + visible_bounds: Option, + state: &'a dyn TextInput, + }, + Text { + id: Option<&'a Id>, + bounds: Rectangle, + visible_bounds: Option, + content: &'a str, + }, + Custom { + id: Option<&'a Id>, + bounds: Rectangle, + visible_bounds: Option, + state: &'a dyn Any, + }, +} + +impl<'a> Target<'a> { + pub fn id(&self) -> Option<&'a Id> { + match self { + Target::Container { id, .. } + | Target::Focusable { id, .. } + | Target::Scrollable { id, .. } + | Target::TextInput { id, .. } + | Target::Text { id, .. } + | Target::Custom { id, .. } => *id, + } + } + + pub fn bounds(&self) -> Rectangle { + match self { + Target::Container { bounds, .. } + | Target::Focusable { bounds, .. } + | Target::Scrollable { bounds, .. } + | Target::TextInput { bounds, .. } + | Target::Text { bounds, .. } + | Target::Custom { bounds, .. } => *bounds, + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Match { + Container { + id: Option, + bounds: Rectangle, + visible_bounds: Option, + }, + Focusable { + id: Option, + bounds: Rectangle, + visible_bounds: Option, + }, + Scrollable { + id: Option, + bounds: Rectangle, + visible_bounds: Option, + content_bounds: Rectangle, + translation: Vector, + }, + TextInput { + id: Option, + bounds: Rectangle, + visible_bounds: Option, + }, + Text { + id: Option, + bounds: Rectangle, + visible_bounds: Option, + content: String, + }, + Custom { + id: Option, + bounds: Rectangle, + visible_bounds: Option, + }, +} + +impl Match { + pub fn from_target(target: Target<'_>) -> Self { + match target { + Target::Container { + id, + bounds, + visible_bounds, + } => Self::Container { + id: id.cloned(), + bounds, + visible_bounds, + }, + Target::Focusable { + id, + bounds, + visible_bounds, + .. + } => Self::Focusable { + id: id.cloned(), + bounds, + visible_bounds, + }, + Target::Scrollable { + id, + bounds, + visible_bounds, + content_bounds, + translation, + .. + } => Self::Scrollable { + id: id.cloned(), + bounds, + visible_bounds, + content_bounds, + translation, + }, + Target::TextInput { + id, + bounds, + visible_bounds, + .. + } => Self::TextInput { + id: id.cloned(), + bounds, + visible_bounds, + }, + Target::Text { + id, + bounds, + visible_bounds, + content, + } => Self::Text { + id: id.cloned(), + bounds, + visible_bounds, + content: content.to_owned(), + }, + Target::Custom { + id, + bounds, + visible_bounds, + .. + } => Self::Custom { + id: id.cloned(), + bounds, + visible_bounds, + }, + } + } +} + +impl Bounded for Match { + fn bounds(&self) -> Rectangle { + match self { + Match::Container { bounds, .. } + | Match::Focusable { bounds, .. } + | Match::Scrollable { bounds, .. } + | Match::TextInput { bounds, .. } + | Match::Text { bounds, .. } + | Match::Custom { bounds, .. } => *bounds, + } + } + + fn visible_bounds(&self) -> Option { + match self { + Match::Container { visible_bounds, .. } + | Match::Focusable { visible_bounds, .. } + | Match::Scrollable { visible_bounds, .. } + | Match::TextInput { visible_bounds, .. } + | Match::Text { visible_bounds, .. } + | Match::Custom { visible_bounds, .. } => *visible_bounds, + } + } +} + +pub trait Bounded: std::fmt::Debug { + fn bounds(&self) -> Rectangle; + + fn visible_bounds(&self) -> Option; +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Text { + Raw { + id: Option, + bounds: Rectangle, + visible_bounds: Option, + }, + Input { + id: Option, + bounds: Rectangle, + visible_bounds: Option, + }, +} + +impl Bounded for Text { + fn bounds(&self) -> Rectangle { + match self { + Text::Raw { bounds, .. } | Text::Input { bounds, .. } => *bounds, + } + } + + fn visible_bounds(&self) -> Option { + match self { + Text::Raw { visible_bounds, .. } + | Text::Input { visible_bounds, .. } => *visible_bounds, + } + } +} diff --git a/test/Cargo.toml b/test/Cargo.toml index dd262ffc..af15020e 100644 --- a/test/Cargo.toml +++ b/test/Cargo.toml @@ -15,6 +15,7 @@ workspace = true [dependencies] iced_runtime.workspace = true +iced_selector.workspace = true iced_program.workspace = true iced_program.features = ["test"] diff --git a/test/src/emulator.rs b/test/src/emulator.rs index 710e5b1b..b51d4a5c 100644 --- a/test/src/emulator.rs +++ b/test/src/emulator.rs @@ -1,4 +1,3 @@ -use crate::Instruction; use crate::core; use crate::core::mouse; use crate::core::renderer; @@ -17,6 +16,7 @@ use crate::runtime::user_interface; use crate::runtime::window; use crate::runtime::{Action, Task, UserInterface}; use crate::selector; +use crate::{Instruction, Selector}; use std::fmt; @@ -216,10 +216,10 @@ impl Emulator

{ let Some(events) = interaction.events(|target| match target { instruction::Target::Point(position) => Some(*position), instruction::Target::Text(text) => { + use selector::target::Bounded; use widget::Operation; - let mut operation = - selector::text(text.to_owned()).operation(); + let mut operation = Selector::find(text.as_str()); user_interface.operate( &self.renderer, @@ -227,11 +227,8 @@ impl Emulator

{ ); match operation.finish() { - widget::operation::Outcome::Some(matches) => { - matches - .first() - .copied() - .map(|target| target.bounds.center()) + widget::operation::Outcome::Some(text) => { + Some(text?.visible_bounds()?.center()) } _ => None, } @@ -273,7 +270,7 @@ impl Emulator

{ instruction::Expectation::Text(text) => { use widget::Operation; - let mut operation = selector::text(text).operation(); + let mut operation = Selector::find(text.as_str()); user_interface.operate( &self.renderer, @@ -281,9 +278,7 @@ impl Emulator

{ ); match operation.finish() { - widget::operation::Outcome::Some(matches) - if matches.len() == 1 => - { + widget::operation::Outcome::Some(Some(_text)) => { self.runtime.send(Event::Ready); } _ => { diff --git a/test/src/error.rs b/test/src/error.rs index ae475f16..45269a81 100644 --- a/test/src/error.rs +++ b/test/src/error.rs @@ -1,5 +1,3 @@ -use crate::Selector; - use std::io; use std::sync::Arc; @@ -7,8 +5,12 @@ use std::sync::Arc; #[derive(Debug, Clone, thiserror::Error)] pub enum Error { /// No matching widget was found for the [`Selector`]. - #[error("no matching widget was found for the selector: {0:?}")] - NotFound(Selector), + #[error("no matching widget was found for the selector: {selector}")] + NotFound { selector: String }, + #[error("the matching target is not visible: {target:?}")] + NotVisible { + target: Arc, + }, /// An IO operation failed. #[error("an IO operation failed: {0}")] IOFailed(Arc), diff --git a/test/src/lib.rs b/test/src/lib.rs index 150521f0..4ba2fe9c 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -89,10 +89,11 @@ use iced_renderer as renderer; use iced_runtime as runtime; use iced_runtime::core; +pub use iced_selector as selector; + pub mod emulator; pub mod ice; pub mod instruction; -pub mod selector; pub mod simulator; mod error; diff --git a/test/src/selector.rs b/test/src/selector.rs deleted file mode 100644 index fa6fce5a..00000000 --- a/test/src/selector.rs +++ /dev/null @@ -1,208 +0,0 @@ -//! Select widgets of a user interface. -use crate::core::text; -use crate::core::widget; -use crate::core::{Rectangle, Vector}; - -/// A selector describes a strategy to find a certain widget in a user interface. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Selector { - /// Find the widget with the given [`widget::Id`]. - Id(widget::Id), - /// Find the widget containing the given [`text::Fragment`]. - Text(text::Fragment<'static>), -} - -impl Selector { - pub fn operation<'a>(&self) -> impl widget::Operation> + 'a { - match self { - Selector::Id(id) => { - struct FindById { - id: widget::Id, - target: Option, - } - - impl widget::Operation> for FindById { - fn container( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - operate_on_children: &mut dyn FnMut( - &mut dyn widget::Operation>, - ), - ) { - if self.target.is_some() { - return; - } - - if Some(&self.id) == id { - self.target = Some(Target { bounds }); - return; - } - - operate_on_children(self); - } - - fn scrollable( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - _content_bounds: Rectangle, - _translation: Vector, - _state: &mut dyn widget::operation::Scrollable, - ) { - if self.target.is_some() { - return; - } - - if Some(&self.id) == id { - self.target = Some(Target { bounds }); - } - } - - fn text_input( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - _state: &mut dyn widget::operation::TextInput, - ) { - if self.target.is_some() { - return; - } - - if Some(&self.id) == id { - self.target = Some(Target { bounds }); - } - } - - fn text( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - _text: &str, - ) { - if self.target.is_some() { - return; - } - - if Some(&self.id) == id { - self.target = Some(Target { bounds }); - } - } - - fn custom( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - _state: &mut dyn std::any::Any, - ) { - if self.target.is_some() { - return; - } - - if Some(&self.id) == id { - self.target = Some(Target { bounds }); - } - } - - fn finish( - &self, - ) -> widget::operation::Outcome> - { - if let Some(target) = self.target { - widget::operation::Outcome::Some(vec![target]) - } else { - widget::operation::Outcome::None - } - } - } - - Box::new(FindById { - id: id.clone(), - target: None, - }) as Box> - } - Selector::Text(text) => { - struct FindByText { - text: text::Fragment<'static>, - target: Vec, - } - - impl widget::Operation> for FindByText { - fn container( - &mut self, - _id: Option<&widget::Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut( - &mut dyn widget::Operation>, - ), - ) { - operate_on_children(self); - } - - fn text_input( - &mut self, - _id: Option<&widget::Id>, - bounds: Rectangle, - state: &mut dyn widget::operation::TextInput, - ) { - if self.text == state.text() { - self.target.push(Target { bounds }); - } - } - - fn text( - &mut self, - _id: Option<&widget::Id>, - bounds: Rectangle, - text: &str, - ) { - if self.text == text { - self.target.push(Target { bounds }); - } - } - - fn finish( - &self, - ) -> widget::operation::Outcome> - { - widget::operation::Outcome::Some(self.target.clone()) - } - } - - Box::new(FindByText { - text: text.clone(), - target: Vec::new(), - }) - } - } - } -} - -impl From for Selector { - fn from(id: widget::Id) -> Self { - Self::Id(id) - } -} - -impl From<&'static str> for Selector { - fn from(text: &'static str) -> Self { - Self::Text(text.into()) - } -} - -/// Creates a [`Selector`] that finds the widget with the given [`widget::Id`]. -pub fn id(id: impl Into) -> Selector { - Selector::Id(id.into()) -} - -/// Creates a [`Selector`] that finds the widget containing the given text fragment. -pub fn text(fragment: impl text::IntoFragment<'static>) -> Selector { - Selector::Text(fragment.into_fragment()) -} - -/// A specific area, normally containing a widget. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Target { - /// The bounds of the area. - pub bounds: Rectangle, -} diff --git a/test/src/simulator.rs b/test/src/simulator.rs index 4e643e5b..a01bf28b 100644 --- a/test/src/simulator.rs +++ b/test/src/simulator.rs @@ -12,13 +12,14 @@ use crate::core::{Element, Event, Font, Point, Settings, Size, SmolStr}; use crate::renderer; use crate::runtime::UserInterface; use crate::runtime::user_interface; -use crate::selector; +use crate::selector::target::Bounded; use crate::{Error, Selector}; use std::borrow::Cow; use std::env; use std::fs; use std::path::{Path, PathBuf}; +use std::sync::Arc; /// A user interface that can be interacted with and inspected programmatically. #[allow(missing_debug_implementations)] @@ -100,14 +101,15 @@ where } /// Finds the [`Target`] of the given widget [`Selector`] in the [`Simulator`]. - pub fn find( - &mut self, - selector: impl Into, - ) -> Result { + pub fn find(&mut self, selector: S) -> Result + where + S: Selector + Send, + S::Output: Clone + Send, + { use widget::Operation; - let selector = selector.into(); - let mut operation = selector.operation(); + let description = selector.description(); + let mut operation = selector.find(); self.raw.operate( &self.renderer, @@ -115,10 +117,14 @@ where ); match operation.finish() { - widget::operation::Outcome::Some(matches) => { - matches.first().copied().ok_or(Error::NotFound(selector)) + widget::operation::Outcome::Some(output) => { + output.ok_or(Error::NotFound { + selector: description, + }) } - _ => Err(Error::NotFound(selector)), + _ => Err(Error::NotFound { + selector: description, + }), } } @@ -134,12 +140,20 @@ where /// This consists in: /// - Pointing the mouse cursor at the center of the [`Target`]. /// - Simulating a [`click`]. - pub fn click( - &mut self, - selector: impl Into, - ) -> Result { + pub fn click(&mut self, selector: S) -> Result + where + S: Selector + Send, + S::Output: Bounded + Clone + Send + Sync + 'static, + { let target = self.find(selector)?; - self.point_at(target.bounds.center()); + + let Some(visible_bounds) = target.visible_bounds() else { + return Err(Error::NotVisible { + target: Arc::new(target), + }); + }; + + self.point_at(visible_bounds.center()); let _ = self.simulate(click()); diff --git a/widget/src/button.rs b/widget/src/button.rs index ac4a27cb..d9021597 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -262,7 +262,8 @@ where renderer: &Renderer, operation: &mut dyn Operation, ) { - operation.container(None, layout.bounds(), &mut |operation| { + operation.container(None, layout.bounds()); + operation.traverse(&mut |operation| { self.content.as_widget().operate( &mut tree.children[0], layout.children().next().unwrap(), diff --git a/widget/src/column.rs b/widget/src/column.rs index 6c126048..51cb8bda 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -234,7 +234,8 @@ where renderer: &Renderer, operation: &mut dyn Operation, ) { - operation.container(None, layout.bounds(), &mut |operation| { + operation.container(None, layout.bounds()); + operation.traverse(&mut |operation| { self.children .iter() .zip(&mut tree.children) diff --git a/widget/src/container.rs b/widget/src/container.rs index af789fec..7b73831e 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -31,10 +31,10 @@ use crate::core::widget::tree::{self, Tree}; use crate::core::widget::{self, Operation}; use crate::core::{ self, Background, Clipboard, Color, Element, Event, Layout, Length, - Padding, Pixels, Point, Rectangle, Shadow, Shell, Size, Theme, Vector, - Widget, color, + Padding, Pixels, Rectangle, Shadow, Shell, Size, Theme, Vector, Widget, + color, }; -use crate::runtime::task::{self, Task}; +use crate::runtime::Task; /// A widget that aligns its contents inside of its boundaries. /// @@ -284,18 +284,15 @@ where renderer: &Renderer, operation: &mut dyn Operation, ) { - operation.container( - self.id.as_ref().map(|id| &id.0), - layout.bounds(), - &mut |operation| { - self.content.as_widget().operate( - tree, - layout.children().next().unwrap(), - renderer, - operation, - ); - }, - ); + operation.container(self.id.as_ref().map(|id| &id.0), layout.bounds()); + operation.traverse(&mut |operation| { + self.content.as_widget().operate( + tree, + layout.children().next().unwrap(), + renderer, + operation, + ); + }); } fn update( @@ -492,94 +489,8 @@ impl From<&'static str> for Id { /// Produces a [`Task`] that queries the visible screen bounds of the /// [`Container`] with the given [`Id`]. -pub fn visible_bounds(id: impl Into) -> Task> { - let id = id.into(); - - struct VisibleBounds { - target: widget::Id, - depth: usize, - scrollables: Vec<(Vector, Rectangle, usize)>, - bounds: Option, - } - - impl Operation> for VisibleBounds { - fn scrollable( - &mut self, - _id: Option<&widget::Id>, - bounds: Rectangle, - _content_bounds: Rectangle, - translation: Vector, - _state: &mut dyn widget::operation::Scrollable, - ) { - match self.scrollables.last() { - Some((last_translation, last_viewport, _depth)) => { - let viewport = last_viewport - .intersection(&(bounds - *last_translation)) - .unwrap_or(Rectangle::new(Point::ORIGIN, Size::ZERO)); - - self.scrollables.push(( - translation + *last_translation, - viewport, - self.depth, - )); - } - None => { - self.scrollables.push((translation, bounds, self.depth)); - } - } - } - - fn container( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - operate_on_children: &mut dyn FnMut( - &mut dyn Operation>, - ), - ) { - if self.bounds.is_some() { - return; - } - - if id == Some(&self.target) { - match self.scrollables.last() { - Some((translation, viewport, _)) => { - self.bounds = - viewport.intersection(&(bounds - *translation)); - } - None => { - self.bounds = Some(bounds); - } - } - - return; - } - - self.depth += 1; - - operate_on_children(self); - - self.depth -= 1; - - match self.scrollables.last() { - Some((_, _, depth)) if self.depth == *depth => { - let _ = self.scrollables.pop(); - } - _ => {} - } - } - - fn finish(&self) -> widget::operation::Outcome> { - widget::operation::Outcome::Some(self.bounds) - } - } - - task::widget(VisibleBounds { - target: id.into(), - depth: 0, - scrollables: Vec::new(), - bounds: None, - }) +pub fn visible_bounds(_id: impl Into) -> Task> { + todo!() } /// The appearance of a container. diff --git a/widget/src/grid.rs b/widget/src/grid.rs index 4a08dc55..15ffa738 100644 --- a/widget/src/grid.rs +++ b/widget/src/grid.rs @@ -257,7 +257,8 @@ where renderer: &Renderer, operation: &mut dyn Operation, ) { - operation.container(None, layout.bounds(), &mut |operation| { + operation.container(None, layout.bounds()); + operation.traverse(&mut |operation| { self.children .iter() .zip(&mut tree.children) diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs index a774c239..40605e14 100644 --- a/widget/src/keyed/column.rs +++ b/widget/src/keyed/column.rs @@ -284,7 +284,8 @@ where renderer: &Renderer, operation: &mut dyn Operation, ) { - operation.container(None, layout.bounds(), &mut |operation| { + operation.container(None, layout.bounds()); + operation.traverse(&mut |operation| { self.children .iter() .zip(&mut tree.children) diff --git a/widget/src/lib.rs b/widget/src/lib.rs index d08a92f9..fc64d938 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -8,6 +8,8 @@ pub use iced_renderer::graphics; pub use iced_runtime as runtime; pub use iced_runtime::core; +pub use core::widget::Id; + mod action; mod column; mod mouse_area; diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 158c265b..e1cbe6c2 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -468,7 +468,8 @@ where renderer: &Renderer, operation: &mut dyn widget::Operation, ) { - operation.container(None, layout.bounds(), &mut |operation| { + operation.container(None, layout.bounds()); + operation.traverse(&mut |operation| { self.panes .iter() .copied() diff --git a/widget/src/row.rs b/widget/src/row.rs index 101e51d8..9baa3bbd 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -234,7 +234,8 @@ where renderer: &Renderer, operation: &mut dyn Operation, ) { - operation.container(None, layout.bounds(), &mut |operation| { + operation.container(None, layout.bounds()); + operation.traverse(&mut |operation| { self.children .iter() .zip(&mut tree.children) diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index baff578e..0b682a78 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -549,18 +549,14 @@ where state, ); - operation.container( - self.id.as_ref().map(|id| &id.0), - bounds, - &mut |operation| { - self.content.as_widget().operate( - &mut tree.children[0], - layout.children().next().unwrap(), - renderer, - operation, - ); - }, - ); + operation.traverse(&mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + operation, + ); + }); } fn update( diff --git a/widget/src/stack.rs b/widget/src/stack.rs index ee81e4de..28290fc2 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -188,7 +188,8 @@ where renderer: &Renderer, operation: &mut dyn Operation, ) { - operation.container(None, layout.bounds(), &mut |operation| { + operation.container(None, layout.bounds()); + operation.traverse(&mut |operation| { self.children .iter() .zip(&mut tree.children)