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", "thiserror 2.0.14",
] ]
[[package]]
name = "iced_selector"
version = "0.14.0-dev"
dependencies = [
"iced_core",
]
[[package]] [[package]]
name = "iced_test" name = "iced_test"
version = "0.14.0-dev" version = "0.14.0-dev"
@ -2634,6 +2641,7 @@ dependencies = [
"iced_program", "iced_program",
"iced_renderer", "iced_renderer",
"iced_runtime", "iced_runtime",
"iced_selector",
"nom 8.0.0", "nom 8.0.0",
"png", "png",
"sha2", "sha2",

View file

@ -132,6 +132,7 @@ members = [
"program", "program",
"renderer", "renderer",
"runtime", "runtime",
"selector",
"test", "test",
"tiny_skia", "tiny_skia",
"wgpu", "wgpu",
@ -163,6 +164,7 @@ iced_highlighter = { version = "0.14.0-dev", path = "highlighter" }
iced_program = { version = "0.14.0-dev", path = "program" } iced_program = { version = "0.14.0-dev", path = "program" }
iced_renderer = { version = "0.14.0-dev", path = "renderer" } iced_renderer = { version = "0.14.0-dev", path = "renderer" }
iced_runtime = { version = "0.14.0-dev", path = "runtime" } 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_test = { version = "0.14.0-dev", path = "test" }
iced_tiny_skia = { version = "0.14.0-dev", path = "tiny_skia" } iced_tiny_skia = { version = "0.14.0-dev", path = "tiny_skia" }
iced_wgpu = { version = "0.14.0-dev", path = "wgpu" } iced_wgpu = { version = "0.14.0-dev", path = "wgpu" }

View file

@ -127,7 +127,7 @@ where
renderer: &Renderer, renderer: &Renderer,
operation: &mut dyn widget::Operation, 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( self.children.iter_mut().zip(layout.children()).for_each(
|(child, layout)| { |(child, layout)| {
child.as_overlay_mut().operate(layout, renderer, operation); 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 /// A piece of logic that can traverse the widget tree of an application in
/// order to query or update some widget state. /// order to query or update some widget state.
pub trait Operation<T = ()>: Send { 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 provided `operate` closure may be called by an [`Operation`]
/// the widget tree and keep traversing it. /// to return control to the widget tree and keep traversing it. If
fn container( /// the closure is not called, the children of the widget asking for
&mut self, /// traversal will be skipped.
id: Option<&Id>, fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>));
bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
);
/// Operates on a widget that can be focused. /// Operates on a widget that contains other widgets.
fn focusable( fn container(&mut self, _id: Option<&Id>, _bounds: Rectangle) {}
&mut self,
_id: Option<&Id>,
_bounds: Rectangle,
_state: &mut dyn Focusable,
) {
}
/// Operates on a widget that can be scrolled. /// Operates on a widget that can be scrolled.
fn scrollable( 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. /// Operates on a widget that has text input.
fn text_input( fn text_input(
&mut self, &mut self,
@ -80,13 +80,12 @@ impl<T, O> Operation<O> for Box<T>
where where
T: Operation<O> + ?Sized, T: Operation<O> + ?Sized,
{ {
fn container( fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<O>)) {
&mut self, self.as_mut().traverse(operate);
id: Option<&Id>, }
bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<O>), fn container(&mut self, id: Option<&Id>, bounds: Rectangle) {
) { self.as_mut().container(id, bounds);
self.as_mut().container(id, bounds, operate_on_children);
} }
fn focusable( fn focusable(
@ -179,17 +178,19 @@ where
} }
impl<T, O> Operation<O> for BlackBox<'_, T> { impl<T, O> Operation<O> for BlackBox<'_, T> {
fn container( fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<O>))
&mut self, where
id: Option<&Id>, Self: Sized,
bounds: Rectangle, {
operate_on_children: &mut dyn FnMut(&mut dyn Operation<O>), self.operation.traverse(&mut |operation| {
) { operate(&mut BlackBox { operation });
self.operation.container(id, bounds, &mut |operation| {
operate_on_children(&mut BlackBox { operation });
}); });
} }
fn container(&mut self, id: Option<&Id>, bounds: Rectangle) {
self.operation.container(id, bounds);
}
fn focusable( fn focusable(
&mut self, &mut self,
id: Option<&Id>, id: Option<&Id>,
@ -267,28 +268,25 @@ where
A: 'static, A: 'static,
B: 'static, B: 'static,
{ {
fn container( fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<B>)) {
&mut self,
id: Option<&Id>,
bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<B>),
) {
struct MapRef<'a, A> { struct MapRef<'a, A> {
operation: &'a mut dyn Operation<A>, operation: &'a mut dyn Operation<A>,
} }
impl<A, B> Operation<B> for MapRef<'_, A> { impl<A, B> Operation<B> for MapRef<'_, A> {
fn container( fn traverse(
&mut self, &mut self,
id: Option<&Id>, operate: &mut dyn FnMut(&mut dyn Operation<B>),
bounds: Rectangle,
operate_on_children: &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; let Self { operation, .. } = self;
operation.container(id, bounds, &mut |operation| { operation.container(id, bounds);
operate_on_children(&mut MapRef { operation });
});
} }
fn scrollable( 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( fn focusable(
@ -444,17 +446,16 @@ where
A: 'static, A: 'static,
B: Send + 'static, B: Send + 'static,
{ {
fn container( fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<B>)) {
&mut self, self.operation.traverse(&mut |operation| {
id: Option<&Id>, operate(&mut black_box(operation));
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 container(&mut self, id: Option<&Id>, bounds: Rectangle) {
self.operation.container(id, bounds);
}
fn focusable( fn focusable(
&mut self, &mut self,
id: Option<&Id>, id: Option<&Id>,
@ -531,21 +532,26 @@ pub fn scope<T: 'static>(
) -> impl Operation<T> { ) -> impl Operation<T> {
struct ScopedOperation<Message> { struct ScopedOperation<Message> {
target: Id, target: Id,
current: Option<Id>,
operation: Box<dyn Operation<Message>>, operation: Box<dyn Operation<Message>>,
} }
impl<Message: 'static> Operation<Message> for ScopedOperation<Message> { impl<Message: 'static> Operation<Message> for ScopedOperation<Message> {
fn container( fn traverse(
&mut self, &mut self,
id: Option<&Id>, operate: &mut dyn FnMut(&mut dyn Operation<Message>),
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<Message>),
) { ) {
if id == Some(&self.target) { if self.current.as_ref() == Some(&self.target) {
operate_on_children(self.operation.as_mut()); self.operation.as_mut().traverse(operate);
} else { } 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> { fn finish(&self) -> Outcome<Message> {
@ -553,6 +559,7 @@ pub fn scope<T: 'static>(
Outcome::Chain(next) => { Outcome::Chain(next) => {
Outcome::Chain(Box::new(ScopedOperation { Outcome::Chain(Box::new(ScopedOperation {
target: self.target.clone(), target: self.target.clone(),
current: None,
operation: next, operation: next,
})) }))
} }
@ -563,6 +570,7 @@ pub fn scope<T: 'static>(
ScopedOperation { ScopedOperation {
target, target,
current: None,
operation: Box::new(operation), operation: Box::new(operation),
} }
} }

View file

@ -48,13 +48,8 @@ pub fn focus<T>(target: Id) -> impl Operation<T> {
} }
} }
fn container( fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
&mut self, operate(self);
_id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) {
operate_on_children(self);
} }
} }
@ -75,13 +70,8 @@ pub fn unfocus<T>() -> impl Operation<T> {
state.unfocus(); state.unfocus();
} }
fn container( fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
&mut self, operate(self);
_id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) {
operate_on_children(self);
} }
} }
@ -109,13 +99,11 @@ pub fn count() -> impl Operation<Count> {
self.count.total += 1; self.count.total += 1;
} }
fn container( fn traverse(
&mut self, &mut self,
_id: Option<&Id>, operate: &mut dyn FnMut(&mut dyn Operation<Count>),
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<Count>),
) { ) {
operate_on_children(self); operate(self);
} }
fn finish(&self) -> Outcome<Count> { fn finish(&self) -> Outcome<Count> {
@ -163,13 +151,8 @@ where
self.current += 1; self.current += 1;
} }
fn container( fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
&mut self, operate(self);
_id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) {
operate_on_children(self);
} }
} }
@ -205,13 +188,8 @@ where
self.current += 1; self.current += 1;
} }
fn container( fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
&mut self, operate(self);
_id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) {
operate_on_children(self);
} }
} }
@ -237,13 +215,11 @@ pub fn find_focused() -> impl Operation<Id> {
} }
} }
fn container( fn traverse(
&mut self, &mut self,
_id: Option<&Id>, operate: &mut dyn FnMut(&mut dyn Operation<Id>),
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<Id>),
) { ) {
operate_on_children(self); operate(self);
} }
fn finish(&self) -> Outcome<Id> { fn finish(&self) -> Outcome<Id> {
@ -279,17 +255,15 @@ pub fn is_focused(target: Id) -> impl Operation<bool> {
} }
} }
fn container( fn traverse(
&mut self, &mut self,
_id: Option<&Id>, operate: &mut dyn FnMut(&mut dyn Operation<bool>),
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<bool>),
) { ) {
if self.is_focused.is_some() { if self.is_focused.is_some() {
return; return;
} }
operate_on_children(self); operate(self);
} }
fn finish(&self) -> Outcome<bool> { 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 { impl<T> Operation<T> for SnapTo {
fn container( fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
&mut self, operate(self);
_id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) {
operate_on_children(self);
} }
fn scrollable( fn scrollable(
@ -63,13 +58,8 @@ pub fn scroll_to<T>(target: Id, offset: AbsoluteOffset) -> impl Operation<T> {
} }
impl<T> Operation<T> for ScrollTo { impl<T> Operation<T> for ScrollTo {
fn container( fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
&mut self, operate(self);
_id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) {
operate_on_children(self);
} }
fn scrollable( fn scrollable(
@ -98,13 +88,8 @@ pub fn scroll_by<T>(target: Id, offset: AbsoluteOffset) -> impl Operation<T> {
} }
impl<T> Operation<T> for ScrollBy { impl<T> Operation<T> for ScrollBy {
fn container( fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
&mut self, operate(self);
_id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) {
operate_on_children(self);
} }
fn scrollable( fn scrollable(

View file

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

View file

@ -349,7 +349,8 @@ mod toast {
renderer: &Renderer, renderer: &Renderer,
operation: &mut dyn Operation, 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( self.content.as_widget().operate(
&mut state.children[0], &mut state.children[0],
layout, layout,
@ -580,7 +581,8 @@ mod toast {
renderer: &Renderer, renderer: &Renderer,
operation: &mut dyn widget::Operation, operation: &mut dyn widget::Operation,
) { ) {
operation.container(None, layout.bounds(), &mut |operation| { operation.container(None, layout.bounds());
operation.traverse(&mut |operation| {
self.toasts self.toasts
.iter() .iter()
.zip(self.state.iter_mut()) .zip(self.state.iter_mut())

View file

@ -613,8 +613,8 @@ fn presets() -> impl Iterator<Item = iced::application::Preset<Todos, Message>>
mod tests { mod tests {
use super::*; use super::*;
use iced::widget;
use iced::{Settings, Theme}; use iced::{Settings, Theme};
use iced_test::selector::id;
use iced_test::{Error, Simulator}; use iced_test::{Error, Simulator};
fn simulator(todos: &Todos) -> Simulator<'_, Message> { fn simulator(todos: &Todos) -> Simulator<'_, Message> {
@ -633,7 +633,7 @@ mod tests {
let _command = todos.update(Message::Loaded(Err(LoadError::File))); let _command = todos.update(Message::Loaded(Err(LoadError::File)));
let mut ui = simulator(&todos); 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.typewrite("Create the universe");
let _ = ui.tap_key(keyboard::key::Named::Enter); 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] [dependencies]
iced_runtime.workspace = true iced_runtime.workspace = true
iced_selector.workspace = true
iced_program.workspace = true iced_program.workspace = true
iced_program.features = ["test"] iced_program.features = ["test"]

View file

@ -1,4 +1,3 @@
use crate::Instruction;
use crate::core; use crate::core;
use crate::core::mouse; use crate::core::mouse;
use crate::core::renderer; use crate::core::renderer;
@ -17,6 +16,7 @@ use crate::runtime::user_interface;
use crate::runtime::window; use crate::runtime::window;
use crate::runtime::{Action, Task, UserInterface}; use crate::runtime::{Action, Task, UserInterface};
use crate::selector; use crate::selector;
use crate::{Instruction, Selector};
use std::fmt; use std::fmt;
@ -216,10 +216,10 @@ impl<P: Program + 'static> Emulator<P> {
let Some(events) = interaction.events(|target| match target { let Some(events) = interaction.events(|target| match target {
instruction::Target::Point(position) => Some(*position), instruction::Target::Point(position) => Some(*position),
instruction::Target::Text(text) => { instruction::Target::Text(text) => {
use selector::target::Bounded;
use widget::Operation; use widget::Operation;
let mut operation = let mut operation = Selector::find(text.as_str());
selector::text(text.to_owned()).operation();
user_interface.operate( user_interface.operate(
&self.renderer, &self.renderer,
@ -227,11 +227,8 @@ impl<P: Program + 'static> Emulator<P> {
); );
match operation.finish() { match operation.finish() {
widget::operation::Outcome::Some(matches) => { widget::operation::Outcome::Some(text) => {
matches Some(text?.visible_bounds()?.center())
.first()
.copied()
.map(|target| target.bounds.center())
} }
_ => None, _ => None,
} }
@ -273,7 +270,7 @@ impl<P: Program + 'static> Emulator<P> {
instruction::Expectation::Text(text) => { instruction::Expectation::Text(text) => {
use widget::Operation; use widget::Operation;
let mut operation = selector::text(text).operation(); let mut operation = Selector::find(text.as_str());
user_interface.operate( user_interface.operate(
&self.renderer, &self.renderer,
@ -281,9 +278,7 @@ impl<P: Program + 'static> Emulator<P> {
); );
match operation.finish() { match operation.finish() {
widget::operation::Outcome::Some(matches) widget::operation::Outcome::Some(Some(_text)) => {
if matches.len() == 1 =>
{
self.runtime.send(Event::Ready); self.runtime.send(Event::Ready);
} }
_ => { _ => {

View file

@ -1,5 +1,3 @@
use crate::Selector;
use std::io; use std::io;
use std::sync::Arc; use std::sync::Arc;
@ -7,8 +5,12 @@ use std::sync::Arc;
#[derive(Debug, Clone, thiserror::Error)] #[derive(Debug, Clone, thiserror::Error)]
pub enum Error { pub enum Error {
/// No matching widget was found for the [`Selector`]. /// No matching widget was found for the [`Selector`].
#[error("no matching widget was found for the selector: {0:?}")] #[error("no matching widget was found for the selector: {selector}")]
NotFound(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. /// An IO operation failed.
#[error("an IO operation failed: {0}")] #[error("an IO operation failed: {0}")]
IOFailed(Arc<io::Error>), IOFailed(Arc<io::Error>),

View file

@ -89,10 +89,11 @@ use iced_renderer as renderer;
use iced_runtime as runtime; use iced_runtime as runtime;
use iced_runtime::core; use iced_runtime::core;
pub use iced_selector as selector;
pub mod emulator; pub mod emulator;
pub mod ice; pub mod ice;
pub mod instruction; pub mod instruction;
pub mod selector;
pub mod simulator; pub mod simulator;
mod error; 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::renderer;
use crate::runtime::UserInterface; use crate::runtime::UserInterface;
use crate::runtime::user_interface; use crate::runtime::user_interface;
use crate::selector; use crate::selector::target::Bounded;
use crate::{Error, Selector}; use crate::{Error, Selector};
use std::borrow::Cow; use std::borrow::Cow;
use std::env; use std::env;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc;
/// A user interface that can be interacted with and inspected programmatically. /// A user interface that can be interacted with and inspected programmatically.
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
@ -100,14 +101,15 @@ where
} }
/// Finds the [`Target`] of the given widget [`Selector`] in the [`Simulator`]. /// Finds the [`Target`] of the given widget [`Selector`] in the [`Simulator`].
pub fn find( pub fn find<S>(&mut self, selector: S) -> Result<S::Output, Error>
&mut self, where
selector: impl Into<Selector>, S: Selector + Send,
) -> Result<selector::Target, Error> { S::Output: Clone + Send,
{
use widget::Operation; use widget::Operation;
let selector = selector.into(); let description = selector.description();
let mut operation = selector.operation(); let mut operation = selector.find();
self.raw.operate( self.raw.operate(
&self.renderer, &self.renderer,
@ -115,10 +117,14 @@ where
); );
match operation.finish() { match operation.finish() {
widget::operation::Outcome::Some(matches) => { widget::operation::Outcome::Some(output) => {
matches.first().copied().ok_or(Error::NotFound(selector)) output.ok_or(Error::NotFound {
selector: description,
})
} }
_ => Err(Error::NotFound(selector)), _ => Err(Error::NotFound {
selector: description,
}),
} }
} }
@ -134,12 +140,20 @@ where
/// This consists in: /// This consists in:
/// - Pointing the mouse cursor at the center of the [`Target`]. /// - Pointing the mouse cursor at the center of the [`Target`].
/// - Simulating a [`click`]. /// - Simulating a [`click`].
pub fn click( pub fn click<S>(&mut self, selector: S) -> Result<S::Output, Error>
&mut self, where
selector: impl Into<Selector>, S: Selector + Send,
) -> Result<selector::Target, Error> { S::Output: Bounded + Clone + Send + Sync + 'static,
{
let target = self.find(selector)?; 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()); let _ = self.simulate(click());

View file

@ -262,7 +262,8 @@ where
renderer: &Renderer, renderer: &Renderer,
operation: &mut dyn Operation, 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( self.content.as_widget().operate(
&mut tree.children[0], &mut tree.children[0],
layout.children().next().unwrap(), layout.children().next().unwrap(),

View file

@ -234,7 +234,8 @@ where
renderer: &Renderer, renderer: &Renderer,
operation: &mut dyn Operation, operation: &mut dyn Operation,
) { ) {
operation.container(None, layout.bounds(), &mut |operation| { operation.container(None, layout.bounds());
operation.traverse(&mut |operation| {
self.children self.children
.iter() .iter()
.zip(&mut tree.children) .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::widget::{self, Operation};
use crate::core::{ use crate::core::{
self, Background, Clipboard, Color, Element, Event, Layout, Length, self, Background, Clipboard, Color, Element, Event, Layout, Length,
Padding, Pixels, Point, Rectangle, Shadow, Shell, Size, Theme, Vector, Padding, Pixels, Rectangle, Shadow, Shell, Size, Theme, Vector, Widget,
Widget, color, color,
}; };
use crate::runtime::task::{self, Task}; use crate::runtime::Task;
/// A widget that aligns its contents inside of its boundaries. /// A widget that aligns its contents inside of its boundaries.
/// ///
@ -284,18 +284,15 @@ where
renderer: &Renderer, renderer: &Renderer,
operation: &mut dyn Operation, operation: &mut dyn Operation,
) { ) {
operation.container( operation.container(self.id.as_ref().map(|id| &id.0), layout.bounds());
self.id.as_ref().map(|id| &id.0), operation.traverse(&mut |operation| {
layout.bounds(), self.content.as_widget().operate(
&mut |operation| { tree,
self.content.as_widget().operate( layout.children().next().unwrap(),
tree, renderer,
layout.children().next().unwrap(), operation,
renderer, );
operation, });
);
},
);
} }
fn update( fn update(
@ -492,94 +489,8 @@ impl From<&'static str> for Id {
/// Produces a [`Task`] that queries the visible screen bounds of the /// Produces a [`Task`] that queries the visible screen bounds of the
/// [`Container`] with the given [`Id`]. /// [`Container`] with the given [`Id`].
pub fn visible_bounds(id: impl Into<Id>) -> Task<Option<Rectangle>> { pub fn visible_bounds(_id: impl Into<Id>) -> Task<Option<Rectangle>> {
let id = id.into(); todo!()
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,
})
} }
/// The appearance of a container. /// The appearance of a container.

View file

@ -257,7 +257,8 @@ where
renderer: &Renderer, renderer: &Renderer,
operation: &mut dyn Operation, operation: &mut dyn Operation,
) { ) {
operation.container(None, layout.bounds(), &mut |operation| { operation.container(None, layout.bounds());
operation.traverse(&mut |operation| {
self.children self.children
.iter() .iter()
.zip(&mut tree.children) .zip(&mut tree.children)

View file

@ -284,7 +284,8 @@ where
renderer: &Renderer, renderer: &Renderer,
operation: &mut dyn Operation, operation: &mut dyn Operation,
) { ) {
operation.container(None, layout.bounds(), &mut |operation| { operation.container(None, layout.bounds());
operation.traverse(&mut |operation| {
self.children self.children
.iter() .iter()
.zip(&mut tree.children) .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 as runtime;
pub use iced_runtime::core; pub use iced_runtime::core;
pub use core::widget::Id;
mod action; mod action;
mod column; mod column;
mod mouse_area; mod mouse_area;

View file

@ -468,7 +468,8 @@ where
renderer: &Renderer, renderer: &Renderer,
operation: &mut dyn widget::Operation, operation: &mut dyn widget::Operation,
) { ) {
operation.container(None, layout.bounds(), &mut |operation| { operation.container(None, layout.bounds());
operation.traverse(&mut |operation| {
self.panes self.panes
.iter() .iter()
.copied() .copied()

View file

@ -234,7 +234,8 @@ where
renderer: &Renderer, renderer: &Renderer,
operation: &mut dyn Operation, operation: &mut dyn Operation,
) { ) {
operation.container(None, layout.bounds(), &mut |operation| { operation.container(None, layout.bounds());
operation.traverse(&mut |operation| {
self.children self.children
.iter() .iter()
.zip(&mut tree.children) .zip(&mut tree.children)

View file

@ -549,18 +549,14 @@ where
state, state,
); );
operation.container( operation.traverse(&mut |operation| {
self.id.as_ref().map(|id| &id.0), self.content.as_widget().operate(
bounds, &mut tree.children[0],
&mut |operation| { layout.children().next().unwrap(),
self.content.as_widget().operate( renderer,
&mut tree.children[0], operation,
layout.children().next().unwrap(), );
renderer, });
operation,
);
},
);
} }
fn update( fn update(

View file

@ -188,7 +188,8 @@ where
renderer: &Renderer, renderer: &Renderer,
operation: &mut dyn Operation, operation: &mut dyn Operation,
) { ) {
operation.container(None, layout.bounds(), &mut |operation| { operation.container(None, layout.bounds());
operation.traverse(&mut |operation| {
self.children self.children
.iter() .iter()
.zip(&mut tree.children) .zip(&mut tree.children)