diff --git a/core/src/widget/operation/text_input.rs b/core/src/widget/operation/text_input.rs index efb2a4d3..6bcae385 100644 --- a/core/src/widget/operation/text_input.rs +++ b/core/src/widget/operation/text_input.rs @@ -5,12 +5,20 @@ use crate::widget::operation::Operation; /// The internal state of a widget that has text input. pub trait TextInput { + /// Returns the current _visible_ text of the text input + /// + /// Normally, this is either its value or its placeholder. + fn text(&self) -> &str; + /// Moves the cursor of the text input to the front of the input text. fn move_cursor_to_front(&mut self); + /// Moves the cursor of the text input to the end of the input text. fn move_cursor_to_end(&mut self); + /// Moves the cursor of the text input to an arbitrary location. fn move_cursor_to(&mut self, position: usize); + /// Selects all the content of the text input. fn select_all(&mut self); } diff --git a/examples/todos/Cargo.toml b/examples/todos/Cargo.toml index 9869a2c5..3dca5c49 100644 --- a/examples/todos/Cargo.toml +++ b/examples/todos/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" publish = false [features] +default = ["tester"] test = ["iced/test"] tester = ["test", "iced/tester"] diff --git a/examples/todos/tests/carl_sagan.ice b/examples/todos/tests/carl_sagan.ice index 860f5d9e..7c94ff3a 100644 --- a/examples/todos/tests/carl_sagan.ice +++ b/examples/todos/tests/carl_sagan.ice @@ -2,13 +2,11 @@ viewport: 512x768 mode: impatient preset: Empty ----- -click left at (377.80, 236.50) +click left at "What needs to be done?" type "Create the universe" type enter type "Make an apple pie" type enter -click left at (135.40, 351.70) -click left at (153.80, 398.10) -move cursor to (511.40, 448.50) -expect text "Create the universe" -expect text "Make an apple pie" +click left at "Create the universe" +click left at "Make an apple pie" +expect "0 tasks left" diff --git a/test/src/emulator.rs b/test/src/emulator.rs index 89e0bd87..710e5b1b 100644 --- a/test/src/emulator.rs +++ b/test/src/emulator.rs @@ -16,6 +16,7 @@ use crate::runtime::task; use crate::runtime::user_interface; use crate::runtime::window; use crate::runtime::{Action, Task, UserInterface}; +use crate::selector; use std::fmt; @@ -212,7 +213,34 @@ impl Emulator

{ match instruction { Instruction::Interact(interaction) => { - let events = interaction.events(); + let Some(events) = interaction.events(|target| match target { + instruction::Target::Point(position) => Some(*position), + instruction::Target::Text(text) => { + use widget::Operation; + + let mut operation = + selector::text(text.to_owned()).operation(); + + user_interface.operate( + &self.renderer, + &mut widget::operation::black_box(&mut operation), + ); + + match operation.finish() { + widget::operation::Outcome::Some(matches) => { + matches + .first() + .copied() + .map(|target| target.bounds.center()) + } + _ => None, + } + } + }) else { + self.runtime.send(Event::Failed); + self.cache = Some(user_interface.into_cache()); + return; + }; for event in &events { if let core::Event::Mouse(mouse::Event::CursorMoved { @@ -242,10 +270,10 @@ impl Emulator

{ self.resubscribe(program); } Instruction::Expect(expectation) => match expectation { - instruction::Expectation::Presence(selector) => { + instruction::Expectation::Text(text) => { use widget::Operation; - let mut operation = selector.operation(); + let mut operation = selector::text(text).operation(); user_interface.operate( &self.renderer, @@ -253,7 +281,9 @@ impl Emulator

{ ); match operation.finish() { - widget::operation::Outcome::Some(Some(_)) => { + widget::operation::Outcome::Some(matches) + if matches.len() == 1 => + { self.runtime.send(Event::Ready); } _ => { diff --git a/test/src/instruction.rs b/test/src/instruction.rs index ba987072..539c61c5 100644 --- a/test/src/instruction.rs +++ b/test/src/instruction.rs @@ -1,8 +1,6 @@ -use crate::Selector; use crate::core::keyboard; use crate::core::mouse; use crate::core::{Event, Point}; -use crate::selector; use crate::simulator; use std::fmt; @@ -38,7 +36,9 @@ impl Interaction { pub fn from_event(event: Event) -> Option { Some(match event { Event::Mouse(mouse) => Self::Mouse(match mouse { - mouse::Event::CursorMoved { position } => Mouse::Move(position), + mouse::Event::CursorMoved { position } => { + Mouse::Move(Target::Point(position)) + } mouse::Event::ButtonPressed(button) => { Mouse::Press { button, at: None } } @@ -115,8 +115,8 @@ impl Interaction { at: release_at, }, ) if press == release - && release_at.is_none_or(|release_at| { - Some(release_at) == press_at + && release_at.as_ref().is_none_or(|release_at| { + Some(release_at) == press_at.as_ref() }) => { ( @@ -127,22 +127,6 @@ impl Interaction { None, ) } - ( - current @ Mouse::Release { - button: button_a, - at: at_a, - } - | current @ Mouse::Click { - button: button_a, - at: at_a, - }, - Mouse::Release { - button: button_b, - at: at_b, - }, - ) if button_a == button_b && at_a == at_b => { - (Self::Mouse(current), None) - } (current, next) => { (Self::Mouse(current), Some(Self::Mouse(next))) } @@ -173,7 +157,10 @@ impl Interaction { } } - pub fn events(&self) -> Vec { + pub fn events( + &self, + find_target: impl FnOnce(&Target) -> Option, + ) -> Option> { let mouse_move_ = |to| Event::Mouse(mouse::Event::CursorMoved { position: to }); @@ -187,20 +174,22 @@ impl Interaction { let key_release = |key| simulator::release_key(key); - match self { + Some(match self { Interaction::Mouse(mouse) => match mouse { - Mouse::Move(to) => vec![mouse_move_(*to)], + Mouse::Move(to) => vec![mouse_move_(find_target(to)?)], Mouse::Press { button, at: Some(at), - } => vec![mouse_move_(*at), mouse_press(*button)], + } => vec![mouse_move_(find_target(at)?), mouse_press(*button)], Mouse::Press { button, at: None } => { vec![mouse_press(*button)] } Mouse::Release { button, at: Some(at), - } => vec![mouse_move_(*at), mouse_release(*button)], + } => { + vec![mouse_move_(find_target(at)?), mouse_release(*button)] + } Mouse::Release { button, at: None } => { vec![mouse_release(*button)] } @@ -209,7 +198,7 @@ impl Interaction { at: Some(at), } => { vec![ - mouse_move_(*at), + mouse_move_(find_target(at)?), mouse_press(*button), mouse_release(*button), ] @@ -226,7 +215,7 @@ impl Interaction { simulator::typewrite(text).collect() } }, - } + }) } } @@ -241,40 +230,55 @@ impl fmt::Display for Interaction { #[derive(Debug, Clone, PartialEq)] pub enum Mouse { - Move(Point), + Move(Target), Press { button: mouse::Button, - at: Option, + at: Option, }, Release { button: mouse::Button, - at: Option, + at: Option, }, Click { button: mouse::Button, - at: Option, + at: Option, }, } impl fmt::Display for Mouse { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Mouse::Move(point) => { - write!(f, "move cursor to ({:.2}, {:.2})", point.x, point.y) + Mouse::Move(target) => { + write!(f, "move cursor to {}", target) } Mouse::Press { button, at } => { - write!(f, "press {}", format::button_at(*button, *at)) + write!(f, "press {}", format::button_at(*button, at.as_ref())) } Mouse::Release { button, at } => { - write!(f, "release {}", format::button_at(*button, *at)) + write!(f, "release {}", format::button_at(*button, at.as_ref())) } Mouse::Click { button, at } => { - write!(f, "click {}", format::button_at(*button, *at)) + write!(f, "click {}", format::button_at(*button, at.as_ref())) } } } } +#[derive(Debug, Clone, PartialEq)] +pub enum Target { + Point(Point), + Text(String), +} + +impl fmt::Display for Target { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Point(point) => f.write_str(&format::point(*point)), + Self::Text(text) => f.write_str(&format::string(text)), + } + } +} + #[derive(Debug, Clone, PartialEq)] pub enum Keyboard { Press(Key), @@ -324,9 +328,9 @@ impl From for keyboard::Key { mod format { use super::*; - pub fn button_at(button: mouse::Button, at: Option) -> String { + pub fn button_at(button: mouse::Button, at: Option<&Target>) -> String { if let Some(at) = at { - format!("{} at {}", self::button(button), point(at)) + format!("{} at {}", self::button(button), at) } else { self::button(button).to_owned() } @@ -355,21 +359,22 @@ mod format { Key::Backspace => "backspace", } } + + pub fn string(text: &str) -> String { + format!("\"{}\"", text.escape_default()) + } } #[derive(Debug, Clone, PartialEq)] pub enum Expectation { - Presence(Selector), + Text(String), } impl fmt::Display for Expectation { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Expectation::Presence(Selector::Id(_id)) => { - write!(f, "expect id") // TODO - } - Expectation::Presence(Selector::Text(text)) => { - write!(f, "expect text \"{text}\"") + Expectation::Text(text) => { + write!(f, "expect {}", format::string(text)) } } } @@ -383,7 +388,7 @@ mod parser { use nom::branch::alt; use nom::bytes::complete::tag; use nom::character::complete::{char, multispace0, satisfy}; - use nom::combinator::{map, opt, recognize}; + use nom::combinator::{cut, map, opt, recognize}; use nom::multi::many0; use nom::number::float; use nom::sequence::{delimited, preceded, separated_pair}; @@ -418,7 +423,7 @@ mod parser { fn mouse(input: &str) -> IResult<&str, Mouse> { let mouse_move = - preceded(tag("move cursor to "), point).map(Mouse::Move); + preceded(tag("move cursor to "), target).map(Mouse::Move); alt((mouse_move, mouse_click)).parse(input) } @@ -433,13 +438,21 @@ mod parser { fn mouse_button_at( input: &str, - ) -> IResult<&str, (mouse::Button, Option)> { + ) -> IResult<&str, (mouse::Button, Option)> { let (input, button) = mouse_button(input)?; - let (input, at) = opt(preceded(tag(" at "), point)).parse(input)?; + let (input, at) = opt(target).parse(input)?; Ok((input, (button, at))) } + fn target(input: &str) -> IResult<&str, Target> { + preceded( + tag(" at "), + cut(alt((string.map(Target::Text), point.map(Target::Point)))), + ) + .parse(input) + } + fn mouse_button(input: &str) -> IResult<&str, mouse::Button> { alt(( tag("left").map(|_| mouse::Button::Left), @@ -457,8 +470,8 @@ mod parser { } fn expectation(input: &str) -> IResult<&str, Expectation> { - map(preceded(tag("expect text "), string), |text| { - Expectation::Presence(selector::text(text)) + map(preceded(tag("expect "), string), |text| { + Expectation::Text(text) }) .parse(input) } @@ -474,6 +487,7 @@ mod parser { } fn string(input: &str) -> IResult<&str, String> { + // TODO: Proper string literal parsing delimited( char('"'), map(recognize(many0(satisfy(|c| c != '"'))), str::to_owned), diff --git a/test/src/selector.rs b/test/src/selector.rs index b6a5b294..fa6fce5a 100644 --- a/test/src/selector.rs +++ b/test/src/selector.rs @@ -13,7 +13,7 @@ pub enum Selector { } impl Selector { - pub fn operation<'a>(&self) -> impl widget::Operation> + 'a { + pub fn operation<'a>(&self) -> impl widget::Operation> + 'a { match self { Selector::Id(id) => { struct FindById { @@ -21,13 +21,13 @@ impl Selector { target: Option, } - impl widget::Operation> for FindById { + 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>, + &mut dyn widget::Operation>, ), ) { if self.target.is_some() { @@ -106,9 +106,13 @@ impl Selector { fn finish( &self, - ) -> widget::operation::Outcome> + ) -> widget::operation::Outcome> { - widget::operation::Outcome::Some(self.target) + if let Some(target) = self.target { + widget::operation::Outcome::Some(vec![target]) + } else { + widget::operation::Outcome::None + } } } @@ -120,51 +124,54 @@ impl Selector { Selector::Text(text) => { struct FindByText { text: text::Fragment<'static>, - target: Option, + target: Vec, } - impl widget::Operation> for FindByText { + 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>, + &mut dyn widget::Operation>, ), ) { - if self.target.is_some() { - return; - } - 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.target.is_some() { - return; - } - if self.text == text { - self.target = Some(Target { bounds }); + self.target.push(Target { bounds }); } } fn finish( &self, - ) -> widget::operation::Outcome> + ) -> widget::operation::Outcome> { - widget::operation::Outcome::Some(self.target) + widget::operation::Outcome::Some(self.target.clone()) } } Box::new(FindByText { text: text.clone(), - target: None, + target: Vec::new(), }) } } diff --git a/test/src/simulator.rs b/test/src/simulator.rs index c5505d5b..4e643e5b 100644 --- a/test/src/simulator.rs +++ b/test/src/simulator.rs @@ -115,7 +115,9 @@ where ); match operation.finish() { - widget::operation::Outcome::Some(Some(target)) => Ok(target), + widget::operation::Outcome::Some(matches) => { + matches.first().copied().ok_or(Error::NotFound(selector)) + } _ => Err(Error::NotFound(selector)), } } diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index fa3dd770..c7ba113c 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -1631,6 +1631,14 @@ impl operation::Focusable for State

{ } impl operation::TextInput for State

{ + fn text(&self) -> &str { + if self.value.content().is_empty() { + self.placeholder.content() + } else { + self.value.content() + } + } + fn move_cursor_to_front(&mut self) { State::move_cursor_to_front(self); }