Introduce new iced_selector subcrate and refactor Operation

This commit is contained in:
Héctor Ramón Jiménez 2025-08-23 01:44:17 +02:00
parent 8ca25d627f
commit 63142d34fc
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
29 changed files with 839 additions and 521 deletions

View file

@ -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<P: Program + 'static> Emulator<P> {
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<P: Program + 'static> Emulator<P> {
);
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<P: Program + 'static> Emulator<P> {
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<P: Program + 'static> Emulator<P> {
);
match operation.finish() {
widget::operation::Outcome::Some(matches)
if matches.len() == 1 =>
{
widget::operation::Outcome::Some(Some(_text)) => {
self.runtime.send(Event::Ready);
}
_ => {

View file

@ -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<dyn std::fmt::Debug + Send + Sync>,
},
/// An IO operation failed.
#[error("an IO operation failed: {0}")]
IOFailed(Arc<io::Error>),

View file

@ -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;

View file

@ -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<Vec<Target>> + 'a {
match self {
Selector::Id(id) => {
struct FindById {
id: widget::Id,
target: Option<Target>,
}
impl widget::Operation<Vec<Target>> for FindById {
fn container(
&mut self,
id: Option<&widget::Id>,
bounds: Rectangle,
operate_on_children: &mut dyn FnMut(
&mut dyn widget::Operation<Vec<Target>>,
),
) {
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<Vec<Target>>
{
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<dyn widget::Operation<_>>
}
Selector::Text(text) => {
struct FindByText {
text: text::Fragment<'static>,
target: Vec<Target>,
}
impl widget::Operation<Vec<Target>> for FindByText {
fn container(
&mut self,
_id: Option<&widget::Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(
&mut dyn widget::Operation<Vec<Target>>,
),
) {
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<Vec<Target>>
{
widget::operation::Outcome::Some(self.target.clone())
}
}
Box::new(FindByText {
text: text.clone(),
target: Vec::new(),
})
}
}
}
}
impl From<widget::Id> 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<widget::Id>) -> 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,
}

View file

@ -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<Selector>,
) -> Result<selector::Target, Error> {
pub fn find<S>(&mut self, selector: S) -> Result<S::Output, Error>
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<Selector>,
) -> Result<selector::Target, Error> {
pub fn click<S>(&mut self, selector: S) -> Result<S::Output, Error>
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());