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

8
Cargo.lock generated
View file

@ -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",

View file

@ -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" }

View file

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

View file

@ -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<T = ()>: 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<T>),
);
/// 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<T>));
/// 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<T = ()>: 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<T, O> Operation<O> for Box<T>
where
T: Operation<O> + ?Sized,
{
fn container(
&mut self,
id: Option<&Id>,
bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<O>),
) {
self.as_mut().container(id, bounds, operate_on_children);
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<O>)) {
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<T, O> Operation<O> for BlackBox<'_, T> {
fn container(
&mut self,
id: Option<&Id>,
bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<O>),
) {
self.operation.container(id, bounds, &mut |operation| {
operate_on_children(&mut BlackBox { operation });
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<O>))
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<B>),
) {
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<B>)) {
struct MapRef<'a, A> {
operation: &'a mut dyn Operation<A>,
}
impl<A, B> Operation<B> for MapRef<'_, A> {
fn container(
fn traverse(
&mut self,
id: Option<&Id>,
bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<B>),
operate: &mut dyn FnMut(&mut dyn Operation<B>),
) {
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<B>),
) {
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<B>)) {
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<T: 'static>(
) -> impl Operation<T> {
struct ScopedOperation<Message> {
target: Id,
current: Option<Id>,
operation: Box<dyn Operation<Message>>,
}
impl<Message: 'static> Operation<Message> for ScopedOperation<Message> {
fn container(
fn traverse(
&mut self,
id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<Message>),
operate: &mut dyn FnMut(&mut dyn Operation<Message>),
) {
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<Message> {
@ -553,6 +559,7 @@ pub fn scope<T: 'static>(
Outcome::Chain(next) => {
Outcome::Chain(Box::new(ScopedOperation {
target: self.target.clone(),
current: None,
operation: next,
}))
}
@ -563,6 +570,7 @@ pub fn scope<T: 'static>(
ScopedOperation {
target,
current: None,
operation: Box::new(operation),
}
}

View file

@ -48,13 +48,8 @@ pub fn focus<T>(target: Id) -> impl Operation<T> {
}
}
fn container(
&mut self,
_id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) {
operate_on_children(self);
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
operate(self);
}
}
@ -75,13 +70,8 @@ pub fn unfocus<T>() -> impl Operation<T> {
state.unfocus();
}
fn container(
&mut self,
_id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) {
operate_on_children(self);
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
operate(self);
}
}
@ -109,13 +99,11 @@ pub fn count() -> impl Operation<Count> {
self.count.total += 1;
}
fn container(
fn traverse(
&mut self,
_id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<Count>),
operate: &mut dyn FnMut(&mut dyn Operation<Count>),
) {
operate_on_children(self);
operate(self);
}
fn finish(&self) -> Outcome<Count> {
@ -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<T>),
) {
operate_on_children(self);
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
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<T>),
) {
operate_on_children(self);
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
operate(self);
}
}
@ -237,13 +215,11 @@ pub fn find_focused() -> impl Operation<Id> {
}
}
fn container(
fn traverse(
&mut self,
_id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<Id>),
operate: &mut dyn FnMut(&mut dyn Operation<Id>),
) {
operate_on_children(self);
operate(self);
}
fn finish(&self) -> Outcome<Id> {
@ -279,17 +255,15 @@ pub fn is_focused(target: Id) -> impl Operation<bool> {
}
}
fn container(
fn traverse(
&mut self,
_id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<bool>),
operate: &mut dyn FnMut(&mut dyn Operation<bool>),
) {
if self.is_focused.is_some() {
return;
}
operate_on_children(self);
operate(self);
}
fn finish(&self) -> Outcome<bool> {

View file

@ -28,13 +28,8 @@ pub fn snap_to<T>(target: Id, offset: RelativeOffset) -> impl Operation<T> {
}
impl<T> Operation<T> for SnapTo {
fn container(
&mut self,
_id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) {
operate_on_children(self);
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
operate(self);
}
fn scrollable(
@ -63,13 +58,8 @@ pub fn scroll_to<T>(target: Id, offset: AbsoluteOffset) -> impl Operation<T> {
}
impl<T> Operation<T> for ScrollTo {
fn container(
&mut self,
_id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) {
operate_on_children(self);
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
operate(self);
}
fn scrollable(
@ -98,13 +88,8 @@ pub fn scroll_by<T>(target: Id, offset: AbsoluteOffset) -> impl Operation<T> {
}
impl<T> Operation<T> for ScrollBy {
fn container(
&mut self,
_id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) {
operate_on_children(self);
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
operate(self);
}
fn scrollable(

View file

@ -45,13 +45,8 @@ pub fn move_cursor_to_front<T>(target: Id) -> impl Operation<T> {
}
}
fn container(
&mut self,
_id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) {
operate_on_children(self);
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
operate(self);
}
}
@ -80,13 +75,8 @@ pub fn move_cursor_to_end<T>(target: Id) -> impl Operation<T> {
}
}
fn container(
&mut self,
_id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) {
operate_on_children(self);
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
operate(self);
}
}
@ -116,13 +106,8 @@ pub fn move_cursor_to<T>(target: Id, position: usize) -> impl Operation<T> {
}
}
fn container(
&mut self,
_id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) {
operate_on_children(self);
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
operate(self);
}
}
@ -150,13 +135,8 @@ pub fn select_all<T>(target: Id) -> impl Operation<T> {
}
}
fn container(
&mut self,
_id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) {
operate_on_children(self);
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
operate(self);
}
}

View file

@ -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())

View file

@ -613,8 +613,8 @@ fn presets() -> impl Iterator<Item = iced::application::Preset<Todos, Message>>
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);

17
selector/Cargo.toml Normal file
View file

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

270
selector/src/find.rs Normal file
View file

@ -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<S> = Finder<One<S>>;
pub type FindAll<S> = Finder<All<S>>;
#[derive(Debug)]
pub struct One<S>
where
S: Selector,
{
selector: S,
output: Option<S::Output>,
}
impl<S> One<S>
where
S: Selector,
{
pub fn new(selector: S) -> Self {
Self {
selector,
output: None,
}
}
}
impl<S> Strategy for One<S>
where
S: Selector,
S::Output: Clone,
{
type Output = Option<S::Output>;
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<S>
where
S: Selector,
{
selector: S,
outputs: Vec<S::Output>,
}
impl<S> All<S>
where
S: Selector,
{
pub fn new(selector: S) -> Self {
Self {
selector,
outputs: Vec::new(),
}
}
}
impl<S> Strategy for All<S>
where
S: Selector,
S::Output: Clone,
{
type Output = Vec<S::Output>;
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<S> {
strategy: S,
stack: Vec<(Rectangle, Vector)>,
viewport: Rectangle,
translation: Vector,
}
impl<S> Finder<S> {
pub fn new(strategy: S) -> Self {
Self {
strategy,
stack: vec![(Rectangle::INFINITE, Vector::ZERO)],
viewport: Rectangle::INFINITE,
translation: Vector::ZERO,
}
}
}
impl<S> Operation<S::Output> for Finder<S>
where
S: Strategy + Send,
S::Output: Send,
{
fn traverse(
&mut self,
operate: &mut dyn FnMut(&mut dyn Operation<S::Output>),
) {
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<S::Output> {
Outcome::Some(self.strategy.finish())
}
}

113
selector/src/lib.rs Normal file
View file

@ -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<Self::Output>;
fn description(&self) -> String;
fn find(self) -> Find<Self>
where
Self: Sized,
{
Find::new(find::One::new(self))
}
fn find_all(self) -> FindAll<Self>
where
Self: Sized,
{
FindAll::new(find::All::new(self))
}
}
impl Selector for &str {
type Output = target::Text;
fn select(&mut self, target: Target<'_>) -> Option<Self::Output> {
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<Self::Output> {
if target.id() != Some(self) {
return None;
}
Some(target::Match::from_target(target))
}
fn description(&self) -> String {
format!("id == \"{:?}\"", self)
}
}
impl<F, T> Selector for F
where
F: FnMut(Target<'_>) -> Option<T>,
{
type Output = T;
fn select(&mut self, target: Target<'_>) -> Option<Self::Output> {
(self)(target)
}
fn description(&self) -> String {
format!("custom selector: {}", std::any::type_name_of_val(self))
}
}
// pub fn inspect(position: Point) -> impl Selector<Output = (Match, Rectangle)> {
// visible(move |target: Target<'_>, visible_bounds: Rectangle| {
// visible_bounds
// .contains(position)
// .then(|| Match::from_target(target))
// })
// }
// pub fn visible<T>(
// f: impl Fn(Target<'_>, Rectangle) -> Option<T>,
// ) -> impl Selector<Output = (T, Rectangle)> {
// todo!()
// }
//

238
selector/src/target.rs Normal file
View file

@ -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<Rectangle>,
},
Focusable {
id: Option<&'a Id>,
bounds: Rectangle,
visible_bounds: Option<Rectangle>,
state: &'a dyn Focusable,
},
Scrollable {
id: Option<&'a Id>,
bounds: Rectangle,
content_bounds: Rectangle,
visible_bounds: Option<Rectangle>,
translation: Vector,
state: &'a dyn Scrollable,
},
TextInput {
id: Option<&'a Id>,
bounds: Rectangle,
visible_bounds: Option<Rectangle>,
state: &'a dyn TextInput,
},
Text {
id: Option<&'a Id>,
bounds: Rectangle,
visible_bounds: Option<Rectangle>,
content: &'a str,
},
Custom {
id: Option<&'a Id>,
bounds: Rectangle,
visible_bounds: Option<Rectangle>,
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<Id>,
bounds: Rectangle,
visible_bounds: Option<Rectangle>,
},
Focusable {
id: Option<Id>,
bounds: Rectangle,
visible_bounds: Option<Rectangle>,
},
Scrollable {
id: Option<Id>,
bounds: Rectangle,
visible_bounds: Option<Rectangle>,
content_bounds: Rectangle,
translation: Vector,
},
TextInput {
id: Option<Id>,
bounds: Rectangle,
visible_bounds: Option<Rectangle>,
},
Text {
id: Option<Id>,
bounds: Rectangle,
visible_bounds: Option<Rectangle>,
content: String,
},
Custom {
id: Option<Id>,
bounds: Rectangle,
visible_bounds: Option<Rectangle>,
},
}
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<Rectangle> {
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<Rectangle>;
}
#[derive(Debug, Clone, PartialEq)]
pub enum Text {
Raw {
id: Option<Id>,
bounds: Rectangle,
visible_bounds: Option<Rectangle>,
},
Input {
id: Option<Id>,
bounds: Rectangle,
visible_bounds: Option<Rectangle>,
},
}
impl Bounded for Text {
fn bounds(&self) -> Rectangle {
match self {
Text::Raw { bounds, .. } | Text::Input { bounds, .. } => *bounds,
}
}
fn visible_bounds(&self) -> Option<Rectangle> {
match self {
Text::Raw { visible_bounds, .. }
| Text::Input { visible_bounds, .. } => *visible_bounds,
}
}
}

View file

@ -15,6 +15,7 @@ workspace = true
[dependencies]
iced_runtime.workspace = true
iced_selector.workspace = true
iced_program.workspace = true
iced_program.features = ["test"]

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());

View file

@ -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(),

View file

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

View file

@ -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<Id>) -> Task<Option<Rectangle>> {
let id = id.into();
struct VisibleBounds {
target: widget::Id,
depth: usize,
scrollables: Vec<(Vector, Rectangle, usize)>,
bounds: Option<Rectangle>,
}
impl Operation<Option<Rectangle>> 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<Option<Rectangle>>,
),
) {
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<Option<Rectangle>> {
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<Id>) -> Task<Option<Rectangle>> {
todo!()
}
/// The appearance of a container.

View file

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

View file

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

View file

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

View file

@ -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()

View file

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

View file

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

View file

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