diff options
| -rw-r--r-- | Cargo.toml | 3 | ||||
| -rw-r--r-- | core/src/keyboard/key.rs | 6 | ||||
| -rw-r--r-- | core/src/widget/operation.rs | 199 | ||||
| -rw-r--r-- | core/src/widget/operation/focusable.rs | 35 | ||||
| -rw-r--r-- | core/src/widget/operation/scrollable.rs | 6 | ||||
| -rw-r--r-- | core/src/widget/operation/text_input.rs | 28 | ||||
| -rw-r--r-- | core/src/widget/text.rs | 12 | ||||
| -rw-r--r-- | examples/todos/Cargo.toml | 3 | ||||
| -rw-r--r-- | examples/todos/src/main.rs | 33 | ||||
| -rw-r--r-- | src/lib.rs | 2 | ||||
| -rw-r--r-- | test/Cargo.toml | 21 | ||||
| -rw-r--r-- | test/src/lib.rs | 296 | ||||
| -rw-r--r-- | test/src/selector.rs | 24 | ||||
| -rw-r--r-- | widget/src/checkbox.rs | 10 | ||||
| -rw-r--r-- | widget/src/container.rs | 2 | ||||
| -rw-r--r-- | widget/src/scrollable.rs | 2 | ||||
| -rw-r--r-- | widget/src/text_editor.rs | 4 | ||||
| -rw-r--r-- | widget/src/text_input.rs | 15 | 
18 files changed, 639 insertions, 62 deletions
@@ -72,6 +72,7 @@ unconditional-rendering = ["iced_winit/unconditional-rendering"]  iced_core.workspace = true  iced_futures.workspace = true  iced_renderer.workspace = true +iced_test.workspace = true  iced_widget.workspace = true  iced_winit.features = ["program"]  iced_winit.workspace = true @@ -111,6 +112,7 @@ members = [      "highlighter",      "renderer",      "runtime", +    "test",      "tiny_skia",      "wgpu",      "widget", @@ -137,6 +139,7 @@ iced_graphics = { version = "0.14.0-dev", path = "graphics" }  iced_highlighter = { version = "0.14.0-dev", path = "highlighter" }  iced_renderer = { version = "0.14.0-dev", path = "renderer" }  iced_runtime = { version = "0.14.0-dev", path = "runtime" } +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" }  iced_widget = { version = "0.14.0-dev", path = "widget" } diff --git a/core/src/keyboard/key.rs b/core/src/keyboard/key.rs index 69a91902..47169d9a 100644 --- a/core/src/keyboard/key.rs +++ b/core/src/keyboard/key.rs @@ -32,6 +32,12 @@ impl Key {      }  } +impl From<Named> for Key { +    fn from(named: Named) -> Self { +        Self::Named(named) +    } +} +  /// A named key.  ///  /// This is mostly the `NamedKey` type found in [`winit`]. diff --git a/core/src/widget/operation.rs b/core/src/widget/operation.rs index 6bdb27f6..8fc627bf 100644 --- a/core/src/widget/operation.rs +++ b/core/src/widget/operation.rs @@ -30,24 +30,45 @@ pub trait Operation<T = ()>: Send {      );      /// Operates on a widget that can be focused. -    fn focusable(&mut self, _state: &mut dyn Focusable, _id: Option<&Id>) {} +    fn focusable( +        &mut self, +        _id: Option<&Id>, +        _bounds: Rectangle, +        _state: &mut dyn Focusable, +    ) { +    }      /// Operates on a widget that can be scrolled.      fn scrollable(          &mut self, -        _state: &mut dyn Scrollable,          _id: Option<&Id>,          _bounds: Rectangle,          _content_bounds: Rectangle,          _translation: Vector, +        _state: &mut dyn Scrollable,      ) {      }      /// Operates on a widget that has text input. -    fn text_input(&mut self, _state: &mut dyn TextInput, _id: Option<&Id>) {} +    fn text_input( +        &mut self, +        _id: Option<&Id>, +        _bounds: Rectangle, +        _state: &mut dyn TextInput, +    ) { +    } + +    /// Operates on a widget that contains some text. +    fn text(&mut self, _id: Option<&Id>, _bounds: Rectangle, _text: &str) {}      /// Operates on a custom widget with some state. -    fn custom(&mut self, _state: &mut dyn Any, _id: Option<&Id>) {} +    fn custom( +        &mut self, +        _id: Option<&Id>, +        _bounds: Rectangle, +        _state: &mut dyn Any, +    ) { +    }      /// Finishes the [`Operation`] and returns its [`Outcome`].      fn finish(&self) -> Outcome<T> { @@ -68,33 +89,52 @@ where          self.as_mut().container(id, bounds, operate_on_children);      } -    fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) { -        self.as_mut().focusable(state, id); +    fn focusable( +        &mut self, +        id: Option<&Id>, +        bounds: Rectangle, +        state: &mut dyn Focusable, +    ) { +        self.as_mut().focusable(id, bounds, state);      }      fn scrollable(          &mut self, -        state: &mut dyn Scrollable,          id: Option<&Id>,          bounds: Rectangle,          content_bounds: Rectangle,          translation: Vector, +        state: &mut dyn Scrollable,      ) {          self.as_mut().scrollable( -            state,              id,              bounds,              content_bounds,              translation, +            state,          );      } -    fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { -        self.as_mut().text_input(state, id); +    fn text_input( +        &mut self, +        id: Option<&Id>, +        bounds: Rectangle, +        state: &mut dyn TextInput, +    ) { +        self.as_mut().text_input(id, bounds, state); +    } + +    fn text(&mut self, id: Option<&Id>, bounds: Rectangle, text: &str) { +        self.as_mut().text(id, bounds, text);      } -    fn custom(&mut self, state: &mut dyn Any, id: Option<&Id>) { -        self.as_mut().custom(state, id); +    fn custom( +        &mut self, +        id: Option<&Id>, +        bounds: Rectangle, +        state: &mut dyn Any, +    ) { +        self.as_mut().custom(id, bounds, state);      }      fn finish(&self) -> Outcome<O> { @@ -150,33 +190,52 @@ where              });          } -        fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) { -            self.operation.focusable(state, id); +        fn focusable( +            &mut self, +            id: Option<&Id>, +            bounds: Rectangle, +            state: &mut dyn Focusable, +        ) { +            self.operation.focusable(id, bounds, state);          }          fn scrollable(              &mut self, -            state: &mut dyn Scrollable,              id: Option<&Id>,              bounds: Rectangle,              content_bounds: Rectangle,              translation: Vector, +            state: &mut dyn Scrollable,          ) {              self.operation.scrollable( -                state,                  id,                  bounds,                  content_bounds,                  translation, +                state,              );          } -        fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { -            self.operation.text_input(state, id); +        fn text_input( +            &mut self, +            id: Option<&Id>, +            bounds: Rectangle, +            state: &mut dyn TextInput, +        ) { +            self.operation.text_input(id, bounds, state);          } -        fn custom(&mut self, state: &mut dyn Any, id: Option<&Id>) { -            self.operation.custom(state, id); +        fn text(&mut self, id: Option<&Id>, bounds: Rectangle, text: &str) { +            self.operation.text(id, bounds, text); +        } + +        fn custom( +            &mut self, +            id: Option<&Id>, +            bounds: Rectangle, +            state: &mut dyn Any, +        ) { +            self.operation.custom(id, bounds, state);          }          fn finish(&self) -> Outcome<O> { @@ -234,39 +293,55 @@ where                  fn scrollable(                      &mut self, -                    state: &mut dyn Scrollable,                      id: Option<&Id>,                      bounds: Rectangle,                      content_bounds: Rectangle,                      translation: Vector, +                    state: &mut dyn Scrollable,                  ) {                      self.operation.scrollable( -                        state,                          id,                          bounds,                          content_bounds,                          translation, +                        state,                      );                  }                  fn focusable(                      &mut self, -                    state: &mut dyn Focusable,                      id: Option<&Id>, +                    bounds: Rectangle, +                    state: &mut dyn Focusable,                  ) { -                    self.operation.focusable(state, id); +                    self.operation.focusable(id, bounds, state);                  }                  fn text_input(                      &mut self, +                    id: Option<&Id>, +                    bounds: Rectangle,                      state: &mut dyn TextInput, +                ) { +                    self.operation.text_input(id, bounds, state); +                } + +                fn text( +                    &mut self,                      id: Option<&Id>, +                    bounds: Rectangle, +                    text: &str,                  ) { -                    self.operation.text_input(state, id); +                    self.operation.text(id, bounds, text);                  } -                fn custom(&mut self, state: &mut dyn Any, id: Option<&Id>) { -                    self.operation.custom(state, id); +                fn custom( +                    &mut self, +                    id: Option<&Id>, +                    bounds: Rectangle, +                    state: &mut dyn Any, +                ) { +                    self.operation.custom(id, bounds, state);                  }              } @@ -275,33 +350,52 @@ where              MapRef { operation }.container(id, bounds, operate_on_children);          } -        fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) { -            self.operation.focusable(state, id); +        fn focusable( +            &mut self, +            id: Option<&Id>, +            bounds: Rectangle, +            state: &mut dyn Focusable, +        ) { +            self.operation.focusable(id, bounds, state);          }          fn scrollable(              &mut self, -            state: &mut dyn Scrollable,              id: Option<&Id>,              bounds: Rectangle,              content_bounds: Rectangle,              translation: Vector, +            state: &mut dyn Scrollable,          ) {              self.operation.scrollable( -                state,                  id,                  bounds,                  content_bounds,                  translation, +                state,              );          } -        fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { -            self.operation.text_input(state, id); +        fn text_input( +            &mut self, +            id: Option<&Id>, +            bounds: Rectangle, +            state: &mut dyn TextInput, +        ) { +            self.operation.text_input(id, bounds, state); +        } + +        fn text(&mut self, id: Option<&Id>, bounds: Rectangle, text: &str) { +            self.operation.text(id, bounds, text);          } -        fn custom(&mut self, state: &mut dyn Any, id: Option<&Id>) { -            self.operation.custom(state, id); +        fn custom( +            &mut self, +            id: Option<&Id>, +            bounds: Rectangle, +            state: &mut dyn Any, +        ) { +            self.operation.custom(id, bounds, state);          }          fn finish(&self) -> Outcome<B> { @@ -361,33 +455,52 @@ where              });          } -        fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) { -            self.operation.focusable(state, id); +        fn focusable( +            &mut self, +            id: Option<&Id>, +            bounds: Rectangle, +            state: &mut dyn Focusable, +        ) { +            self.operation.focusable(id, bounds, state);          }          fn scrollable(              &mut self, -            state: &mut dyn Scrollable,              id: Option<&Id>,              bounds: Rectangle,              content_bounds: Rectangle,              translation: crate::Vector, +            state: &mut dyn Scrollable,          ) {              self.operation.scrollable( -                state,                  id,                  bounds,                  content_bounds,                  translation, +                state,              );          } -        fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { -            self.operation.text_input(state, id); +        fn text_input( +            &mut self, +            id: Option<&Id>, +            bounds: Rectangle, +            state: &mut dyn TextInput, +        ) { +            self.operation.text_input(id, bounds, state);          } -        fn custom(&mut self, state: &mut dyn std::any::Any, id: Option<&Id>) { -            self.operation.custom(state, id); +        fn text(&mut self, id: Option<&Id>, bounds: Rectangle, text: &str) { +            self.operation.text(id, bounds, text); +        } + +        fn custom( +            &mut self, +            id: Option<&Id>, +            bounds: Rectangle, +            state: &mut dyn Any, +        ) { +            self.operation.custom(id, bounds, state);          }          fn finish(&self) -> Outcome<B> { diff --git a/core/src/widget/operation/focusable.rs b/core/src/widget/operation/focusable.rs index 867c682e..8f66e575 100644 --- a/core/src/widget/operation/focusable.rs +++ b/core/src/widget/operation/focusable.rs @@ -32,7 +32,12 @@ pub fn focus<T>(target: Id) -> impl Operation<T> {      }      impl<T> Operation<T> for Focus { -        fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) { +        fn focusable( +            &mut self, +            id: Option<&Id>, +            _bounds: Rectangle, +            state: &mut dyn Focusable, +        ) {              match id {                  Some(id) if id == &self.target => {                      state.focus(); @@ -64,7 +69,12 @@ pub fn count() -> impl Operation<Count> {      }      impl Operation<Count> for CountFocusable { -        fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) { +        fn focusable( +            &mut self, +            _id: Option<&Id>, +            _bounds: Rectangle, +            state: &mut dyn Focusable, +        ) {              if state.is_focused() {                  self.count.focused = Some(self.count.total);              } @@ -104,7 +114,12 @@ where      }      impl<T> Operation<T> for FocusPrevious { -        fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) { +        fn focusable( +            &mut self, +            _id: Option<&Id>, +            _bounds: Rectangle, +            state: &mut dyn Focusable, +        ) {              if self.count.total == 0 {                  return;              } @@ -147,7 +162,12 @@ where      }      impl<T> Operation<T> for FocusNext { -        fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) { +        fn focusable( +            &mut self, +            _id: Option<&Id>, +            _bounds: Rectangle, +            state: &mut dyn Focusable, +        ) {              match self.count.focused {                  None if self.current == 0 => state.focus(),                  Some(focused) if focused == self.current => state.unfocus(), @@ -179,7 +199,12 @@ pub fn find_focused() -> impl Operation<Id> {      }      impl Operation<Id> for FindFocused { -        fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) { +        fn focusable( +            &mut self, +            id: Option<&Id>, +            _bounds: Rectangle, +            state: &mut dyn Focusable, +        ) {              if state.is_focused() && id.is_some() {                  self.focused = id.cloned();              } diff --git a/core/src/widget/operation/scrollable.rs b/core/src/widget/operation/scrollable.rs index c2fecf56..7c78c087 100644 --- a/core/src/widget/operation/scrollable.rs +++ b/core/src/widget/operation/scrollable.rs @@ -39,11 +39,11 @@ pub fn snap_to<T>(target: Id, offset: RelativeOffset) -> impl Operation<T> {          fn scrollable(              &mut self, -            state: &mut dyn Scrollable,              id: Option<&Id>,              _bounds: Rectangle,              _content_bounds: Rectangle,              _translation: Vector, +            state: &mut dyn Scrollable,          ) {              if Some(&self.target) == id {                  state.snap_to(self.offset); @@ -74,11 +74,11 @@ pub fn scroll_to<T>(target: Id, offset: AbsoluteOffset) -> impl Operation<T> {          fn scrollable(              &mut self, -            state: &mut dyn Scrollable,              id: Option<&Id>,              _bounds: Rectangle,              _content_bounds: Rectangle,              _translation: Vector, +            state: &mut dyn Scrollable,          ) {              if Some(&self.target) == id {                  state.scroll_to(self.offset); @@ -109,11 +109,11 @@ pub fn scroll_by<T>(target: Id, offset: AbsoluteOffset) -> impl Operation<T> {          fn scrollable(              &mut self, -            state: &mut dyn Scrollable,              id: Option<&Id>,              bounds: Rectangle,              content_bounds: Rectangle,              _translation: Vector, +            state: &mut dyn Scrollable,          ) {              if Some(&self.target) == id {                  state.scroll_by(self.offset, bounds, content_bounds); diff --git a/core/src/widget/operation/text_input.rs b/core/src/widget/operation/text_input.rs index 41731d4c..a46f1a2d 100644 --- a/core/src/widget/operation/text_input.rs +++ b/core/src/widget/operation/text_input.rs @@ -23,7 +23,12 @@ pub fn move_cursor_to_front<T>(target: Id) -> impl Operation<T> {      }      impl<T> Operation<T> for MoveCursor { -        fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { +        fn text_input( +            &mut self, +            id: Option<&Id>, +            _bounds: Rectangle, +            state: &mut dyn TextInput, +        ) {              match id {                  Some(id) if id == &self.target => {                      state.move_cursor_to_front(); @@ -53,7 +58,12 @@ pub fn move_cursor_to_end<T>(target: Id) -> impl Operation<T> {      }      impl<T> Operation<T> for MoveCursor { -        fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { +        fn text_input( +            &mut self, +            id: Option<&Id>, +            _bounds: Rectangle, +            state: &mut dyn TextInput, +        ) {              match id {                  Some(id) if id == &self.target => {                      state.move_cursor_to_end(); @@ -84,7 +94,12 @@ pub fn move_cursor_to<T>(target: Id, position: usize) -> impl Operation<T> {      }      impl<T> Operation<T> for MoveCursor { -        fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { +        fn text_input( +            &mut self, +            id: Option<&Id>, +            _bounds: Rectangle, +            state: &mut dyn TextInput, +        ) {              match id {                  Some(id) if id == &self.target => {                      state.move_cursor_to(self.position); @@ -113,7 +128,12 @@ pub fn select_all<T>(target: Id) -> impl Operation<T> {      }      impl<T> Operation<T> for MoveCursor { -        fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { +        fn text_input( +            &mut self, +            id: Option<&Id>, +            _bounds: Rectangle, +            state: &mut dyn TextInput, +        ) {              match id {                  Some(id) if id == &self.target => {                      state.select_all(); diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index d3d1cffd..5dd7892f 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -267,6 +267,18 @@ where          draw(renderer, defaults, layout, state.0.raw(), style, viewport);      } + +    fn operate( +        &self, +        _state: &mut Tree, +        layout: Layout<'_>, +        _renderer: &Renderer, +        operation: &mut dyn super::Operation, +    ) { +        dbg!(&self.fragment); + +        operation.text(None, layout.bounds(), &self.fragment); +    }  }  /// Produces the [`layout::Node`] of a [`Text`] widget. diff --git a/examples/todos/Cargo.toml b/examples/todos/Cargo.toml index 0d72be86..65c34a8f 100644 --- a/examples/todos/Cargo.toml +++ b/examples/todos/Cargo.toml @@ -26,6 +26,9 @@ uuid = { version = "1.0", features = ["js"] }  web-sys = { workspace = true, features = ["Window", "Storage"] }  wasm-timer.workspace = true +[dev-dependencies] +iced_test.workspace = true +  [package.metadata.deb]  assets = [      ["target/release-opt/todos", "usr/bin/iced-todos", "755"], diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 25e3ead2..8772bb80 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -584,3 +584,36 @@ impl SavedState {          Ok(())      }  } + +#[cfg(test)] +mod tests { +    use super::*; + +    use iced::test; +    use iced::test::selector; + +    #[test] +    fn it_creates_a_new_task() { +        let (mut todos, _command) = Todos::new(); +        let _command = todos.update(Message::Loaded(Err(LoadError::File))); + +        let mut interface = test::interface(todos.view()); + +        let _input = interface +            .click("new-task") +            .expect("new-task input must be present"); + +        interface.typewrite("Create the universe"); +        interface.press_key(keyboard::key::Named::Enter); + +        for message in interface.into_messages() { +            let _command = todos.update(message); +        } + +        let mut interface = test::interface(todos.view()); + +        let _ = interface +            .find(selector::text("Create the universe")) +            .expect("New task must be present"); +    } +} @@ -479,6 +479,7 @@ use iced_winit::runtime;  pub use iced_futures::futures;  pub use iced_futures::stream; +pub use iced_test as test;  #[cfg(feature = "highlighter")]  pub use iced_highlighter as highlighter; @@ -624,6 +625,7 @@ pub use error::Error;  pub use event::Event;  pub use executor::Executor;  pub use font::Font; +pub use program::Program;  pub use renderer::Renderer;  pub use settings::Settings;  pub use task::Task; diff --git a/test/Cargo.toml b/test/Cargo.toml new file mode 100644 index 00000000..c09a196d --- /dev/null +++ b/test/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "iced_test" +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 + +[lints] +workspace = true + +[dependencies] +iced_runtime.workspace = true +iced_tiny_skia.workspace = true + +iced_renderer.workspace = true +iced_renderer.features = ["tiny-skia"] diff --git a/test/src/lib.rs b/test/src/lib.rs new file mode 100644 index 00000000..211ed4a2 --- /dev/null +++ b/test/src/lib.rs @@ -0,0 +1,296 @@ +//! Test your `iced` applications in headless mode. +#![allow(missing_docs, missing_debug_implementations)] +pub mod selector; + +pub use selector::Selector; + +use iced_renderer as renderer; +use iced_runtime as runtime; +use iced_runtime::core; +use iced_tiny_skia as tiny_skia; + +use crate::core::clipboard; +use crate::core::keyboard; +use crate::core::mouse; +use crate::core::widget; +use crate::core::{Element, Event, Font, Pixels, Rectangle, Size, SmolStr}; +use crate::renderer::Renderer; +use crate::runtime::user_interface; +use crate::runtime::UserInterface; + +pub fn interface<'a, Message, Theme>( +    element: impl Into<Element<'a, Message, Theme, Renderer>>, +) -> Interface<'a, Message, Theme, Renderer> { +    let mut renderer = Renderer::Secondary(tiny_skia::Renderer::new( +        Font::default(), +        Pixels(16.0), +    )); + +    let raw = UserInterface::build( +        element, +        Size::new(1024.0, 1024.0), +        user_interface::Cache::default(), +        &mut renderer, +    ); + +    Interface { +        raw, +        renderer, +        messages: Vec::new(), +    } +} + +pub struct Interface<'a, Message, Theme, Renderer> { +    raw: UserInterface<'a, Message, Theme, Renderer>, +    renderer: Renderer, +    messages: Vec<Message>, +} + +pub struct Target { +    bounds: Rectangle, +} + +impl<Message, Theme, Renderer> Interface<'_, Message, Theme, Renderer> +where +    Renderer: core::Renderer, +{ +    pub fn find( +        &mut self, +        selector: impl Into<Selector>, +    ) -> Result<Target, Error> { +        let selector = selector.into(); + +        match &selector { +            Selector::Id(id) => { +                struct FindById<'a> { +                    id: &'a widget::Id, +                    target: Option<Target>, +                } + +                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: core::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 }); +                        } +                    } +                } + +                let mut find = FindById { id, target: None }; + +                self.raw.operate(&self.renderer, &mut find); + +                find.target.ok_or(Error::NotFound(selector)) +            } +            Selector::Text(text) => { +                struct FindByText<'a> { +                    text: &'a str, +                    target: Option<Target>, +                } + +                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<()>, +                        ), +                    ) { +                        if self.target.is_some() { +                            return; +                        } + +                        operate_on_children(self); +                    } + +                    fn text( +                        &mut self, +                        _id: Option<&widget::Id>, +                        bounds: Rectangle, +                        text: &str, +                    ) { +                        if self.target.is_some() { +                            return; +                        } + +                        if self.text == text { +                            self.target = Some(Target { bounds }); +                        } +                    } +                } + +                let mut find = FindByText { text, target: None }; + +                self.raw.operate(&self.renderer, &mut find); + +                find.target.ok_or(Error::NotFound(selector)) +            } +        } +    } + +    pub fn click( +        &mut self, +        selector: impl Into<Selector>, +    ) -> Result<Target, Error> { +        let target = self.find(selector)?; + +        let _ = self.raw.update( +            &[ +                Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)), +                Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)), +            ], +            mouse::Cursor::Available(target.bounds.center()), +            &mut self.renderer, +            &mut clipboard::Null, +            &mut self.messages, +        ); + +        Ok(target) +    } + +    pub fn typewrite(&mut self, text: impl AsRef<str>) { +        let events: Vec<_> = text +            .as_ref() +            .chars() +            .map(|c| SmolStr::new_inline(&c.to_string())) +            .flat_map(|c| { +                key_press_and_release( +                    keyboard::Key::Character(c.clone()), +                    Some(c), +                ) +            }) +            .collect(); + +        let _ = self.raw.update( +            &events, +            mouse::Cursor::Unavailable, +            &mut self.renderer, +            &mut clipboard::Null, +            &mut self.messages, +        ); +    } + +    pub fn press_key(&mut self, key: impl Into<keyboard::Key>) { +        let _ = self.raw.update( +            &key_press_and_release(key, None), +            mouse::Cursor::Unavailable, +            &mut self.renderer, +            &mut clipboard::Null, +            &mut self.messages, +        ); +    } + +    pub fn into_messages(self) -> impl IntoIterator<Item = Message> { +        self.messages +    } +} + +fn key_press_and_release( +    key: impl Into<keyboard::Key>, +    text: Option<SmolStr>, +) -> [Event; 2] { +    let key = key.into(); + +    [ +        Event::Keyboard(keyboard::Event::KeyPressed { +            key: key.clone(), +            modified_key: key.clone(), +            physical_key: keyboard::key::Physical::Unidentified( +                keyboard::key::NativeCode::Unidentified, +            ), +            location: keyboard::Location::Standard, +            modifiers: keyboard::Modifiers::default(), +            text, +        }), +        Event::Keyboard(keyboard::Event::KeyReleased { +            key: key.clone(), +            modified_key: key, +            physical_key: keyboard::key::Physical::Unidentified( +                keyboard::key::NativeCode::Unidentified, +            ), +            location: keyboard::Location::Standard, +            modifiers: keyboard::Modifiers::default(), +        }), +    ] +} + +#[derive(Debug, Clone)] +pub enum Error { +    NotFound(Selector), +} diff --git a/test/src/selector.rs b/test/src/selector.rs new file mode 100644 index 00000000..54faa1a9 --- /dev/null +++ b/test/src/selector.rs @@ -0,0 +1,24 @@ +use crate::core::text; +use crate::core::widget; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Selector { +    Id(widget::Id), +    Text(text::Fragment<'static>), +} + +impl From<widget::Id> for Selector { +    fn from(id: widget::Id) -> Self { +        Self::Id(id) +    } +} + +impl From<&'static str> for Selector { +    fn from(id: &'static str) -> Self { +        Self::Id(widget::Id::new(id)) +    } +} + +pub fn text(fragment: impl text::IntoFragment<'static>) -> Selector { +    Selector::Text(fragment.into_fragment()) +} diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index 3686d34c..663bfad1 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -445,6 +445,16 @@ where              );          }      } + +    fn operate( +        &self, +        _state: &mut Tree, +        layout: Layout<'_>, +        _renderer: &Renderer, +        operation: &mut dyn widget::Operation, +    ) { +        operation.text(None, layout.bounds(), &self.label); +    }  }  impl<'a, Message, Theme, Renderer> From<Checkbox<'a, Message, Theme, Renderer>> diff --git a/widget/src/container.rs b/widget/src/container.rs index d9740f72..a411a7d2 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -493,11 +493,11 @@ pub fn visible_bounds(id: Id) -> Task<Option<Rectangle>> {      impl Operation<Option<Rectangle>> for VisibleBounds {          fn scrollable(              &mut self, -            _state: &mut dyn widget::operation::Scrollable,              _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)) => { diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 41bb15f9..4188f67d 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -487,11 +487,11 @@ where              state.translation(self.direction, bounds, content_bounds);          operation.scrollable( -            state,              self.id.as_ref().map(|id| &id.0),              bounds,              content_bounds,              translation, +            state,          );          operation.container( diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index ffd06b77..ad852ce9 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -971,13 +971,13 @@ where      fn operate(          &self,          tree: &mut widget::Tree, -        _layout: Layout<'_>, +        layout: Layout<'_>,          _renderer: &Renderer,          operation: &mut dyn widget::Operation,      ) {          let state = tree.state.downcast_mut::<State<Highlighter>>(); -        operation.focusable(state, None); +        operation.focusable(None, layout.bounds(), state);      }  } diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index c3f0b25a..57ebe46a 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -617,14 +617,23 @@ where      fn operate(          &self,          tree: &mut Tree, -        _layout: Layout<'_>, +        layout: Layout<'_>,          _renderer: &Renderer,          operation: &mut dyn Operation,      ) {          let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>(); -        operation.focusable(state, self.id.as_ref().map(|id| &id.0)); -        operation.text_input(state, self.id.as_ref().map(|id| &id.0)); +        operation.focusable( +            self.id.as_ref().map(|id| &id.0), +            layout.bounds(), +            state, +        ); + +        operation.text_input( +            self.id.as_ref().map(|id| &id.0), +            layout.bounds(), +            state, +        );      }      fn update(  | 
