Introduce new iced_selector subcrate and refactor Operation
This commit is contained in:
parent
8ca25d627f
commit
63142d34fc
29 changed files with 839 additions and 521 deletions
8
Cargo.lock
generated
8
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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
17
selector/Cargo.toml
Normal 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
270
selector/src/find.rs
Normal 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
113
selector/src/lib.rs
Normal 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
238
selector/src/target.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ workspace = true
|
|||
|
||||
[dependencies]
|
||||
iced_runtime.workspace = true
|
||||
iced_selector.workspace = true
|
||||
|
||||
iced_program.workspace = true
|
||||
iced_program.features = ["test"]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
_ => {
|
||||
|
|
|
|||
|
|
@ -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>),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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());
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue