From 3b5fbf61cb9ff2f9db8496cdbfa60bb65208de6a Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 2 May 2023 15:48:20 -0700 Subject: [PATCH 1/7] feat: accessibility with some widget impls --- Cargo.toml | 10 +- accessibility/Cargo.toml | 19 ++ accessibility/src/a11y_tree.rs | 80 ++++++ accessibility/src/id.rs | 176 +++++++++++++ accessibility/src/lib.rs | 19 ++ accessibility/src/node.rs | 46 ++++ accessibility/src/traits.rs | 19 ++ core/Cargo.toml | 8 + core/src/element.rs | 8 +- core/src/event.rs | 10 + core/src/id.rs | 130 ++++++++++ core/src/lib.rs | 31 +-- core/src/overlay.rs | 6 +- core/src/overlay/element.rs | 6 +- core/src/overlay/group.rs | 4 +- core/src/widget.rs | 24 +- core/src/widget/id.rs | 43 ---- core/src/widget/operation.rs | 208 ++++++++++++--- core/src/widget/text.rs | 39 +++ runtime/Cargo.toml | 7 + runtime/src/command.rs | 1 + runtime/src/command/action.rs | 9 + runtime/src/command/platform_specific/mod.rs | 33 +++ runtime/src/user_interface.rs | 17 +- widget/Cargo.toml | 6 + widget/src/button.rs | 216 +++++++++++++++- widget/src/checkbox.rs | 124 +++++++++ widget/src/column.rs | 24 +- widget/src/container.rs | 16 +- widget/src/helpers.rs | 8 +- widget/src/image.rs | 120 ++++++++- widget/src/lazy.rs | 5 +- widget/src/lazy/component.rs | 7 +- widget/src/lazy/responsive.rs | 4 +- widget/src/mouse_area.rs | 4 +- widget/src/pane_grid.rs | 4 +- widget/src/pane_grid/content.rs | 6 +- widget/src/pane_grid/title_bar.rs | 6 +- widget/src/row.rs | 24 +- widget/src/scrollable.rs | 243 ++++++++++++++---- widget/src/slider.rs | 128 ++++++++++ widget/src/svg.rs | 118 ++++++++- widget/src/text_input.rs | 3 +- widget/src/toggler.rs | 149 ++++++++++- winit/Cargo.toml | 13 +- winit/src/application.rs | 252 +++++++++++++++++-- winit/src/application/state.rs | 5 + winit/src/conversion.rs | 10 + 48 files changed, 2241 insertions(+), 207 deletions(-) create mode 100644 accessibility/Cargo.toml create mode 100644 accessibility/src/a11y_tree.rs create mode 100644 accessibility/src/id.rs create mode 100644 accessibility/src/lib.rs create mode 100644 accessibility/src/node.rs create mode 100644 accessibility/src/traits.rs create mode 100644 core/src/id.rs delete mode 100644 core/src/widget/id.rs create mode 100644 runtime/src/command/platform_specific/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 2f92f9db0c..d613143b00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"] categories = ["gui"] [features] -default = ["wgpu", "tiny-skia"] +default = ["wgpu", "tiny-skia", "winit", "a11y"] # Enable the `wgpu` GPU-accelerated renderer backend wgpu = ["iced_renderer/wgpu"] # Enable the `tiny-skia` software renderer backend @@ -41,6 +41,10 @@ palette = ["iced_core/palette"] system = ["iced_winit/system"] # Enables the advanced module advanced = [] +# Enables the `accesskit` accessibility library +a11y = ["iced_accessibility", "iced_widget/a11y", "iced_winit?/a11y"] +# Enables the winit shell. Conflicts with `wayland` and `glutin`. +winit = ["iced_winit"] [badges] maintenance = { status = "actively-developed" } @@ -58,6 +62,7 @@ members = [ "widget", "winit", "examples/*", + "accessibility", ] [dependencies] @@ -65,7 +70,8 @@ iced_core = { version = "0.9", path = "core" } iced_futures = { version = "0.6", path = "futures" } iced_renderer = { version = "0.1", path = "renderer" } iced_widget = { version = "0.1", path = "widget" } -iced_winit = { version = "0.9", path = "winit", features = ["application"] } +iced_winit = { version = "0.9", path = "winit", features = ["application"], optional = true } +iced_accessibility = { version = "0.1", path = "accessibility", optional = true } thiserror = "1" [dependencies.image_rs] diff --git a/accessibility/Cargo.toml b/accessibility/Cargo.toml new file mode 100644 index 0000000000..25aaeb0bd9 --- /dev/null +++ b/accessibility/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "iced_accessibility" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +# TODO Ashley re-export more platform adapters + +[dependencies] +accesskit = { git = "https://github.com/wash2/accesskit.git", tag = "v0.11.0", version = "0.11.0" } +accesskit_unix = { git = "https://github.com/wash2/accesskit.git", tag = "v0.11.0", version = "0.4.0", optional = true } +accesskit_windows = { git = "https://github.com/wash2/accesskit.git", tag = "v0.11.0", version = "0.14.0", optional = true} +accesskit_macos = { git = "https://github.com/wash2/accesskit.git", tag = "v0.11.0", version = "0.7.0", optional = true} +accesskit_winit = { git = "https://github.com/wash2/accesskit.git", tag = "v0.11.0", version = "0.13.0", optional = true} +# accesskit = { path = "../../fork/accesskit/common/", version = "0.11.0" } +# accesskit_unix = { path = "../../fork/accesskit/platforms/unix/", version = "0.4.0", optional = true } +# accesskit_windows = { path = "../../fork/accesskit/platforms/windows/", version = "0.14.0", optional = true} +# accesskit_macos = { path = "../../fork/accesskit/platforms/macos/", version = "0.7.0", optional = true} +# accesskit_winit = { path = "../../fork/accesskit/platforms/winit/", version = "0.13.0", optional = true} diff --git a/accessibility/src/a11y_tree.rs b/accessibility/src/a11y_tree.rs new file mode 100644 index 0000000000..964b7656b1 --- /dev/null +++ b/accessibility/src/a11y_tree.rs @@ -0,0 +1,80 @@ +use crate::{A11yId, A11yNode}; + +#[derive(Debug, Clone, Default)] +/// Accessible tree of nodes +pub struct A11yTree { + /// The root of the current widget, children of the parent widget or the Window if there is no parent widget + root: Vec, + /// The children of a widget and its children + children: Vec, +} + +impl A11yTree { + /// Create a new A11yTree + /// XXX if you use this method, you will need to manually add the children of the root nodes + pub fn new(root: Vec, children: Vec) -> Self { + Self { root, children } + } + + pub fn leaf>(node: accesskit::NodeBuilder, id: T) -> Self { + Self { + root: vec![A11yNode::new(node, id)], + children: vec![], + } + } + + /// Helper for creating an A11y tree with a single root node and some children + pub fn node_with_child_tree(mut root: A11yNode, child_tree: Self) -> Self { + root.add_children( + child_tree.root.iter().map(|n| n.id()).cloned().collect(), + ); + Self { + root: vec![root], + children: child_tree + .children + .into_iter() + .chain(child_tree.root) + .collect(), + } + } + + /// Joins multiple trees into a single tree + pub fn join>(trees: T) -> Self { + trees.fold(Self::default(), |mut acc, A11yTree { root, children }| { + acc.root.extend(root); + acc.children.extend(children); + acc + }) + } + + pub fn root(&self) -> &Vec { + &self.root + } + + pub fn children(&self) -> &Vec { + &self.children + } + + pub fn root_mut(&mut self) -> &mut Vec { + &mut self.root + } + + pub fn children_mut(&mut self) -> &mut Vec { + &mut self.children + } + + pub fn contains(&self, id: &A11yId) -> bool { + self.root.iter().any(|n| n.id() == id) + || self.children.iter().any(|n| n.id() == id) + } +} + +impl From for Vec<(accesskit::NodeId, accesskit::Node)> { + fn from(tree: A11yTree) -> Vec<(accesskit::NodeId, accesskit::Node)> { + tree.root + .into_iter() + .map(|node| node.into()) + .chain(tree.children.into_iter().map(|node| node.into())) + .collect() + } +} diff --git a/accessibility/src/id.rs b/accessibility/src/id.rs new file mode 100644 index 0000000000..81e4dac897 --- /dev/null +++ b/accessibility/src/id.rs @@ -0,0 +1,176 @@ +//! Widget and Window IDs. + +use std::sync::atomic::{self, AtomicU64}; +use std::{borrow, num::NonZeroU128}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum A11yId { + Window(NonZeroU128), + Widget(Id), +} + +// impl A11yId { +// pub fn new_widget() -> Self { +// Self::Widget(Id::unique()) +// } + +// pub fn new_window() -> Self { +// Self::Window(window_node_id()) +// } +// } + +impl From for A11yId { + fn from(id: NonZeroU128) -> Self { + Self::Window(id) + } +} + +impl From for A11yId { + fn from(id: Id) -> Self { + Self::Widget(id) + } +} + +impl From for A11yId { + fn from(value: accesskit::NodeId) -> Self { + let val = u128::from(value.0); + if val > u64::MAX as u128 { + Self::Window(value.0) + } else { + Self::Widget(Id::from(val as u64)) + } + } +} + +impl From for accesskit::NodeId { + fn from(value: A11yId) -> Self { + let node_id = match value { + A11yId::Window(id) => id, + A11yId::Widget(id) => id.into(), + }; + accesskit::NodeId(node_id) + } +} + +static NEXT_ID: AtomicU64 = AtomicU64::new(1); +static NEXT_WINDOW_ID: AtomicU64 = AtomicU64::new(1); + +/// The identifier of a generic widget. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Id(pub Internal); + +impl Id { + /// Creates a custom [`Id`]. + pub fn new(id: impl Into>) -> Self { + Self(Internal::Custom(Self::next(), id.into())) + } + + /// resets the id counter + pub fn reset() { + NEXT_ID.store(1, atomic::Ordering::Relaxed); + } + + fn next() -> u64 { + NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed) + } + + /// Creates a unique [`Id`]. + /// + /// This function produces a different [`Id`] every time it is called. + pub fn unique() -> Self { + let id = Self::next(); + + Self(Internal::Unique(id)) + } +} + +// Not meant to be used directly +impl From for Id { + fn from(value: u64) -> Self { + Self(Internal::Unique(value)) + } +} + +// Not meant to be used directly +impl Into for Id { + fn into(self) -> NonZeroU128 { + match &self.0 { + Internal::Unique(id) => NonZeroU128::try_from(*id as u128).unwrap(), + Internal::Custom(id, _) => { + NonZeroU128::try_from(*id as u128).unwrap() + } + // this is a set id, which is not a valid id and will not ever be converted to a NonZeroU128 + // so we panic + Internal::Set(_) => { + panic!("Cannot convert a set id to a NonZeroU128") + } + } + } +} + +impl ToString for Id { + fn to_string(&self) -> String { + match &self.0 { + Internal::Unique(_) => "Undefined".to_string(), + Internal::Custom(_, id) => id.to_string(), + Internal::Set(_) => "Set".to_string(), + } + } +} + +// XXX WIndow IDs are made unique by adding u64::MAX to them +/// get window node id that won't conflict with other node ids for the duration of the program +pub fn window_node_id() -> NonZeroU128 { + std::num::NonZeroU128::try_from( + u64::MAX as u128 + + NEXT_WINDOW_ID.fetch_add(1, atomic::Ordering::Relaxed) as u128, + ) + .unwrap() +} + +// TODO refactor to make panic impossible? +#[derive(Debug, Clone, Eq, Hash)] +/// Internal representation of an [`Id`]. +pub enum Internal { + /// a unique id + Unique(u64), + /// a custom id, which is equal to any [`Id`] with a matching number or string + Custom(u64, borrow::Cow<'static, str>), + /// XXX Do not use this as an id for an accessibility node, it will panic! + /// XXX Only meant to be used for widgets that have multiple accessibility nodes, each with a + /// unique or custom id + /// an Id Set, which is equal to any [`Id`] with a matching number or string + Set(Vec), +} + +impl PartialEq for Internal { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Unique(l0), Self::Unique(r0)) => l0 == r0, + (Self::Custom(l0, l1), Self::Custom(r0, r1)) => { + l0 == r0 || l1 == r1 + } + // allow custom ids to be equal to unique ids + (Self::Unique(l0), Self::Custom(r0, _)) + | (Self::Custom(l0, _), Self::Unique(r0)) => l0 == r0, + (Self::Set(l0), Self::Set(r0)) => l0 == r0, + // allow set ids to just be equal to any of their members + (Self::Set(l0), r) | (r, Self::Set(l0)) => { + l0.iter().any(|l| l == r) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::Id; + + #[test] + fn unique_generates_different_ids() { + let a = Id::unique(); + let b = Id::unique(); + + assert_ne!(a, b); + } +} diff --git a/accessibility/src/lib.rs b/accessibility/src/lib.rs new file mode 100644 index 0000000000..8d57e1c7e2 --- /dev/null +++ b/accessibility/src/lib.rs @@ -0,0 +1,19 @@ +mod a11y_tree; +pub mod id; +mod node; +mod traits; + +pub use a11y_tree::*; +pub use accesskit; +pub use id::*; +pub use node::*; +pub use traits::*; + +#[cfg(feature = "accesskit_macos")] +pub use accesskit_macos; +#[cfg(feature = "accesskit_unix")] +pub use accesskit_unix; +#[cfg(feature = "accesskit_windows")] +pub use accesskit_windows; +#[cfg(feature = "accesskit_winit")] +pub use accesskit_winit; diff --git a/accessibility/src/node.rs b/accessibility/src/node.rs new file mode 100644 index 0000000000..e419903c3c --- /dev/null +++ b/accessibility/src/node.rs @@ -0,0 +1,46 @@ +use accesskit::NodeClassSet; + +use crate::A11yId; + +#[derive(Debug, Clone)] +pub struct A11yNode { + node: accesskit::NodeBuilder, + id: A11yId, +} + +impl A11yNode { + pub fn new>(node: accesskit::NodeBuilder, id: T) -> Self { + Self { + node, + id: id.into(), + } + } + + pub fn id(&self) -> &A11yId { + &self.id + } + + pub fn node_mut(&mut self) -> &mut accesskit::NodeBuilder { + &mut self.node + } + + pub fn node(&self) -> &accesskit::NodeBuilder { + &self.node + } + + pub fn add_children(&mut self, children: Vec) { + let mut children = + children.into_iter().map(|id| id.into()).collect::>(); + children.extend_from_slice(self.node.children()); + self.node.set_children(children); + } +} + +impl From for (accesskit::NodeId, accesskit::Node) { + fn from(node: A11yNode) -> Self { + ( + node.id.into(), + node.node.build(&mut NodeClassSet::lock_global()), + ) + } +} diff --git a/accessibility/src/traits.rs b/accessibility/src/traits.rs new file mode 100644 index 0000000000..14ece49a20 --- /dev/null +++ b/accessibility/src/traits.rs @@ -0,0 +1,19 @@ +use std::borrow::Cow; + +use crate::A11yId; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Description<'a> { + Text(Cow<'a, str>), + Id(Vec), +} + +// Describes a widget +pub trait Describes { + fn description(&self) -> Vec; +} + +// Labels a widget +pub trait Labels { + fn label(&self) -> Vec; +} diff --git a/core/Cargo.toml b/core/Cargo.toml index 92d9773f43..b09fd30b1c 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -7,6 +7,9 @@ description = "The essential concepts of Iced" license = "MIT" repository = "https://github.com/iced-rs/iced" +[features] +a11y = ["iced_accessibility"] + [dependencies] bitflags = "1.2" thiserror = "1" @@ -18,3 +21,8 @@ optional = true [target.'cfg(target_arch = "wasm32")'.dependencies] instant = "0.1" + +[dependencies.iced_accessibility] +version = "0.1.0" +path = "../accessibility" +optional = true diff --git a/core/src/element.rs b/core/src/element.rs index 98c5373786..c65ba678f4 100644 --- a/core/src/element.rs +++ b/core/src/element.rs @@ -317,7 +317,9 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn widget::Operation< + widget::OperationOutputWrapper, + >, ) { struct MapOperation<'a, B> { operation: &'a mut dyn widget::Operation, @@ -509,7 +511,9 @@ where state: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn widget::Operation< + widget::OperationOutputWrapper, + >, ) { self.element .widget diff --git a/core/src/event.rs b/core/src/event.rs index 953cd73f94..21faca3a1c 100644 --- a/core/src/event.rs +++ b/core/src/event.rs @@ -24,6 +24,13 @@ pub enum Event { /// A touch event Touch(touch::Event), + #[cfg(feature = "a11y")] + /// An Accesskit event for a specific Accesskit Node in an accessible widget + A11y( + crate::widget::Id, + iced_accessibility::accesskit::ActionRequest, + ), + /// A platform specific event PlatformSpecific(PlatformSpecific), } @@ -31,6 +38,9 @@ pub enum Event { /// A platform specific event #[derive(Debug, Clone, PartialEq, Eq)] pub enum PlatformSpecific { + /// A Wayland specific event + #[cfg(feature = "wayland")] + Wayland(wayland::Event), /// A MacOS specific event MacOS(MacOS), } diff --git a/core/src/id.rs b/core/src/id.rs new file mode 100644 index 0000000000..e2af48ee00 --- /dev/null +++ b/core/src/id.rs @@ -0,0 +1,130 @@ +//! Widget and Window IDs. + +use std::borrow; +use std::num::NonZeroU128; +use std::sync::atomic::{self, AtomicU64}; + +static NEXT_ID: AtomicU64 = AtomicU64::new(1); +static NEXT_WINDOW_ID: AtomicU64 = AtomicU64::new(1); + +/// The identifier of a generic widget. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Id(pub Internal); + +impl Id { + /// Creates a custom [`Id`]. + pub fn new(id: impl Into>) -> Self { + Self(Internal::Custom(Self::next(), id.into())) + } + + /// resets the id counter + pub fn reset() { + NEXT_ID.store(1, atomic::Ordering::Relaxed); + } + + fn next() -> u64 { + NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed) + } + + /// Creates a unique [`Id`]. + /// + /// This function produces a different [`Id`] every time it is called. + pub fn unique() -> Self { + let id = Self::next(); + + Self(Internal::Unique(id)) + } +} + +// Not meant to be used directly +#[cfg(feature = "a11y")] +impl From for Id { + fn from(value: u64) -> Self { + Self(Internal::Unique(value)) + } +} + +// Not meant to be used directly +#[cfg(feature = "a11y")] +impl Into for Id { + fn into(self) -> NonZeroU128 { + match &self.0 { + Internal::Unique(id) => NonZeroU128::try_from(*id as u128).unwrap(), + Internal::Custom(id, _) => { + NonZeroU128::try_from(*id as u128).unwrap() + } + // this is a set id, which is not a valid id and will not ever be converted to a NonZeroU128 + // so we panic + Internal::Set(_) => { + panic!("Cannot convert a set id to a NonZeroU128") + } + } + } +} + +impl ToString for Id { + fn to_string(&self) -> String { + match &self.0 { + Internal::Unique(_) => "Undefined".to_string(), + Internal::Custom(_, id) => id.to_string(), + Internal::Set(_) => "Set".to_string(), + } + } +} + +// XXX WIndow IDs are made unique by adding u64::MAX to them +/// get window node id that won't conflict with other node ids for the duration of the program +pub fn window_node_id() -> NonZeroU128 { + std::num::NonZeroU128::try_from( + u64::MAX as u128 + + NEXT_WINDOW_ID.fetch_add(1, atomic::Ordering::Relaxed) as u128, + ) + .unwrap() +} + +// TODO refactor to make panic impossible? +#[derive(Debug, Clone, Eq, Hash)] +/// Internal representation of an [`Id`]. +pub enum Internal { + /// a unique id + Unique(u64), + /// a custom id, which is equal to any [`Id`] with a matching number or string + Custom(u64, borrow::Cow<'static, str>), + /// XXX Do not use this as an id for an accessibility node, it will panic! + /// XXX Only meant to be used for widgets that have multiple accessibility nodes, each with a + /// unique or custom id + /// an Id Set, which is equal to any [`Id`] with a matching number or string + Set(Vec), +} + +impl PartialEq for Internal { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Unique(l0), Self::Unique(r0)) => l0 == r0, + (Self::Custom(l0, l1), Self::Custom(r0, r1)) => { + l0 == r0 || l1 == r1 + } + // allow custom ids to be equal to unique ids + (Self::Unique(l0), Self::Custom(r0, _)) + | (Self::Custom(l0, _), Self::Unique(r0)) => l0 == r0, + (Self::Set(l0), Self::Set(r0)) => l0 == r0, + // allow set ids to just be equal to any of their members + (Self::Set(l0), r) | (r, Self::Set(l0)) => { + l0.iter().any(|l| l == r) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::Id; + + #[test] + fn unique_generates_different_ids() { + let a = Id::unique(); + let b = Id::unique(); + + assert_ne!(a, b); + } +} diff --git a/core/src/lib.rs b/core/src/lib.rs index 89dfb8286e..2d807dcf69 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -25,36 +25,37 @@ #![forbid(unsafe_code, rust_2018_idioms)] #![allow(clippy::inherent_to_string, clippy::type_complexity)] pub mod alignment; +mod background; pub mod clipboard; +mod color; +mod content_fit; +mod element; pub mod event; pub mod font; pub mod gradient; +mod hasher; +#[cfg(not(feature = "a11y"))] +pub mod id; pub mod image; pub mod keyboard; pub mod layout; +mod length; pub mod mouse; pub mod overlay; -pub mod renderer; -pub mod svg; -pub mod text; -pub mod time; -pub mod touch; -pub mod widget; -pub mod window; - -mod background; -mod color; -mod content_fit; -mod element; -mod hasher; -mod length; mod padding; mod pixels; mod point; mod rectangle; +pub mod renderer; mod shell; mod size; +pub mod svg; +pub mod text; +pub mod time; +pub mod touch; mod vector; +pub mod widget; +pub mod window; pub use alignment::Alignment; pub use background::Background; @@ -66,6 +67,8 @@ pub use event::Event; pub use font::Font; pub use gradient::Gradient; pub use hasher::Hasher; +#[cfg(feature = "a11y")] +pub use iced_accessibility::id; pub use layout::Layout; pub use length::Length; pub use overlay::Overlay; diff --git a/core/src/overlay.rs b/core/src/overlay.rs index b9f3c735a8..03d13b269f 100644 --- a/core/src/overlay.rs +++ b/core/src/overlay.rs @@ -9,8 +9,8 @@ use crate::event::{self, Event}; use crate::layout; use crate::mouse; use crate::renderer; -use crate::widget; -use crate::widget::Tree; +use crate::widget::{self, Operation}; +use crate::widget::{OperationOutputWrapper, Tree}; use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size}; /// An interactive component that can be displayed on top of other widgets. @@ -46,7 +46,7 @@ where &mut self, _layout: Layout<'_>, _renderer: &Renderer, - _operation: &mut dyn widget::Operation, + _operation: &mut dyn Operation>, ) { } diff --git a/core/src/overlay/element.rs b/core/src/overlay/element.rs index 237d25d175..38a3de657b 100644 --- a/core/src/overlay/element.rs +++ b/core/src/overlay/element.rs @@ -4,7 +4,7 @@ use crate::event::{self, Event}; use crate::layout; use crate::mouse; use crate::renderer; -use crate::widget; +use crate::widget::{self, Operation, OperationOutputWrapper}; use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size, Vector}; use std::any::Any; @@ -117,7 +117,7 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { self.overlay.operate(layout, renderer, operation); } @@ -159,7 +159,7 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { struct MapOperation<'a, B> { operation: &'a mut dyn widget::Operation, diff --git a/core/src/overlay/group.rs b/core/src/overlay/group.rs index 0c48df34a7..b3b5f79f44 100644 --- a/core/src/overlay/group.rs +++ b/core/src/overlay/group.rs @@ -4,6 +4,8 @@ use crate::mouse; use crate::overlay; use crate::renderer; use crate::widget; +use crate::widget::Operation; +use crate::widget::OperationOutputWrapper; use crate::{Clipboard, Event, Layout, Overlay, Point, Rectangle, Shell, Size}; /// An [`Overlay`] container that displays multiple overlay [`overlay::Element`] @@ -141,7 +143,7 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { operation.container(None, &mut |operation| { self.children.iter_mut().zip(layout.children()).for_each( diff --git a/core/src/widget.rs b/core/src/widget.rs index 769f86591b..1253d258f7 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -3,10 +3,8 @@ pub mod operation; pub mod text; pub mod tree; -mod id; - -pub use id::Id; -pub use operation::Operation; +pub use crate::id::Id; +pub use operation::{Operation, OperationOutputWrapper}; pub use text::Text; pub use tree::Tree; @@ -99,7 +97,7 @@ where _state: &mut Tree, _layout: Layout<'_>, _renderer: &Renderer, - _operation: &mut dyn Operation, + _operation: &mut dyn Operation>, ) { } @@ -142,4 +140,20 @@ where ) -> Option> { None } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget and its children + fn a11y_nodes( + &self, + _layout: Layout<'_>, + _state: &Tree, + _cursor_position: Point, + ) -> iced_accessibility::A11yTree { + iced_accessibility::A11yTree::default() + } + + /// Returns the id of the widget + fn id(&self) -> Option { + None + } } diff --git a/core/src/widget/id.rs b/core/src/widget/id.rs deleted file mode 100644 index ae739bb73d..0000000000 --- a/core/src/widget/id.rs +++ /dev/null @@ -1,43 +0,0 @@ -use std::borrow; -use std::sync::atomic::{self, AtomicUsize}; - -static NEXT_ID: AtomicUsize = AtomicUsize::new(0); - -/// The identifier of a generic widget. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Id(Internal); - -impl Id { - /// Creates a custom [`Id`]. - pub fn new(id: impl Into>) -> Self { - Self(Internal::Custom(id.into())) - } - - /// Creates a unique [`Id`]. - /// - /// This function produces a different [`Id`] every time it is called. - pub fn unique() -> Self { - let id = NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed); - - Self(Internal::Unique(id)) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -enum Internal { - Unique(usize), - Custom(borrow::Cow<'static, str>), -} - -#[cfg(test)] -mod tests { - use super::Id; - - #[test] - fn unique_generates_different_ids() { - let a = Id::unique(); - let b = Id::unique(); - - assert_ne!(a, b); - } -} diff --git a/core/src/widget/operation.rs b/core/src/widget/operation.rs index ad188c364d..0844b01777 100644 --- a/core/src/widget/operation.rs +++ b/core/src/widget/operation.rs @@ -9,9 +9,157 @@ pub use text_input::TextInput; use crate::widget::Id; -use std::any::Any; -use std::fmt; -use std::rc::Rc; +use std::{any::Any, fmt, rc::Rc}; + +#[allow(missing_debug_implementations)] +/// A wrapper around an [`Operation`] that can be used for Application Messages and internally in Iced. +pub enum OperationWrapper { + /// Application Message + Message(Box>), + /// Widget Id + Id(Box>), + /// Wrapper + Wrapper(Box>>), +} + +#[allow(missing_debug_implementations)] +/// A wrapper around an [`Operation`] output that can be used for Application Messages and internally in Iced. +pub enum OperationOutputWrapper { + /// Application Message + Message(M), + /// Widget Id + Id(crate::widget::Id), +} + +impl Operation> for OperationWrapper { + fn container( + &mut self, + id: Option<&Id>, + operate_on_children: &mut dyn FnMut( + &mut dyn Operation>, + ), + ) { + match self { + OperationWrapper::Message(operation) => { + operation.container(id, &mut |operation| { + operate_on_children(&mut MapOperation { operation }); + }); + } + OperationWrapper::Id(operation) => { + operation.container(id, &mut |operation| { + operate_on_children(&mut MapOperation { operation }); + }); + } + OperationWrapper::Wrapper(operation) => { + operation.container(id, operate_on_children); + } + } + } + + fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) { + match self { + OperationWrapper::Message(operation) => { + operation.focusable(state, id); + } + OperationWrapper::Id(operation) => { + operation.focusable(state, id); + } + OperationWrapper::Wrapper(operation) => { + operation.focusable(state, id); + } + } + } + + fn scrollable(&mut self, state: &mut dyn Scrollable, id: Option<&Id>) { + match self { + OperationWrapper::Message(operation) => { + operation.scrollable(state, id); + } + OperationWrapper::Id(operation) => { + operation.scrollable(state, id); + } + OperationWrapper::Wrapper(operation) => { + operation.scrollable(state, id); + } + } + } + + fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { + match self { + OperationWrapper::Message(operation) => { + operation.text_input(state, id); + } + OperationWrapper::Id(operation) => { + operation.text_input(state, id); + } + OperationWrapper::Wrapper(operation) => { + operation.text_input(state, id); + } + } + } + + fn finish(&self) -> Outcome> { + match self { + OperationWrapper::Message(operation) => match operation.finish() { + Outcome::None => Outcome::None, + Outcome::Some(o) => { + Outcome::Some(OperationOutputWrapper::Message(o)) + } + Outcome::Chain(c) => { + Outcome::Chain(Box::new(OperationWrapper::Message(c))) + } + }, + OperationWrapper::Id(operation) => match operation.finish() { + Outcome::None => Outcome::None, + Outcome::Some(id) => { + Outcome::Some(OperationOutputWrapper::Id(id)) + } + Outcome::Chain(c) => { + Outcome::Chain(Box::new(OperationWrapper::Id(c))) + } + }, + OperationWrapper::Wrapper(c) => c.as_ref().finish(), + } + } +} + +#[allow(missing_debug_implementations)] +/// Map Operation +pub struct MapOperation<'a, B> { + /// inner operation + pub(crate) operation: &'a mut dyn Operation, +} + +impl<'a, B> MapOperation<'a, B> { + /// Creates a new [`MapOperation`]. + pub fn new(operation: &'a mut dyn Operation) -> MapOperation<'a, B> { + MapOperation { operation } + } +} + +impl<'a, T, B> Operation for MapOperation<'a, B> { + fn container( + &mut self, + id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + self.operation.container(id, &mut |operation| { + operate_on_children(&mut MapOperation { operation }); + }); + } + + fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) { + self.operation.focusable(state, id); + } + + fn scrollable(&mut self, state: &mut dyn Scrollable, id: Option<&Id>) { + self.operation.scrollable(state, id); + } + + fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { + self.operation.text_input(state, id) + } +} /// A piece of logic that can traverse the widget tree of an application in /// order to query or update some widget state. @@ -35,7 +183,7 @@ pub trait Operation { /// Operates on a widget that has text input. fn text_input(&mut self, _state: &mut dyn TextInput, _id: Option<&Id>) {} - /// Operates on a custom widget with some state. + /// Operates on a custom widget. fn custom(&mut self, _state: &mut dyn Any, _id: Option<&Id>) {} /// Finishes the [`Operation`] and returns its [`Outcome`]. @@ -44,31 +192,6 @@ pub trait Operation { } } -/// The result of an [`Operation`]. -pub enum Outcome { - /// The [`Operation`] produced no result. - None, - - /// The [`Operation`] produced some result. - Some(T), - - /// The [`Operation`] needs to be followed by another [`Operation`]. - Chain(Box>), -} - -impl fmt::Debug for Outcome -where - T: fmt::Debug, -{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::None => write!(f, "Outcome::None"), - Self::Some(output) => write!(f, "Outcome::Some({output:?})"), - Self::Chain(_) => write!(f, "Outcome::Chain(...)"), - } - } -} - /// Maps the output of an [`Operation`] using the given function. pub fn map( operation: Box>, @@ -182,9 +305,34 @@ where } } +/// The result of an [`Operation`]. +pub enum Outcome { + /// The [`Operation`] produced no result. + None, + + /// The [`Operation`] produced some result. + Some(T), + + /// The [`Operation`] needs to be followed by another [`Operation`]. + Chain(Box>), +} + +impl fmt::Debug for Outcome +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::None => write!(f, "Outcome::None"), + Self::Some(output) => write!(f, "Outcome::Some({:?})", output), + Self::Chain(_) => write!(f, "Outcome::Chain(...)"), + } + } +} + /// Produces an [`Operation`] that applies the given [`Operation`] to the /// children of a container with the given [`Id`]. -pub fn scope( +pub fn scoped( target: Id, operation: impl Operation + 'static, ) -> impl Operation { diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index 3193ba84f0..a389c366e5 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -17,6 +17,7 @@ where Renderer: text::Renderer, Renderer::Theme: StyleSheet, { + id: crate::widget::Id, content: Cow<'a, str>, size: Option, width: Length, @@ -35,6 +36,7 @@ where /// Create a new fragment of [`Text`] with the given contents. pub fn new(content: impl Into>) -> Self { Text { + id: crate::widget::Id::unique(), content: content.into(), size: None, font: None, @@ -158,6 +160,42 @@ where self.vertical_alignment, ); } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + layout: Layout<'_>, + _state: &Tree, + _: Point, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::{ + accesskit::{Live, NodeBuilder, Rect, Role}, + A11yTree, + }; + + let Rectangle { + x, + y, + width, + height, + } = layout.bounds(); + let bounds = Rect::new( + x as f64, + y as f64, + (x + width) as f64, + (y + height) as f64, + ); + + let mut node = NodeBuilder::new(Role::StaticText); + + // TODO is the name likely different from the content? + node.set_name(self.content.to_string().into_boxed_str()); + node.set_bounds(bounds); + + // TODO make this configurable + node.set_live(Live::Polite); + A11yTree::leaf(node, self.id.clone()) + } } /// Draws text using the same logic as the [`Text`] widget. @@ -226,6 +264,7 @@ where { fn clone(&self) -> Self { Self { + id: self.id.clone(), content: self.content.clone(), size: self.size, width: self.width, diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index a65f07f26a..b689a4c46b 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -9,6 +9,8 @@ repository = "https://github.com/iced-rs/iced" [features] debug = [] +a11y = ["iced_accessibility", "iced_core/a11y"] + [dependencies] thiserror = "1" @@ -21,3 +23,8 @@ path = "../core" version = "0.6" path = "../futures" features = ["thread-pool"] + +[dependencies.iced_accessibility] +version = "0.1.0" +path = "../accessibility" +optional = true diff --git a/runtime/src/command.rs b/runtime/src/command.rs index cd4c51ff05..f641353f77 100644 --- a/runtime/src/command.rs +++ b/runtime/src/command.rs @@ -1,5 +1,6 @@ //! Run asynchronous actions. mod action; +pub mod platform_specific; pub use action::Action; diff --git a/runtime/src/command/action.rs b/runtime/src/command/action.rs index 6c74f0efb2..066d0a51c1 100644 --- a/runtime/src/command/action.rs +++ b/runtime/src/command/action.rs @@ -38,6 +38,9 @@ pub enum Action { /// The message to produce when the font has been loaded. tagger: Box) -> T>, }, + + /// Run a platform specific action + PlatformSpecific(crate::command::platform_specific::Action), } impl Action { @@ -66,6 +69,9 @@ impl Action { bytes, tagger: Box::new(move |result| f(tagger(result))), }, + Self::PlatformSpecific(action) => { + Action::PlatformSpecific(action.map(f)) + } } } } @@ -81,6 +87,9 @@ impl fmt::Debug for Action { Self::System(action) => write!(f, "Action::System({action:?})"), Self::Widget(_action) => write!(f, "Action::Widget"), Self::LoadFont { .. } => write!(f, "Action::LoadFont"), + Self::PlatformSpecific(action) => { + write!(f, "Action::PlatformSpecific({:?})", action) + } } } } diff --git a/runtime/src/command/platform_specific/mod.rs b/runtime/src/command/platform_specific/mod.rs new file mode 100644 index 0000000000..7c5f6605ba --- /dev/null +++ b/runtime/src/command/platform_specific/mod.rs @@ -0,0 +1,33 @@ +use std::{fmt, marker::PhantomData}; + +use iced_futures::MaybeSend; + +/// Platform specific actions defined for wayland +pub enum Action { + /// phantom data variant in case the platform has not specific actions implemented + Phantom(PhantomData), +} + +impl Action { + /// Maps the output of an [`Action`] using the given function. + pub fn map( + self, + _f: impl Fn(T) -> A + 'static + MaybeSend + Sync, + ) -> Action + where + T: 'static, + A: 'static, + { + match self { + Action::Phantom(_) => unimplemented!(), + } + } +} + +impl fmt::Debug for Action { + fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Action::Phantom(_) => unimplemented!(), + } + } +} diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index c29de7db1a..e7eea6364e 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -1,4 +1,6 @@ //! Implement your own event loop to drive a user interface. +use iced_core::widget::{Operation, OperationOutputWrapper}; + use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; @@ -513,7 +515,7 @@ where pub fn operate( &mut self, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { self.root.as_widget().operate( &mut self.state, @@ -551,6 +553,19 @@ where pub fn into_cache(self) -> Cache { Cache { state: self.state } } + + /// get a11y nodes + #[cfg(feature = "a11y")] + pub fn a11y_nodes( + &self, + cursor_position: Point, + ) -> iced_accessibility::A11yTree { + self.root.as_widget().a11y_nodes( + Layout::new(&self.base), + &self.state, + cursor_position, + ) + } } /// Reusable data of a specific [`UserInterface`]. diff --git a/widget/Cargo.toml b/widget/Cargo.toml index 40e4db374d..09fa0024b3 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -9,6 +9,7 @@ image = ["iced_renderer/image"] svg = ["iced_renderer/svg"] canvas = ["iced_renderer/geometry"] qr_code = ["canvas", "qrcode"] +a11y = ["iced_accessibility"] [dependencies] unicode-segmentation = "1.6" @@ -35,3 +36,8 @@ optional = true version = "0.12" optional = true default-features = false + +[dependencies.iced_accessibility] +version = "0.1.0" +path = "../accessibility" +optional = true diff --git a/widget/src/button.rs b/widget/src/button.rs index 7eee69cbae..804dc5a33c 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -1,6 +1,10 @@ //! Allow your users to perform actions by pressing a button. //! //! A [`Button`] has some local [`State`]. +use iced_runtime::core::widget::Id; +use iced_runtime::{keyboard, Command}; +use std::borrow::Cow; + use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; @@ -14,6 +18,7 @@ use crate::core::{ Rectangle, Shell, Vector, Widget, }; +use iced_renderer::core::widget::{operation, OperationOutputWrapper}; pub use iced_style::button::{Appearance, StyleSheet}; /// A generic widget that produces a message when pressed. @@ -56,6 +61,13 @@ where Renderer: crate::core::Renderer, Renderer::Theme: StyleSheet, { + id: Id, + #[cfg(feature = "a11y")] + name: Option>, + #[cfg(feature = "a11y")] + description: Option>, + #[cfg(feature = "a11y")] + label: Option>, content: Element<'a, Message, Renderer>, on_press: Option, width: Length, @@ -72,6 +84,13 @@ where /// Creates a new [`Button`] with the given content. pub fn new(content: impl Into>) -> Self { Button { + id: Id::unique(), + #[cfg(feature = "a11y")] + name: None, + #[cfg(feature = "a11y")] + description: None, + #[cfg(feature = "a11y")] + label: None, content: content.into(), on_press: None, width: Length::Shrink, @@ -115,6 +134,47 @@ where self.style = style; self } + + /// Sets the [`Id`] of the [`Button`]. + pub fn id(mut self, id: Id) -> Self { + self.id = id; + self + } + + #[cfg(feature = "a11y")] + /// Sets the name of the [`Button`]. + pub fn name(mut self, name: impl Into>) -> Self { + self.name = Some(name.into()); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description_widget( + mut self, + description: &T, + ) -> Self { + self.description = Some(iced_accessibility::Description::Id( + description.description(), + )); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description(mut self, description: impl Into>) -> Self { + self.description = + Some(iced_accessibility::Description::Text(description.into())); + self + } + + #[cfg(feature = "a11y")] + /// Sets the label of the [`Button`]. + pub fn label(mut self, label: &dyn iced_accessibility::Labels) -> Self { + self.label = + Some(label.label().into_iter().map(|l| l.into()).collect()); + self + } } impl<'a, Message, Renderer> Widget @@ -170,7 +230,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation>, ) { operation.container(None, &mut |operation| { self.content.as_widget().operate( @@ -205,6 +265,7 @@ where } update( + self.id.clone(), event, layout, cursor_position, @@ -273,6 +334,87 @@ where renderer, ) } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + p: Point, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::{ + accesskit::{ + Action, DefaultActionVerb, NodeBuilder, NodeId, Rect, Role, + }, + A11yNode, A11yTree, + }; + + let child_layout = layout.children().next().unwrap(); + let child_tree = &state.children[0]; + let child_tree = + self.content + .as_widget() + .a11y_nodes(child_layout, &child_tree, p); + + let Rectangle { + x, + y, + width, + height, + } = layout.bounds(); + let bounds = Rect::new( + x as f64, + y as f64, + (x + width) as f64, + (y + height) as f64, + ); + let is_hovered = state.state.downcast_ref::().is_hovered; + + let mut node = NodeBuilder::new(Role::Button); + node.add_action(Action::Focus); + node.add_action(Action::Default); + node.set_bounds(bounds); + if let Some(name) = self.name.as_ref() { + node.set_name(name.clone()); + } + match self.description.as_ref() { + Some(iced_accessibility::Description::Id(id)) => { + node.set_described_by( + id.iter() + .cloned() + .map(|id| NodeId::from(id)) + .collect::>(), + ); + } + Some(iced_accessibility::Description::Text(text)) => { + node.set_description(text.clone()); + } + None => {} + } + + if let Some(label) = self.label.as_ref() { + node.set_labelled_by(label.clone()); + } + + if self.on_press.is_none() { + node.set_disabled() + } + if is_hovered { + node.set_hovered() + } + node.set_default_action_verb(DefaultActionVerb::Click); + + A11yTree::node_with_child_tree( + A11yNode::new(node, self.id.clone()), + child_tree, + ) + } + + // TODO each accessible widget really should always have Id, so maybe this doesn't need to be an option + fn id(&self) -> Option { + Some(self.id.clone()) + } } impl<'a, Message, Renderer> From> @@ -290,7 +432,9 @@ where /// The local state of a [`Button`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct State { + is_hovered: bool, is_pressed: bool, + is_focused: bool, } impl State { @@ -298,11 +442,32 @@ impl State { pub fn new() -> State { State::default() } + + /// Returns whether the [`Button`] is currently focused or not. + pub fn is_focused(&self) -> bool { + self.is_focused + } + + /// Returns whether the [`Button`] is currently hovered or not. + pub fn is_hovered(&self) -> bool { + self.is_hovered + } + + /// Focuses the [`Button`]. + pub fn focus(&mut self) { + self.is_focused = true; + } + + /// Unfocuses the [`Button`]. + pub fn unfocus(&mut self) { + self.is_focused = false; + } } /// Processes the given [`Event`] and updates the [`State`] of a [`Button`] /// accordingly. pub fn update<'a, Message: Clone>( + id: Id, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -343,9 +508,37 @@ pub fn update<'a, Message: Clone>( } } } + #[cfg(feature = "a11y")] + Event::A11y( + event_id, + iced_accessibility::accesskit::ActionRequest { action, .. }, + ) => { + let state = state(); + if let Some(Some(on_press)) = (id == event_id + && matches!( + action, + iced_accessibility::accesskit::Action::Default + )) + .then(|| on_press.clone()) + { + state.is_pressed = false; + shell.publish(on_press); + } + return event::Status::Captured; + } + Event::Keyboard(keyboard::Event::KeyPressed { key_code, .. }) => { + if let Some(on_press) = on_press.clone() { + let state = state(); + if state.is_focused && key_code == keyboard::KeyCode::Enter { + state.is_pressed = true; + shell.publish(on_press); + return event::Status::Captured; + } + } + } Event::Touch(touch::Event::FingerLost { .. }) => { let state = state(); - + state.is_hovered = false; state.is_pressed = false; } _ => {} @@ -453,3 +646,22 @@ pub fn mouse_interaction( mouse::Interaction::default() } } + +/// Produces a [`Command`] that focuses the [`Button`] with the given [`Id`]. +pub fn focus(id: Id) -> Command { + Command::widget(operation::focusable::focus(id)) +} + +impl operation::Focusable for State { + fn is_focused(&self) -> bool { + State::is_focused(self) + } + + fn focus(&mut self) { + State::focus(self) + } + + fn unfocus(&mut self) { + State::unfocus(self) + } +} diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index 6505cfdd95..27ae7bf530 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -1,4 +1,7 @@ //! Show toggle controls using checkboxes. +use iced_runtime::core::widget::Id; +use std::borrow::Cow; + use crate::core::alignment; use crate::core::event::{self, Event}; use crate::core::layout; @@ -39,6 +42,12 @@ where Renderer: text::Renderer, Renderer::Theme: StyleSheet + crate::text::StyleSheet, { + id: Id, + label_id: Id, + #[cfg(feature = "a11y")] + name: Option>, + #[cfg(feature = "a11y")] + description: Option>, is_checked: bool, on_toggle: Box Message + 'a>, label: String, @@ -75,6 +84,12 @@ where F: 'a + Fn(bool) -> Message, { Checkbox { + id: Id::unique(), + label_id: Id::unique(), + #[cfg(feature = "a11y")] + name: None, + #[cfg(feature = "a11y")] + description: None, is_checked, on_toggle: Box::new(f), label: label.into(), @@ -138,6 +153,33 @@ where self.style = style.into(); self } + + #[cfg(feature = "a11y")] + /// Sets the name of the [`Button`]. + pub fn name(mut self, name: impl Into>) -> Self { + self.name = Some(name.into()); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description_widget( + mut self, + description: &T, + ) -> Self { + self.description = Some(iced_accessibility::Description::Id( + description.description(), + )); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description(mut self, description: impl Into>) -> Self { + self.description = + Some(iced_accessibility::Description::Text(description.into())); + self + } } impl<'a, Message, Renderer> Widget @@ -295,6 +337,88 @@ where ); } } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + _state: &Tree, + cursor_position: Point, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::{ + accesskit::{ + Action, CheckedState, NodeBuilder, NodeId, Rect, Role, + }, + A11yNode, A11yTree, + }; + + let bounds = layout.bounds(); + let is_hovered = bounds.contains(cursor_position); + let Rectangle { + x, + y, + width, + height, + } = bounds; + + let bounds = Rect::new( + x as f64, + y as f64, + (x + width) as f64, + (y + height) as f64, + ); + + let mut node = NodeBuilder::new(Role::CheckBox); + node.add_action(Action::Focus); + node.add_action(Action::Default); + node.set_bounds(bounds); + if let Some(name) = self.name.as_ref() { + node.set_name(name.clone()); + } + match self.description.as_ref() { + Some(iced_accessibility::Description::Id(id)) => { + node.set_described_by( + id.iter() + .cloned() + .map(|id| NodeId::from(id)) + .collect::>(), + ); + } + Some(iced_accessibility::Description::Text(text)) => { + node.set_description(text.clone()); + } + None => {} + } + node.set_checked_state(if self.is_checked { + CheckedState::True + } else { + CheckedState::False + }); + if is_hovered { + node.set_hovered(); + } + node.add_action(Action::Default); + let mut label_node = NodeBuilder::new(Role::StaticText); + + label_node.set_name(self.label.clone()); + // TODO proper label bounds + label_node.set_bounds(bounds); + + A11yTree::node_with_child_tree( + A11yNode::new(node, self.id.clone()), + A11yTree::leaf(label_node, self.label_id.clone()), + ) + } + + fn id(&self) -> Option { + use iced_accessibility::Internal; + + Some(Id(Internal::Set(vec![ + self.id.0.clone(), + self.label_id.0.clone(), + ]))) + } } impl<'a, Message, Renderer> From> diff --git a/widget/src/column.rs b/widget/src/column.rs index 8f363ec65f..25c90cf486 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -1,4 +1,6 @@ //! Distribute content vertically. +use iced_renderer::core::widget::OperationOutputWrapper; + use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; @@ -146,7 +148,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation>, ) { operation.container(None, &mut |operation| { self.children @@ -250,6 +252,26 @@ where ) -> Option> { overlay::from_children(&mut self.children, tree, layout, renderer) } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + p: Point, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::A11yTree; + A11yTree::join( + self.children + .iter() + .zip(layout.children()) + .zip(state.children.iter()) + .map(|((c, c_layout), state)| { + c.as_widget().a11y_nodes(c_layout, state, p) + }), + ) + } } impl<'a, Message, Renderer> From> diff --git a/widget/src/container.rs b/widget/src/container.rs index 9d932772d1..5a738f47b9 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -11,6 +11,7 @@ use crate::core::{ Point, Rectangle, Shell, Widget, }; +use iced_renderer::core::widget::OperationOutputWrapper; pub use iced_style::container::{Appearance, StyleSheet}; /// An element decorating some content. @@ -176,7 +177,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation>, ) { operation.container( self.id.as_ref().map(|id| &id.0), @@ -270,6 +271,19 @@ where renderer, ) } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + p: Point, + ) -> iced_accessibility::A11yTree { + let c_layout = layout.children().next().unwrap(); + let c_state = &state.children[0]; + self.content.as_widget().a11y_nodes(c_layout, c_state, p) + } } impl<'a, Message, Renderer> From> diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 336ac4ee0f..31dc20b91f 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -311,7 +311,9 @@ where /// [`Image`]: widget::Image #[cfg(feature = "image")] #[cfg_attr(docsrs, doc(cfg(feature = "image")))] -pub fn image(handle: impl Into) -> crate::Image { +pub fn image<'a, Handle>( + handle: impl Into, +) -> crate::Image<'a, Handle> { crate::Image::new(handle.into()) } @@ -321,9 +323,9 @@ pub fn image(handle: impl Into) -> crate::Image { /// [`Handle`]: widget::svg::Handle #[cfg(feature = "svg")] #[cfg_attr(docsrs, doc(cfg(feature = "svg")))] -pub fn svg( +pub fn svg<'a, Renderer>( handle: impl Into, -) -> crate::Svg +) -> crate::Svg<'a, Renderer> where Renderer: core::svg::Renderer, Renderer::Theme: crate::svg::StyleSheet, diff --git a/widget/src/image.rs b/widget/src/image.rs index abcb6ef22b..a48463c722 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -1,5 +1,6 @@ //! Display images in your user interface. pub mod viewer; +use iced_runtime::core::widget::Id; pub use viewer::Viewer; use crate::core::image; @@ -10,6 +11,7 @@ use crate::core::{ ContentFit, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget, }; +use std::borrow::Cow; use std::hash::Hash; pub use image::Handle; @@ -31,21 +33,37 @@ pub fn viewer(handle: Handle) -> Viewer { /// /// #[derive(Debug)] -pub struct Image { +pub struct Image<'a, Handle> { + id: Id, + #[cfg(feature = "a11y")] + name: Option>, + #[cfg(feature = "a11y")] + description: Option>, + #[cfg(feature = "a11y")] + label: Option>, handle: Handle, width: Length, height: Length, content_fit: ContentFit, + phantom_data: std::marker::PhantomData<&'a ()>, } -impl Image { +impl<'a, Handle> Image<'a, Handle> { /// Creates a new [`Image`] with the given path. pub fn new>(handle: T) -> Self { Image { + id: Id::unique(), + #[cfg(feature = "a11y")] + name: None, + #[cfg(feature = "a11y")] + description: None, + #[cfg(feature = "a11y")] + label: None, handle: handle.into(), width: Length::Shrink, height: Length::Shrink, content_fit: ContentFit::Contain, + phantom_data: std::marker::PhantomData, } } @@ -70,6 +88,41 @@ impl Image { ..self } } + + #[cfg(feature = "a11y")] + /// Sets the name of the [`Button`]. + pub fn name(mut self, name: impl Into>) -> Self { + self.name = Some(name.into()); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description_widget( + mut self, + description: &T, + ) -> Self { + self.description = Some(iced_accessibility::Description::Id( + description.description(), + )); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description(mut self, description: impl Into>) -> Self { + self.description = + Some(iced_accessibility::Description::Text(description.into())); + self + } + + #[cfg(feature = "a11y")] + /// Sets the label of the [`Button`]. + pub fn label(mut self, label: &dyn iced_accessibility::Labels) -> Self { + self.label = + Some(label.label().into_iter().map(|l| l.into()).collect()); + self + } } /// Computes the layout of an [`Image`]. @@ -151,7 +204,8 @@ pub fn draw( } } -impl Widget for Image +impl<'a, Message, Renderer, Handle> Widget + for Image<'a, Handle> where Renderer: image::Renderer, Handle: Clone + Hash, @@ -191,15 +245,71 @@ where ) { draw(renderer, layout, &self.handle, self.content_fit) } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + layout: Layout<'_>, + _state: &Tree, + _cursor_position: Point, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::{ + accesskit::{NodeBuilder, NodeId, Rect, Role}, + A11yTree, + }; + + let bounds = layout.bounds(); + let Rectangle { + x, + y, + width, + height, + } = bounds; + let bounds = Rect::new( + x as f64, + y as f64, + (x + width) as f64, + (y + height) as f64, + ); + let mut node = NodeBuilder::new(Role::Image); + node.set_bounds(bounds); + if let Some(name) = self.name.as_ref() { + node.set_name(name.clone()); + } + match self.description.as_ref() { + Some(iced_accessibility::Description::Id(id)) => { + node.set_described_by( + id.iter() + .cloned() + .map(|id| NodeId::from(id)) + .collect::>(), + ); + } + Some(iced_accessibility::Description::Text(text)) => { + node.set_description(text.clone()); + } + None => {} + } + + if let Some(label) = self.label.as_ref() { + node.set_labelled_by(label.clone()); + } + + A11yTree::leaf(node, self.id.clone()) + } + + fn id(&self) -> Option { + Some(self.id.clone()) + } } -impl<'a, Message, Renderer, Handle> From> +impl<'a, Message, Renderer, Handle> From> for Element<'a, Message, Renderer> where Renderer: image::Renderer, Handle: Clone + Hash + 'a, { - fn from(image: Image) -> Element<'a, Message, Renderer> { + fn from(image: Image<'a, Handle>) -> Element<'a, Message, Renderer> { Element::new(image) } } diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index b08ed8cb9d..63e410f8ea 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -5,6 +5,7 @@ pub mod component; pub mod responsive; pub use component::Component; +use iced_renderer::core::widget::{Operation, OperationOutputWrapper}; pub use responsive::Responsive; mod cache; @@ -15,7 +16,7 @@ use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; -use crate::core::widget::{self, Widget}; +use crate::core::widget::Widget; use crate::core::Element; use crate::core::{ self, Clipboard, Hasher, Length, Point, Rectangle, Shell, Size, @@ -161,7 +162,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { self.with_element(|element| { element.as_widget().operate( diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index 49ae68aff1..1a48b449ac 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -10,6 +10,7 @@ use crate::core::{ self, Clipboard, Element, Length, Point, Rectangle, Shell, Size, Widget, }; +use iced_renderer::core::widget::{Operation, OperationOutputWrapper}; use ouroboros::self_referencing; use std::cell::RefCell; use std::marker::PhantomData; @@ -54,7 +55,7 @@ pub trait Component { fn operate( &self, _state: &mut Self::State, - _operation: &mut dyn widget::Operation, + _operation: &mut dyn Operation>, ) { } } @@ -153,7 +154,7 @@ where fn rebuild_element_with_operation( &self, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { let heads = self.state.borrow_mut().take().unwrap().into_heads(); @@ -325,7 +326,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { self.rebuild_element_with_operation(operation); diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index 7b2fc37c4e..adc3937a98 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -3,13 +3,13 @@ use crate::core::layout::{self, Layout}; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; -use crate::core::widget; use crate::core::widget::tree::{self, Tree}; use crate::core::{ self, Clipboard, Element, Length, Point, Rectangle, Shell, Size, Widget, }; use crate::horizontal_space; +use iced_renderer::core::widget::{Operation, OperationOutputWrapper}; use ouroboros::self_referencing; use std::cell::{RefCell, RefMut}; use std::marker::PhantomData; @@ -148,7 +148,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { let state = tree.state.downcast_mut::(); let mut content = self.content.borrow_mut(); diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index 0232c4947c..d49c467a05 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -1,5 +1,7 @@ //! A container for capturing mouse events. +use iced_renderer::core::widget::OperationOutputWrapper; + use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; @@ -131,7 +133,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation>, ) { self.content.as_widget().operate( &mut tree.children[0], diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 67145e8e42..3e12ff85b1 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -24,6 +24,7 @@ pub use configuration::Configuration; pub use content::Content; pub use direction::Direction; pub use draggable::Draggable; +use iced_renderer::core::widget::{Operation, OperationOutputWrapper}; pub use node::Node; pub use pane::Pane; pub use split::Split; @@ -39,7 +40,6 @@ use crate::core::mouse; use crate::core::overlay::{self, Group}; use crate::core::renderer; use crate::core::touch; -use crate::core::widget; use crate::core::widget::tree::{self, Tree}; use crate::core::{ Clipboard, Color, Element, Layout, Length, Pixels, Point, Rectangle, Shell, @@ -295,7 +295,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { operation.container(None, &mut |operation| { self.contents diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs index 035ef05b4e..a5ddaa3513 100644 --- a/widget/src/pane_grid/content.rs +++ b/widget/src/pane_grid/content.rs @@ -1,10 +1,12 @@ +use iced_renderer::core::widget::{Operation, OperationOutputWrapper}; + use crate::container; use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; -use crate::core::widget::{self, Tree}; +use crate::core::widget::Tree; use crate::core::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size}; use crate::pane_grid::{Draggable, TitleBar}; @@ -188,7 +190,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { let body_layout = if let Some(title_bar) = &self.title_bar { let mut children = layout.children(); diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs index 2129937be8..c1f7d4e62c 100644 --- a/widget/src/pane_grid/title_bar.rs +++ b/widget/src/pane_grid/title_bar.rs @@ -1,10 +1,12 @@ +use iced_renderer::core::widget::{Operation, OperationOutputWrapper}; + use crate::container; use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; -use crate::core::widget::{self, Tree}; +use crate::core::widget::Tree; use crate::core::{ Clipboard, Element, Layout, Padding, Point, Rectangle, Shell, Size, }; @@ -259,7 +261,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { let mut children = layout.children(); let padded = children.next().unwrap(); diff --git a/widget/src/row.rs b/widget/src/row.rs index 3ce363f8d6..dab6eb2f32 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -1,4 +1,6 @@ //! Distribute content horizontally. +use iced_renderer::core::widget::OperationOutputWrapper; + use crate::core::event::{self, Event}; use crate::core::layout::{self, Layout}; use crate::core::mouse; @@ -135,7 +137,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation>, ) { operation.container(None, &mut |operation| { self.children @@ -239,6 +241,26 @@ where ) -> Option> { overlay::from_children(&mut self.children, tree, layout, renderer) } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + p: Point, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::A11yTree; + A11yTree::join( + self.children + .iter() + .zip(layout.children()) + .zip(state.children.iter()) + .map(|((c, c_layout), state)| { + c.as_widget().a11y_nodes(c_layout, state, p) + }), + ) + } } impl<'a, Message, Renderer> From> diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 161ae664e2..cdae39ce54 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -1,4 +1,7 @@ //! Navigate an endless amount of content with a scrollbar. +use iced_runtime::core::widget::Id; +use std::borrow::Cow; + use crate::core::event::{self, Event}; use crate::core::keyboard; use crate::core::layout; @@ -6,7 +9,6 @@ use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; use crate::core::touch; -use crate::core::widget; use crate::core::widget::operation::{self, Operation}; use crate::core::widget::tree::{self, Tree}; use crate::core::{ @@ -16,6 +18,7 @@ use crate::core::{ use crate::runtime::Command; pub use crate::style::scrollable::{Scrollbar, Scroller, StyleSheet}; +use iced_renderer::core::widget::OperationOutputWrapper; pub use operation::scrollable::RelativeOffset; /// A widget that can vertically display an infinite amount of content with a @@ -26,7 +29,14 @@ where Renderer: crate::core::Renderer, Renderer::Theme: StyleSheet, { - id: Option, + id: Id, + scrollbar_id: Id, + #[cfg(feature = "a11y")] + name: Option>, + #[cfg(feature = "a11y")] + description: Option>, + #[cfg(feature = "a11y")] + label: Option>, width: Length, height: Length, vertical: Properties, @@ -44,7 +54,14 @@ where /// Creates a new [`Scrollable`]. pub fn new(content: impl Into>) -> Self { Scrollable { - id: None, + id: Id::unique(), + scrollbar_id: Id::unique(), + #[cfg(feature = "a11y")] + name: None, + #[cfg(feature = "a11y")] + description: None, + #[cfg(feature = "a11y")] + label: None, width: Length::Shrink, height: Length::Shrink, vertical: Properties::default(), @@ -57,7 +74,7 @@ where /// Sets the [`Id`] of the [`Scrollable`]. pub fn id(mut self, id: Id) -> Self { - self.id = Some(id); + self.id = id; self } @@ -105,6 +122,41 @@ where self.style = style.into(); self } + + #[cfg(feature = "a11y")] + /// Sets the name of the [`Button`]. + pub fn name(mut self, name: impl Into>) -> Self { + self.name = Some(name.into()); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description_widget( + mut self, + description: &impl iced_accessibility::Describes, + ) -> Self { + self.description = Some(iced_accessibility::Description::Id( + description.description(), + )); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description(mut self, description: impl Into>) -> Self { + self.description = + Some(iced_accessibility::Description::Text(description.into())); + self + } + + #[cfg(feature = "a11y")] + /// Sets the label of the [`Button`]. + pub fn label(mut self, label: &dyn iced_accessibility::Labels) -> Self { + self.label = + Some(label.label().into_iter().map(|l| l.into()).collect()); + self + } } /// Properties of a scrollbar within a [`Scrollable`]. @@ -204,23 +256,20 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation>, ) { let state = tree.state.downcast_mut::(); - operation.scrollable(state, self.id.as_ref().map(|id| &id.0)); + operation.scrollable(state, Some(&self.id)); - operation.container( - self.id.as_ref().map(|id| &id.0), - &mut |operation| { - self.content.as_widget().operate( - &mut tree.children[0], - layout.children().next().unwrap(), - renderer, - operation, - ); - }, - ); + operation.container(Some(&self.id), &mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + operation, + ); + }); } fn on_event( @@ -341,6 +390,140 @@ where overlay.translate(Vector::new(-offset.x, -offset.y)) }) } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + cursor_position: Point, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::{ + accesskit::{NodeBuilder, NodeId, Rect, Role}, + A11yId, A11yNode, A11yTree, + }; + + let child_layout = layout.children().next().unwrap(); + let child_tree = &state.children[0]; + let child_tree = self.content.as_widget().a11y_nodes( + child_layout, + &child_tree, + cursor_position, + ); + + let window = layout.bounds(); + let is_hovered = window.contains(cursor_position); + let Rectangle { + x, + y, + width, + height, + } = window; + let bounds = Rect::new( + x as f64, + y as f64, + (x + width) as f64, + (y + height) as f64, + ); + let mut node = NodeBuilder::new(Role::ScrollView); + node.set_bounds(bounds); + if let Some(name) = self.name.as_ref() { + node.set_name(name.clone()); + } + match self.description.as_ref() { + Some(iced_accessibility::Description::Id(id)) => { + node.set_described_by( + id.iter() + .cloned() + .map(|id| NodeId::from(id)) + .collect::>(), + ); + } + Some(iced_accessibility::Description::Text(text)) => { + node.set_description(text.clone()); + } + None => {} + } + + if is_hovered { + node.set_hovered(); + } + + if let Some(label) = self.label.as_ref() { + node.set_labelled_by(label.clone()); + } + + let content = layout.children().next().unwrap(); + let content_bounds = content.bounds(); + + let mut scrollbar_node = NodeBuilder::new(Role::ScrollBar); + if matches!(state.state, tree::State::Some(_)) { + let state = state.state.downcast_ref::(); + let scrollbars = Scrollbars::new( + state, + &self.vertical, + self.horizontal.as_ref(), + window, + content_bounds, + ); + for (window, content, offset, scrollbar) in scrollbars + .x + .iter() + .map(|s| { + (window.width, content_bounds.width, state.offset_x, s) + }) + .chain(scrollbars.y.iter().map(|s| { + (window.height, content_bounds.height, state.offset_y, s) + })) + { + let scrollbar_bounds = scrollbar.total_bounds; + let is_hovered = scrollbar_bounds.contains(cursor_position); + let Rectangle { + x, + y, + width, + height, + } = scrollbar_bounds; + let bounds = Rect::new( + x as f64, + y as f64, + (x + width) as f64, + (y + height) as f64, + ); + scrollbar_node.set_bounds(bounds); + if is_hovered { + scrollbar_node.set_hovered(); + } + scrollbar_node + .set_controls(vec![A11yId::Widget(self.id.clone()).into()]); + scrollbar_node.set_numeric_value( + 100.0 * offset.absolute(window, content) as f64 + / scrollbar_bounds.height as f64, + ); + } + } + + let child_tree = A11yTree::join( + [ + child_tree, + A11yTree::leaf(scrollbar_node, self.scrollbar_id.clone()), + ] + .into_iter(), + ); + A11yTree::node_with_child_tree( + A11yNode::new(node, self.id.clone()), + child_tree, + ) + } + + fn id(&self) -> Option { + use iced_accessibility::Internal; + + Some(Id(Internal::Set(vec![ + self.id.0.clone(), + self.scrollbar_id.0.clone(), + ]))) + } } impl<'a, Message, Renderer> From> @@ -357,37 +540,13 @@ where } } -/// The identifier of a [`Scrollable`]. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Id(widget::Id); - -impl Id { - /// Creates a custom [`Id`]. - pub fn new(id: impl Into>) -> Self { - Self(widget::Id::new(id)) - } - - /// Creates a unique [`Id`]. - /// - /// This function produces a different [`Id`] every time it is called. - pub fn unique() -> Self { - Self(widget::Id::unique()) - } -} - -impl From for widget::Id { - fn from(id: Id) -> Self { - id.0 - } -} - /// Produces a [`Command`] that snaps the [`Scrollable`] with the given [`Id`] /// to the provided `percentage` along the x & y axis. pub fn snap_to( id: Id, offset: RelativeOffset, ) -> Command { - Command::widget(operation::scrollable::snap_to(id.0, offset)) + Command::widget(operation::scrollable::snap_to(id, offset)) } /// Computes the layout of a [`Scrollable`]. diff --git a/widget/src/slider.rs b/widget/src/slider.rs index 5a884e213f..94036a8df2 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -7,11 +7,13 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; +use crate::core::widget::Id; use crate::core::{ Clipboard, Color, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size, Widget, }; +use std::borrow::Cow; use std::ops::RangeInclusive; pub use iced_style::slider::{ @@ -48,6 +50,13 @@ where Renderer: crate::core::Renderer, Renderer::Theme: StyleSheet, { + id: Id, + #[cfg(feature = "a11y")] + name: Option>, + #[cfg(feature = "a11y")] + description: Option>, + #[cfg(feature = "a11y")] + label: Option>, range: RangeInclusive, step: T, value: T, @@ -93,6 +102,13 @@ where }; Slider { + id: Id::unique(), + #[cfg(feature = "a11y")] + name: None, + #[cfg(feature = "a11y")] + description: None, + #[cfg(feature = "a11y")] + label: None, value, range, step: T::from(1), @@ -141,6 +157,41 @@ where self.step = step; self } + + #[cfg(feature = "a11y")] + /// Sets the name of the [`Button`]. + pub fn name(mut self, name: impl Into>) -> Self { + self.name = Some(name.into()); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description_widget( + mut self, + description: &impl iced_accessibility::Describes, + ) -> Self { + self.description = Some(iced_accessibility::Description::Id( + description.description(), + )); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description(mut self, description: impl Into>) -> Self { + self.description = + Some(iced_accessibility::Description::Text(description.into())); + self + } + + #[cfg(feature = "a11y")] + /// Sets the label of the [`Button`]. + pub fn label(mut self, label: &dyn iced_accessibility::Labels) -> Self { + self.label = + Some(label.label().into_iter().map(|l| l.into()).collect()); + self + } } impl<'a, T, Message, Renderer> Widget @@ -238,6 +289,83 @@ where tree.state.downcast_ref::(), ) } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + layout: Layout<'_>, + _state: &Tree, + cursor_position: Point, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::{ + accesskit::{NodeBuilder, NodeId, Rect, Role}, + A11yTree, + }; + + let bounds = layout.bounds(); + let is_hovered = bounds.contains(cursor_position); + let Rectangle { + x, + y, + width, + height, + } = bounds; + let bounds = Rect::new( + x as f64, + y as f64, + (x + width) as f64, + (y + height) as f64, + ); + let mut node = NodeBuilder::new(Role::Slider); + node.set_bounds(bounds); + if let Some(name) = self.name.as_ref() { + node.set_name(name.clone()); + } + match self.description.as_ref() { + Some(iced_accessibility::Description::Id(id)) => { + node.set_described_by( + id.iter() + .cloned() + .map(|id| NodeId::from(id)) + .collect::>(), + ); + } + Some(iced_accessibility::Description::Text(text)) => { + node.set_description(text.clone()); + } + None => {} + } + + if is_hovered { + node.set_hovered(); + } + + if let Some(label) = self.label.as_ref() { + node.set_labelled_by(label.clone()); + } + + if let Ok(min) = self.range.start().clone().try_into() { + node.set_min_numeric_value(min); + } + if let Ok(max) = self.range.end().clone().try_into() { + node.set_max_numeric_value(max); + } + if let Ok(value) = self.value.clone().try_into() { + node.set_numeric_value(value); + } + if let Ok(step) = self.step.clone().try_into() { + node.set_numeric_value_step(step); + } + + // TODO: This could be a setting on the slider + node.set_live(iced_accessibility::accesskit::Live::Polite); + + A11yTree::leaf(node, self.id.clone()) + } + + fn id(&self) -> Option { + Some(self.id.clone()) + } } impl<'a, T, Message, Renderer> From> diff --git a/widget/src/svg.rs b/widget/src/svg.rs index 89017fcff3..e5f93496e0 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -1,4 +1,6 @@ //! Display vector graphics in your application. +use iced_runtime::core::widget::Id; + use crate::core::layout; use crate::core::renderer; use crate::core::svg; @@ -7,6 +9,7 @@ use crate::core::{ ContentFit, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget, }; +use std::borrow::Cow; use std::path::PathBuf; pub use crate::style::svg::{Appearance, StyleSheet}; @@ -19,11 +22,18 @@ pub use svg::Handle; /// [`Svg`] images can have a considerable rendering cost when resized, /// specially when they are complex. #[allow(missing_debug_implementations)] -pub struct Svg +pub struct Svg<'a, Renderer = crate::Renderer> where Renderer: svg::Renderer, Renderer::Theme: StyleSheet, { + id: Id, + #[cfg(feature = "a11y")] + name: Option>, + #[cfg(feature = "a11y")] + description: Option>, + #[cfg(feature = "a11y")] + label: Option>, handle: Handle, width: Length, height: Length, @@ -31,7 +41,7 @@ where style: ::Style, } -impl Svg +impl<'a, Renderer> Svg<'a, Renderer> where Renderer: svg::Renderer, Renderer::Theme: StyleSheet, @@ -39,6 +49,13 @@ where /// Creates a new [`Svg`] from the given [`Handle`]. pub fn new(handle: impl Into) -> Self { Svg { + id: Id::unique(), + #[cfg(feature = "a11y")] + name: None, + #[cfg(feature = "a11y")] + description: None, + #[cfg(feature = "a11y")] + label: None, handle: handle.into(), width: Length::Fill, height: Length::Shrink, @@ -88,9 +105,44 @@ where self.style = style; self } + + #[cfg(feature = "a11y")] + /// Sets the name of the [`Button`]. + pub fn name(mut self, name: impl Into>) -> Self { + self.name = Some(name.into()); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description_widget( + mut self, + description: &T, + ) -> Self { + self.description = Some(iced_accessibility::Description::Id( + description.description(), + )); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description(mut self, description: impl Into>) -> Self { + self.description = + Some(iced_accessibility::Description::Text(description.into())); + self + } + + #[cfg(feature = "a11y")] + /// Sets the label of the [`Button`]. + pub fn label(mut self, label: &dyn iced_accessibility::Labels) -> Self { + self.label = + Some(label.label().into_iter().map(|l| l.into()).collect()); + self + } } -impl Widget for Svg +impl<'a, Message, Renderer> Widget for Svg<'a, Renderer> where Renderer: svg::Renderer, Renderer::Theme: iced_style::svg::StyleSheet, @@ -181,15 +233,71 @@ where render(renderer); } } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + layout: Layout<'_>, + _state: &Tree, + _cursor_position: Point, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::{ + accesskit::{NodeBuilder, NodeId, Rect, Role}, + A11yTree, + }; + + let bounds = layout.bounds(); + let Rectangle { + x, + y, + width, + height, + } = bounds; + let bounds = Rect::new( + x as f64, + y as f64, + (x + width) as f64, + (y + height) as f64, + ); + let mut node = NodeBuilder::new(Role::Image); + node.set_bounds(bounds); + if let Some(name) = self.name.as_ref() { + node.set_name(name.clone()); + } + match self.description.as_ref() { + Some(iced_accessibility::Description::Id(id)) => { + node.set_described_by( + id.iter() + .cloned() + .map(|id| NodeId::from(id)) + .collect::>(), + ); + } + Some(iced_accessibility::Description::Text(text)) => { + node.set_description(text.clone()); + } + None => {} + } + + if let Some(label) = self.label.as_ref() { + node.set_labelled_by(label.clone()); + } + + A11yTree::leaf(node, self.id.clone()) + } + + fn id(&self) -> Option { + Some(self.id.clone()) + } } -impl<'a, Message, Renderer> From> +impl<'a, Message, Renderer> From> for Element<'a, Message, Renderer> where Renderer: svg::Renderer + 'a, Renderer::Theme: iced_style::svg::StyleSheet, { - fn from(icon: Svg) -> Element<'a, Message, Renderer> { + fn from(icon: Svg<'a, Renderer>) -> Element<'a, Message, Renderer> { Element::new(icon) } } diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 9db382f73f..7599e4bba4 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -7,6 +7,7 @@ mod value; pub mod cursor; pub use cursor::Cursor; +use iced_renderer::core::widget::OperationOutputWrapper; pub use value::Value; use editor::Editor; @@ -272,7 +273,7 @@ where tree: &mut Tree, _layout: Layout<'_>, _renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation>, ) { let state = tree.state.downcast_mut::(); diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index 713a9c302f..643f969438 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -1,10 +1,13 @@ //! Show toggle controls using togglers. +use std::borrow::Cow; + use crate::core::alignment; use crate::core::event; use crate::core::layout; use crate::core::mouse; use crate::core::renderer; use crate::core::text; +use crate::core::widget::Id; use crate::core::widget::Tree; use crate::core::{ Alignment, Clipboard, Element, Event, Layout, Length, Pixels, Point, @@ -36,6 +39,14 @@ where Renderer: text::Renderer, Renderer::Theme: StyleSheet, { + id: Id, + label_id: Option, + #[cfg(feature = "a11y")] + name: Option>, + #[cfg(feature = "a11y")] + description: Option>, + #[cfg(feature = "a11y")] + labeled_by_widget: Option>, is_toggled: bool, on_toggle: Box Message + 'a>, label: Option, @@ -72,10 +83,20 @@ where where F: 'a + Fn(bool) -> Message, { + let label = label.into(); + Toggler { + id: Id::unique(), + label_id: label.as_ref().map(|_| Id::unique()), + #[cfg(feature = "a11y")] + name: None, + #[cfg(feature = "a11y")] + description: None, + #[cfg(feature = "a11y")] + labeled_by_widget: None, is_toggled, on_toggle: Box::new(f), - label: label.into(), + label: label, width: Length::Fill, size: Self::DEFAULT_SIZE, text_size: None, @@ -132,6 +153,41 @@ where self.style = style.into(); self } + + #[cfg(feature = "a11y")] + /// Sets the name of the [`Button`]. + pub fn name(mut self, name: impl Into>) -> Self { + self.name = Some(name.into()); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description_widget( + mut self, + description: &T, + ) -> Self { + self.description = Some(iced_accessibility::Description::Id( + description.description(), + )); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description(mut self, description: impl Into>) -> Self { + self.description = + Some(iced_accessibility::Description::Text(description.into())); + self + } + + #[cfg(feature = "a11y")] + /// Sets the label of the [`Button`] using another widget. + pub fn label(mut self, label: &dyn iced_accessibility::Labels) -> Self { + self.labeled_by_widget = + Some(label.label().into_iter().map(|l| l.into()).collect()); + self + } } impl<'a, Message, Renderer> Widget @@ -309,6 +365,97 @@ where style.foreground, ); } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + _state: &Tree, + cursor_position: Point, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::{ + accesskit::{ + Action, CheckedState, NodeBuilder, NodeId, Rect, Role, + }, + A11yNode, A11yTree, + }; + + let bounds = layout.bounds(); + let is_hovered = bounds.contains(cursor_position); + let Rectangle { + x, + y, + width, + height, + } = bounds; + + let bounds = Rect::new( + x as f64, + y as f64, + (x + width) as f64, + (y + height) as f64, + ); + + let mut node = NodeBuilder::new(Role::Switch); + node.add_action(Action::Focus); + node.add_action(Action::Default); + node.set_bounds(bounds); + if let Some(name) = self.name.as_ref() { + node.set_name(name.clone()); + } + match self.description.as_ref() { + Some(iced_accessibility::Description::Id(id)) => { + node.set_described_by( + id.iter() + .cloned() + .map(|id| NodeId::from(id)) + .collect::>(), + ); + } + Some(iced_accessibility::Description::Text(text)) => { + node.set_description(text.clone()); + } + None => {} + } + node.set_checked_state(if self.is_toggled { + CheckedState::True + } else { + CheckedState::False + }); + if is_hovered { + node.set_hovered(); + } + node.add_action(Action::Default); + if let Some(label) = self.label.as_ref() { + let mut label_node = NodeBuilder::new(Role::StaticText); + + label_node.set_name(label.clone()); + // TODO proper label bounds for the label + label_node.set_bounds(bounds); + + A11yTree::node_with_child_tree( + A11yNode::new(node, self.id.clone()), + A11yTree::leaf(label_node, self.label_id.clone().unwrap()), + ) + } else { + if let Some(labeled_by_widget) = self.labeled_by_widget.as_ref() { + node.set_labelled_by(labeled_by_widget.clone()); + } + A11yTree::leaf(node, self.id.clone()) + } + } + + fn id(&self) -> Option { + if self.label.is_some() { + Some(Id(iced_accessibility::Internal::Set(vec![ + self.id.0.clone(), + self.label_id.clone().unwrap().0, + ]))) + } else { + Some(self.id.clone()) + } + } } impl<'a, Message, Renderer> From> diff --git a/winit/Cargo.toml b/winit/Cargo.toml index 58e13b3eeb..c795597698 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -21,6 +21,7 @@ x11 = ["winit/x11"] wayland = ["winit/wayland"] wayland-dlopen = ["winit/wayland-dlopen"] wayland-csd-adwaita = ["winit/wayland-csd-adwaita"] +a11y = ["iced_accessibility", "iced_runtime/a11y"] [dependencies] window_clipboard = "0.2" @@ -28,10 +29,8 @@ log = "0.4" thiserror = "1.0" [dependencies.winit] -version = "0.27" -git = "https://github.com/iced-rs/winit.git" -rev = "940457522e9fb9f5dac228b0ecfafe0138b4048c" -default-features = false +git = "https://github.com/pop-os/winit.git" +branch = "iced" [dependencies.iced_runtime] version = "0.1" @@ -73,3 +72,9 @@ features = ["Document", "Window"] [dependencies.sysinfo] version = "0.28" optional = true + +[dependencies.iced_accessibility] +version = "0.1.0" +path = "../accessibility" +optional = true +features = ["accesskit_winit"] diff --git a/winit/src/application.rs b/winit/src/application.rs index 3d7c6e5d84..0a3fa8f65f 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -3,6 +3,10 @@ mod profiler; mod state; +use iced_graphics::core::widget::operation::focusable::focus; +use iced_graphics::core::widget::operation::OperationWrapper; +use iced_graphics::core::widget::Operation; +use iced_runtime::futures::futures::FutureExt; pub use state::State; use crate::conversion; @@ -32,6 +36,30 @@ pub use profiler::Profiler; #[cfg(feature = "trace")] use tracing::{info_span, instrument::Instrument}; +#[derive(Debug)] +/// Wrapper aroun application Messages to allow for more UserEvent variants +pub enum UserEventWrapper { + /// Application Message + Message(Message), + #[cfg(feature = "a11y")] + /// A11y Action Request + A11y(iced_accessibility::accesskit_winit::ActionRequestEvent), + #[cfg(feature = "a11y")] + /// A11y was enabled + A11yEnabled, +} + +#[cfg(feature = "a11y")] +impl From + for UserEventWrapper +{ + fn from( + action_request: iced_accessibility::accesskit_winit::ActionRequestEvent, + ) -> Self { + UserEventWrapper::A11y(action_request) + } +} + /// An interactive, native cross-platform application. /// /// This trait is the main entrypoint of Iced. Once implemented, you can run @@ -268,11 +296,15 @@ async fn run_instance( mut application: A, mut compositor: C, mut renderer: A::Renderer, - mut runtime: Runtime, A::Message>, - mut proxy: winit::event_loop::EventLoopProxy, + mut runtime: Runtime< + E, + Proxy>, + UserEventWrapper, + >, + mut proxy: winit::event_loop::EventLoopProxy>, mut debug: Debug, mut event_receiver: mpsc::UnboundedReceiver< - winit::event::Event<'_, A::Message>, + winit::event::Event<'_, UserEventWrapper>, >, mut control_sender: mpsc::UnboundedSender, init_command: Command, @@ -320,7 +352,12 @@ async fn run_instance( &window, || compositor.fetch_information(), ); - runtime.track(application.subscription().into_recipes()); + runtime.track( + application + .subscription() + .map(subscription_map::) + .into_recipes(), + ); let mut user_interface = ManuallyDrop::new(build_user_interface( &application, @@ -334,6 +371,39 @@ async fn run_instance( let mut events = Vec::new(); let mut messages = Vec::new(); let mut redraw_pending = false; + let mut commands: Vec> = Vec::new(); + + #[cfg(feature = "a11y")] + let (window_a11y_id, adapter, mut a11y_enabled) = { + let node_id = core::id::window_node_id(); + + use iced_accessibility::accesskit::{ + NodeBuilder, NodeId, Role, Tree, TreeUpdate, + }; + use iced_accessibility::accesskit_winit::Adapter; + let title = state.title().to_string(); + let proxy_clone = proxy.clone(); + ( + node_id, + Adapter::new( + &window, + move || { + let _ = + proxy_clone.send_event(UserEventWrapper::A11yEnabled); + let mut node = NodeBuilder::new(Role::Window); + node.set_name(title.clone()); + let node = node.build(&mut iced_accessibility::accesskit::NodeClassSet::lock_global()); + TreeUpdate { + nodes: vec![(NodeId(node_id), node)], + tree: Some(Tree::new(NodeId(node_id))), + focus: None, + } + }, + proxy.clone(), + ), + false, + ) + }; debug.startup_finished(); @@ -476,7 +546,26 @@ async fn run_instance( )); } event::Event::UserEvent(message) => { - messages.push(message); + match message { + UserEventWrapper::Message(m) => messages.push(m), + #[cfg(feature = "a11y")] + UserEventWrapper::A11y(request) => { + match request.request.action { + iced_accessibility::accesskit::Action::Focus => { + commands.push(Command::widget(focus( + core::widget::Id::from(u128::from( + request.request.target.0, + ) + as u64), + ))); + } + _ => {} + } + events.push(conversion::a11y(request.request)); + } + #[cfg(feature = "a11y")] + UserEventWrapper::A11yEnabled => a11y_enabled = true, + }; } event::Event::RedrawRequested(_) => { #[cfg(feature = "trace")] @@ -488,6 +577,79 @@ async fn run_instance( continue; } + #[cfg(feature = "a11y")] + if a11y_enabled { + use iced_accessibility::{ + accesskit::{ + NodeBuilder, NodeId, Role, Tree, TreeUpdate, + }, + A11yId, A11yNode, A11yTree, + }; + // TODO send a11y tree + let child_tree = + user_interface.a11y_nodes(state.cursor_position()); + let mut root = NodeBuilder::new(Role::Window); + root.set_name(state.title()); + + let window_tree = A11yTree::node_with_child_tree( + A11yNode::new(root, window_a11y_id), + child_tree, + ); + let tree = Tree::new(NodeId(window_a11y_id)); + let mut current_operation = + Some(Box::new(OperationWrapper::Id(Box::new( + operation::focusable::find_focused(), + )))); + + let mut focus = None; + while let Some(mut operation) = current_operation.take() { + user_interface.operate(&renderer, operation.as_mut()); + + match operation.finish() { + operation::Outcome::None => {} + operation::Outcome::Some(message) => match message { + operation::OperationOutputWrapper::Message( + _, + ) => { + unimplemented!(); + } + operation::OperationOutputWrapper::Id(id) => { + focus = Some(A11yId::from(id)); + } + }, + operation::Outcome::Chain(next) => { + current_operation = Some(Box::new( + OperationWrapper::Wrapper(next), + )); + } + } + } + + log::debug!( + "focus: {:?}\ntree root: {:?}\n children: {:?}", + &focus, + window_tree + .root() + .iter() + .map(|n| (n.node().role(), n.id())) + .collect::>(), + window_tree + .children() + .iter() + .map(|n| (n.node().role(), n.id())) + .collect::>() + ); + // TODO maybe optimize this? + let focus = focus + .filter(|f_id| window_tree.contains(f_id)) + .map(|id| id.into()); + adapter.update(TreeUpdate { + nodes: window_tree.into(), + tree: Some(tree), + focus, + }); + } + debug.render_started(); let current_viewport_version = state.viewport_version(); @@ -620,6 +782,11 @@ pub fn build_user_interface<'a, A: Application>( where ::Theme: StyleSheet, { + // XXX Ashley: for multi-window this should be moved to build_user_interfaces + // TODO refactor: + #[cfg(feature = "a11y")] + core::widget::Id::reset(); + #[cfg(feature = "trace")] let view_span = info_span!("Application", "VIEW").entered(); @@ -643,6 +810,16 @@ where user_interface } +/// subscription mapper helper +pub fn subscription_map(e: A::Message) -> UserEventWrapper +where + A: Application, + E: Executor, + ::Theme: StyleSheet, +{ + UserEventWrapper::Message(e) +} + /// Updates an [`Application`] by feeding it the provided messages, spawning any /// resulting [`Command`], and tracking its [`Subscription`]. pub fn update( @@ -650,10 +827,14 @@ pub fn update( cache: &mut user_interface::Cache, state: &State, renderer: &mut A::Renderer, - runtime: &mut Runtime, A::Message>, + runtime: &mut Runtime< + E, + Proxy>, + UserEventWrapper, + >, clipboard: &mut Clipboard, should_exit: &mut bool, - proxy: &mut winit::event_loop::EventLoopProxy, + proxy: &mut winit::event_loop::EventLoopProxy>, debug: &mut Debug, messages: &mut Vec, window: &winit::window::Window, @@ -690,8 +871,12 @@ pub fn update( ); } - let subscription = application.subscription(); - runtime.track(subscription.into_recipes()); + runtime.track( + application + .subscription() + .map(subscription_map::) + .into_recipes(), + ); } /// Runs the actions of a [`Command`]. @@ -701,10 +886,14 @@ pub fn run_command( state: &State, renderer: &mut A::Renderer, command: Command, - runtime: &mut Runtime, A::Message>, + runtime: &mut Runtime< + E, + Proxy>, + UserEventWrapper, + >, clipboard: &mut Clipboard, should_exit: &mut bool, - proxy: &mut winit::event_loop::EventLoopProxy, + proxy: &mut winit::event_loop::EventLoopProxy>, debug: &mut Debug, window: &winit::window::Window, _graphics_info: impl FnOnce() -> compositor::Information + Copy, @@ -720,14 +909,16 @@ pub fn run_command( for action in command.actions() { match action { command::Action::Future(future) => { - runtime.spawn(future); + runtime.spawn(Box::pin( + future.map(|e| UserEventWrapper::Message(e)), + )); } command::Action::Clipboard(action) => match action { clipboard::Action::Read(tag) => { let message = tag(clipboard.read()); proxy - .send_event(message) + .send_event(UserEventWrapper::Message(message)) .expect("Send message to event loop"); } clipboard::Action::Write(contents) => { @@ -777,7 +968,7 @@ pub fn run_command( }; proxy - .send_event(tag(mode)) + .send_event(UserEventWrapper::Message(tag(mode))) .expect("Send message to event loop"); } window::Action::ToggleMaximize => { @@ -799,7 +990,9 @@ pub fn run_command( } window::Action::FetchId(tag) => { proxy - .send_event(tag(window.id().into())) + .send_event(UserEventWrapper::Message(tag(window + .id() + .into()))) .expect("Send message to event loop"); } }, @@ -817,7 +1010,7 @@ pub fn run_command( let message = _tag(information); proxy - .send_event(message) + .send_event(UserEventWrapper::Message(message)) .expect("Send message to event loop") }); } @@ -825,8 +1018,8 @@ pub fn run_command( }, command::Action::Widget(action) => { let mut current_cache = std::mem::take(cache); - let mut current_operation = Some(action); - + let mut current_operation = + Some(Box::new(OperationWrapper::Message(action))); let mut user_interface = build_user_interface( application, current_cache, @@ -841,12 +1034,24 @@ pub fn run_command( match operation.finish() { operation::Outcome::None => {} operation::Outcome::Some(message) => { - proxy - .send_event(message) - .expect("Send message to event loop"); + match message { + operation::OperationOutputWrapper::Message( + m, + ) => { + proxy + .send_event(UserEventWrapper::Message( + m, + )) + .expect("Send message to event loop"); + } + operation::OperationOutputWrapper::Id(_) => { + // TODO ASHLEY should not ever happen, should this panic!()? + } + } } operation::Outcome::Chain(next) => { - current_operation = Some(next); + current_operation = + Some(Box::new(OperationWrapper::Wrapper(next))); } } } @@ -861,9 +1066,10 @@ pub fn run_command( renderer.load_font(bytes); proxy - .send_event(tagger(Ok(()))) + .send_event(UserEventWrapper::Message(tagger(Ok(())))) .expect("Send message to event loop"); } + command::Action::PlatformSpecific(_) => todo!(), } } } diff --git a/winit/src/application/state.rs b/winit/src/application/state.rs index c37ccca614..776732ac32 100644 --- a/winit/src/application/state.rs +++ b/winit/src/application/state.rs @@ -106,6 +106,11 @@ where &self.theme } + /// Returns the current title of the [`State`]. + pub fn title(&self) -> &str { + &self.title + } + /// Returns the current background [`Color`] of the [`State`]. pub fn background_color(&self) -> Color { self.appearance.background_color diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index a926218470..926f527f44 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -529,3 +529,13 @@ pub(crate) fn is_private_use_character(c: char) -> bool { | '\u{100000}'..='\u{10FFFD}' ) } + +#[cfg(feature = "a11y")] +pub(crate) fn a11y( + event: iced_accessibility::accesskit::ActionRequest, +) -> Event { + // XXX + let id = + iced_runtime::core::id::Id::from(u128::from(event.target.0) as u64); + Event::A11y(id, event) +} From ebcda204ca90557888e0a0a9b1491da78065d6c0 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 4 May 2023 19:16:39 -0400 Subject: [PATCH 2/7] feat: stable ids --- accessibility/Cargo.toml | 10 ++-- accessibility/src/id.rs | 18 +++++- accessibility/src/traits.rs | 2 +- core/src/element.rs | 93 ++++++++++++++++++++++++++++++- core/src/widget.rs | 6 +- core/src/widget/text.rs | 8 +++ core/src/widget/tree.rs | 92 +++++++++++++++++++++++++----- examples/todos/src/main.rs | 7 ++- runtime/src/user_interface.rs | 4 +- src/lib.rs | 4 +- widget/src/button.rs | 10 +++- widget/src/checkbox.rs | 16 ++++-- widget/src/column.rs | 4 +- widget/src/container.rs | 49 ++++------------ widget/src/image.rs | 4 ++ widget/src/lazy.rs | 25 ++++++++- widget/src/lazy/component.rs | 35 +++++++++++- widget/src/lazy/responsive.rs | 36 +++++++++++- widget/src/mouse_area.rs | 4 +- widget/src/overlay/menu.rs | 4 +- widget/src/pane_grid.rs | 21 ++++--- widget/src/pane_grid/content.rs | 6 +- widget/src/pane_grid/title_bar.rs | 6 +- widget/src/row.rs | 4 +- widget/src/scrollable.rs | 19 +++++-- widget/src/slider.rs | 4 ++ widget/src/svg.rs | 4 ++ widget/src/text_input.rs | 42 +++----------- widget/src/toggler.rs | 13 ++++- widget/src/tooltip.rs | 4 +- winit/src/application.rs | 4 +- 31 files changed, 406 insertions(+), 152 deletions(-) diff --git a/accessibility/Cargo.toml b/accessibility/Cargo.toml index 25aaeb0bd9..86f55feb32 100644 --- a/accessibility/Cargo.toml +++ b/accessibility/Cargo.toml @@ -12,8 +12,8 @@ accesskit_unix = { git = "https://github.com/wash2/accesskit.git", tag = "v0.11. accesskit_windows = { git = "https://github.com/wash2/accesskit.git", tag = "v0.11.0", version = "0.14.0", optional = true} accesskit_macos = { git = "https://github.com/wash2/accesskit.git", tag = "v0.11.0", version = "0.7.0", optional = true} accesskit_winit = { git = "https://github.com/wash2/accesskit.git", tag = "v0.11.0", version = "0.13.0", optional = true} -# accesskit = { path = "../../fork/accesskit/common/", version = "0.11.0" } -# accesskit_unix = { path = "../../fork/accesskit/platforms/unix/", version = "0.4.0", optional = true } -# accesskit_windows = { path = "../../fork/accesskit/platforms/windows/", version = "0.14.0", optional = true} -# accesskit_macos = { path = "../../fork/accesskit/platforms/macos/", version = "0.7.0", optional = true} -# accesskit_winit = { path = "../../fork/accesskit/platforms/winit/", version = "0.13.0", optional = true} +# accesskit = { path = "../../accesskit/common/", version = "0.11.0" } +# accesskit_unix = { path = "../../accesskit/platforms/unix/", version = "0.4.0", optional = true } +# accesskit_windows = { path = "../../accesskit/platforms/windows/", version = "0.14.0", optional = true} +# accesskit_macos = { path = "../../accesskit/platforms/macos/", version = "0.7.0", optional = true} +# accesskit_winit = { path = "../../accesskit/platforms/winit/", version = "0.13.0", optional = true} diff --git a/accessibility/src/id.rs b/accessibility/src/id.rs index 81e4dac897..a9702eac05 100644 --- a/accessibility/src/id.rs +++ b/accessibility/src/id.rs @@ -1,9 +1,10 @@ //! Widget and Window IDs. +use std::hash::Hash; use std::sync::atomic::{self, AtomicU64}; use std::{borrow, num::NonZeroU128}; -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Hash, Eq)] pub enum A11yId { Window(NonZeroU128), Widget(Id), @@ -27,6 +28,7 @@ impl From for A11yId { impl From for A11yId { fn from(id: Id) -> Self { + assert!(!matches!(id.0, Internal::Set(_))); Self::Widget(id) } } @@ -56,7 +58,7 @@ static NEXT_ID: AtomicU64 = AtomicU64::new(1); static NEXT_WINDOW_ID: AtomicU64 = AtomicU64::new(1); /// The identifier of a generic widget. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Hash, Eq)] pub struct Id(pub Internal); impl Id { @@ -129,7 +131,7 @@ pub fn window_node_id() -> NonZeroU128 { } // TODO refactor to make panic impossible? -#[derive(Debug, Clone, Eq, Hash)] +#[derive(Debug, Clone, Eq)] /// Internal representation of an [`Id`]. pub enum Internal { /// a unique id @@ -162,6 +164,16 @@ impl PartialEq for Internal { } } +impl Hash for Internal { + fn hash(&self, state: &mut H) { + match self { + Self::Unique(id) => id.hash(state), + Self::Custom(name, _) => name.hash(state), + Self::Set(ids) => ids.hash(state), + } + } +} + #[cfg(test)] mod tests { use super::Id; diff --git a/accessibility/src/traits.rs b/accessibility/src/traits.rs index 14ece49a20..be5ebb825e 100644 --- a/accessibility/src/traits.rs +++ b/accessibility/src/traits.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use crate::A11yId; -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Hash)] pub enum Description<'a> { Text(Cow<'a, str>), Id(Vec), diff --git a/core/src/element.rs b/core/src/element.rs index c65ba678f4..6036fb9298 100644 --- a/core/src/element.rs +++ b/core/src/element.rs @@ -1,4 +1,5 @@ use crate::event::{self, Event}; +use crate::id::Id; use crate::layout; use crate::mouse; use crate::overlay; @@ -10,7 +11,7 @@ use crate::{ }; use std::any::Any; -use std::borrow::Borrow; +use std::borrow::{Borrow, BorrowMut}; /// A generic [`Widget`]. /// @@ -254,6 +255,65 @@ impl<'a, Message, Renderer> Borrow + 'a> } } +impl<'a, Message, Renderer> Borrow + 'a> + for &mut Element<'a, Message, Renderer> +{ + fn borrow(&self) -> &(dyn Widget + 'a) { + self.widget.borrow() + } +} + +impl<'a, Message, Renderer> BorrowMut + 'a> + for &mut Element<'a, Message, Renderer> +{ + fn borrow_mut(&mut self) -> &mut (dyn Widget + 'a) { + self.widget.borrow_mut() + } +} + +impl<'a, Message, Renderer> BorrowMut + 'a> + for Element<'a, Message, Renderer> +{ + fn borrow_mut(&mut self) -> &mut (dyn Widget + 'a) { + self.widget.borrow_mut() + } +} + +impl<'a, Message, Renderer> Widget + for Element<'a, Message, Renderer> +where + Renderer: crate::Renderer, +{ + fn width(&self) -> Length { + self.widget.width() + } + + fn height(&self) -> Length { + self.widget.height() + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + todo!() + } + + fn draw( + &self, + state: &Tree, + renderer: &mut Renderer, + theme: &::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + todo!() + } +} + struct Map<'a, A, B, Renderer> { widget: Box + 'a>, mapper: Box B + 'a>, @@ -292,7 +352,7 @@ where self.widget.children() } - fn diff(&self, tree: &mut Tree) { + fn diff(&mut self, tree: &mut Tree) { self.widget.diff(tree) } @@ -453,6 +513,24 @@ where .overlay(tree, layout, renderer) .map(move |overlay| overlay.map(mapper)) } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + _layout: Layout<'_>, + _state: &Tree, + _cursor_position: Point, + ) -> iced_accessibility::A11yTree { + self.widget.a11y_nodes(_layout, _state, _cursor_position) + } + + fn id(&self) -> Option { + self.widget.id() + } + + fn set_id(&mut self, id: Id) { + self.widget.set_id(id); + } } struct Explain<'a, Message, Renderer: crate::Renderer> { @@ -494,7 +572,7 @@ where self.element.widget.children() } - fn diff(&self, tree: &mut Tree) { + fn diff(&mut self, tree: &mut Tree) { self.element.widget.diff(tree); } @@ -609,4 +687,13 @@ where ) -> Option> { self.element.widget.overlay(state, layout, renderer) } + + fn id(&self) -> Option { + self.element.widget.id() + } + + fn set_id(&mut self, id: Id) { + self.element.widget.set_id(id); + } + // TODO maybe a11y_nodes } diff --git a/core/src/widget.rs b/core/src/widget.rs index 1253d258f7..dd950899c2 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -89,7 +89,7 @@ where } /// Reconciliates the [`Widget`] with the provided [`Tree`]. - fn diff(&self, _tree: &mut Tree) {} + fn diff(&mut self, _tree: &mut Tree) {} /// Applies an [`Operation`] to the [`Widget`]. fn operate( @@ -156,4 +156,8 @@ where fn id(&self) -> Option { None } + + /// Sets the id of the widget + /// This may be called while diffing the widget tree + fn set_id(&mut self, _id: Id) {} } diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index a389c366e5..2105c5fe07 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -196,6 +196,14 @@ where node.set_live(Live::Polite); A11yTree::leaf(node, self.id.clone()) } + + fn id(&self) -> Option { + Some(self.id.clone().into()) + } + + fn set_id(&mut self, id: crate::widget::Id) { + self.id = id + } } /// Draws text using the same logic as the [`Text`] widget. diff --git a/core/src/widget/tree.rs b/core/src/widget/tree.rs index 0af40c33d9..5a830f1d0b 100644 --- a/core/src/widget/tree.rs +++ b/core/src/widget/tree.rs @@ -1,9 +1,11 @@ //! Store internal widget state in a state tree to ensure continuity. +use crate::id::{Id, Internal}; use crate::Widget; - use std::any::{self, Any}; -use std::borrow::Borrow; +use std::borrow::{Borrow, BorrowMut}; +use std::collections::HashMap; use std::fmt; +use std::hash::Hash; /// A persistent state widget tree. /// @@ -13,6 +15,9 @@ pub struct Tree { /// The tag of the [`Tree`]. pub tag: Tag, + /// the Id of the [`Tree`] + pub id: Option, + /// The [`State`] of the [`Tree`]. pub state: State, @@ -24,6 +29,7 @@ impl Tree { /// Creates an empty, stateless [`Tree`] with no children. pub fn empty() -> Self { Self { + id: None, tag: Tag::stateless(), state: State::None, children: Vec::new(), @@ -40,6 +46,7 @@ impl Tree { let widget = widget.borrow(); Self { + id: widget.id(), tag: widget.tag(), state: widget.state(), children: widget.children(), @@ -56,12 +63,27 @@ impl Tree { /// [`Widget::diff`]: crate::Widget::diff pub fn diff<'a, Message, Renderer>( &mut self, - new: impl Borrow + 'a>, + mut new: impl BorrowMut + 'a>, ) where Renderer: crate::Renderer, { - if self.tag == new.borrow().tag() { - new.borrow().diff(self) + let borrowed: &mut dyn Widget = new.borrow_mut(); + if self.tag == borrowed.tag() { + // TODO can we take here? + if let Some(id) = self.id.clone() { + if matches!(id, Id(Internal::Custom(_, _))) { + borrowed.set_id(id); + } else if borrowed.id() == Some(id.clone()) { + for (old_c, new_c) in + self.children.iter_mut().zip(borrowed.children()) + { + old_c.id = new_c.id; + } + } else { + borrowed.set_id(id); + } + } + borrowed.diff(self) } else { *self = Self::new(new); } @@ -70,14 +92,21 @@ impl Tree { /// Reconciliates the children of the tree with the provided list of widgets. pub fn diff_children<'a, Message, Renderer>( &mut self, - new_children: &[impl Borrow + 'a>], + new_children: &mut [impl BorrowMut + 'a>], ) where Renderer: crate::Renderer, { self.diff_children_custom( new_children, - |tree, widget| tree.diff(widget.borrow()), - |widget| Self::new(widget.borrow()), + new_children.iter().map(|c| c.borrow().id()).collect(), + |tree, widget| { + let borrowed: &mut dyn Widget<_, _> = widget.borrow_mut(); + tree.diff(borrowed) + }, + |widget| { + let borrowed: &dyn Widget<_, _> = widget.borrow(); + Self::new(borrowed) + }, ) } @@ -85,17 +114,54 @@ impl Tree { /// logic both for diffing and creating new widget state. pub fn diff_children_custom( &mut self, - new_children: &[T], - diff: impl Fn(&mut Tree, &T), + new_children: &mut [T], + new_ids: Vec>, + diff: impl Fn(&mut Tree, &mut T), new_state: impl Fn(&T) -> Self, ) { if self.children.len() > new_children.len() { self.children.truncate(new_children.len()); } - for (child_state, new) in - self.children.iter_mut().zip(new_children.iter()) - { + let len_changed = self.children.len() != new_children.len(); + + let children_len = self.children.len(); + let (mut id_map, mut id_list): ( + HashMap, + Vec<&mut Tree>, + ) = self.children.iter_mut().fold( + (HashMap::new(), Vec::with_capacity(children_len)), + |(mut id_map, mut id_list), c| { + if let Some(id) = c.id.as_ref() { + if matches!(id.0, Internal::Custom(_, _)) { + let _ = id_map.insert(id.clone(), c); + } else { + id_list.push(c); + } + } else { + id_list.push(c); + } + (id_map, id_list) + }, + ); + + let mut child_state_i = 0; + for (new, new_id) in new_children.iter_mut().zip(new_ids.iter()) { + let child_state = if let Some(c) = + new_id.as_ref().and_then(|id| id_map.remove(id)) + { + c + } else if child_state_i < id_list.len() { + let c = &mut id_list[child_state_i]; + if len_changed { + c.id = new_id.clone(); + } + child_state_i += 1; + c + } else { + continue; + }; + diff(child_state, new); } diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 8bc7be0901..da805e4e08 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -1,6 +1,7 @@ use iced::alignment::{self, Alignment}; use iced::event::{self, Event}; use iced::font::{self, Font}; +use iced::id::Id; use iced::keyboard::{self, KeyCode, Modifiers}; use iced::subscription; use iced::theme::{self, Theme}; @@ -15,7 +16,7 @@ use iced::{Color, Command, Length, Settings, Subscription}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; -static INPUT_ID: Lazy = Lazy::new(text_input::Id::unique); +static INPUT_ID: Lazy = Lazy::new(Id::unique); pub fn main() -> iced::Result { Todos::run(Settings { @@ -324,8 +325,8 @@ pub enum TaskMessage { } impl Task { - fn text_input_id(i: usize) -> text_input::Id { - text_input::Id::new(format!("task-{i}")) + fn text_input_id(i: usize) -> Id { + Id::new(format!("task-{i}")) } fn new(description: String) -> Self { diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index e7eea6364e..75c1fcd066 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -93,10 +93,10 @@ where cache: Cache, renderer: &mut Renderer, ) -> Self { - let root = root.into(); + let mut root = root.into(); let Cache { mut state } = cache; - state.diff(root.as_widget()); + state.diff(root.as_widget_mut()); let base = renderer.layout(&root, &layout::Limits::new(Size::ZERO, bounds)); diff --git a/src/lib.rs b/src/lib.rs index c73cc48db7..e5e87cce19 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -189,8 +189,8 @@ pub use style::theme; pub use crate::core::alignment; pub use crate::core::event; pub use crate::core::{ - color, Alignment, Background, Color, ContentFit, Length, Padding, Pixels, - Point, Rectangle, Size, Vector, + color, id, Alignment, Background, Color, ContentFit, Length, Padding, + Pixels, Point, Rectangle, Size, Vector, }; pub use crate::runtime::Command; diff --git a/widget/src/button.rs b/widget/src/button.rs index 804dc5a33c..f0915ad687 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -196,8 +196,9 @@ where vec![Tree::new(&self.content)] } - fn diff(&self, tree: &mut Tree) { - tree.diff_children(std::slice::from_ref(&self.content)) + fn diff(&mut self, tree: &mut Tree) { + let children = std::slice::from_mut(&mut self.content); + tree.diff_children(std::slice::from_mut(&mut self.content)) } fn width(&self) -> Length { @@ -411,10 +412,13 @@ where ) } - // TODO each accessible widget really should always have Id, so maybe this doesn't need to be an option fn id(&self) -> Option { Some(self.id.clone()) } + + fn set_id(&mut self, id: Id) { + self.id = id; + } } impl<'a, Message, Renderer> From> diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index 27ae7bf530..c9cac61828 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -11,8 +11,8 @@ use crate::core::text; use crate::core::touch; use crate::core::widget::Tree; use crate::core::{ - Alignment, Clipboard, Element, Layout, Length, Pixels, Point, Rectangle, - Shell, Widget, + id::Internal, Alignment, Clipboard, Element, Layout, Length, Pixels, Point, + Rectangle, Shell, Widget, }; use crate::{Row, Text}; @@ -410,15 +410,21 @@ where A11yTree::leaf(label_node, self.label_id.clone()), ) } - fn id(&self) -> Option { - use iced_accessibility::Internal; - Some(Id(Internal::Set(vec![ self.id.0.clone(), self.label_id.0.clone(), ]))) } + + fn set_id(&mut self, id: Id) { + if let Id(Internal::Set(list)) = id { + if list.len() == 2 { + self.id.0 = list[0].clone(); + self.label_id.0 = list[1].clone(); + } + } + } } impl<'a, Message, Renderer> From> diff --git a/widget/src/column.rs b/widget/src/column.rs index 25c90cf486..8238500f3c 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -110,8 +110,8 @@ where self.children.iter().map(Tree::new).collect() } - fn diff(&self, tree: &mut Tree) { - tree.diff_children(&self.children); + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(self.children.as_mut_slice()); } fn width(&self) -> Length { diff --git a/widget/src/container.rs b/widget/src/container.rs index 5a738f47b9..088ab71d06 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -5,7 +5,7 @@ use crate::core::layout; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; -use crate::core::widget::{self, Operation, Tree}; +use crate::core::widget::{Id, Operation, Tree}; use crate::core::{ Background, Clipboard, Color, Element, Layout, Length, Padding, Pixels, Point, Rectangle, Shell, Widget, @@ -139,8 +139,8 @@ where vec![Tree::new(&self.content)] } - fn diff(&self, tree: &mut Tree) { - tree.diff_children(std::slice::from_ref(&self.content)) + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(std::slice::from_mut(&mut self.content)) } fn width(&self) -> Length { @@ -179,17 +179,14 @@ where renderer: &Renderer, operation: &mut dyn Operation>, ) { - operation.container( - self.id.as_ref().map(|id| &id.0), - &mut |operation| { - self.content.as_widget().operate( - &mut tree.children[0], - layout.children().next().unwrap(), - renderer, - operation, - ); - }, - ); + operation.container(self.id.as_ref(), &mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + operation, + ); + }); } fn on_event( @@ -356,27 +353,3 @@ pub fn draw_background( ); } } - -/// The identifier of a [`Container`]. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Id(widget::Id); - -impl Id { - /// Creates a custom [`Id`]. - pub fn new(id: impl Into>) -> Self { - Self(widget::Id::new(id)) - } - - /// Creates a unique [`Id`]. - /// - /// This function produces a different [`Id`] every time it is called. - pub fn unique() -> Self { - Self(widget::Id::unique()) - } -} - -impl From for widget::Id { - fn from(id: Id) -> Self { - id.0 - } -} diff --git a/widget/src/image.rs b/widget/src/image.rs index a48463c722..a90552dbbd 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -301,6 +301,10 @@ where fn id(&self) -> Option { Some(self.id.clone()) } + + fn set_id(&mut self, id: Id) { + self.id = id; + } } impl<'a, Message, Renderer, Handle> From> diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index 63e410f8ea..c7e481842a 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -117,7 +117,7 @@ where self.with_element(|element| vec![Tree::new(element.as_widget())]) } - fn diff(&self, tree: &mut Tree) { + fn diff(&mut self, tree: &mut Tree) { let current = tree.state.downcast_mut::>(); let mut hasher = Hasher::default(); @@ -131,8 +131,10 @@ where current.element = Rc::new(RefCell::new(Some(element))); (*self.element.borrow_mut()) = Some(current.element.clone()); - self.with_element(|element| { - tree.diff_children(std::slice::from_ref(&element.as_widget())) + self.with_element_mut(|element| { + tree.diff_children(std::slice::from_mut( + &mut element.as_widget_mut(), + )) }); } else { (*self.element.borrow_mut()) = Some(current.element.clone()); @@ -270,6 +272,23 @@ where has_overlay .map(|position| overlay::Element::new(position, Box::new(overlay))) } + + fn set_id(&mut self, _id: iced_accessibility::Id) { + if let Some(e) = self.element.borrow_mut().as_mut() { + if let Some(e) = e.borrow_mut().as_mut() { + e.as_widget_mut().set_id(_id); + } + } + } + + fn id(&self) -> Option { + if let Some(e) = self.element.borrow().as_ref() { + if let Some(e) = e.borrow().as_ref() { + return e.as_widget().id(); + } + } + None + } } #[self_referencing] diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index 1a48b449ac..28842985fa 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -110,13 +110,13 @@ where Renderer: renderer::Renderer, { fn diff_self(&self) { - self.with_element(|element| { + self.with_element_mut(|element| { self.tree .borrow_mut() .borrow_mut() .as_mut() .unwrap() - .diff_children(std::slice::from_ref(&element)); + .diff_children(std::slice::from_mut(element)); }); } @@ -225,6 +225,7 @@ where fn state(&self) -> tree::State { let state = Rc::new(RefCell::new(Some(Tree { + id: None, tag: tree::Tag::of::>(), state: tree::State::new(S::default()), children: vec![Tree::empty()], @@ -237,7 +238,7 @@ where vec![] } - fn diff(&self, tree: &mut Tree) { + fn diff(&mut self, tree: &mut Tree) { let tree = tree.state.downcast_ref::>>>(); *self.tree.borrow_mut() = tree.clone(); self.rebuild_element_if_necessary(); @@ -481,6 +482,34 @@ where ) }) } + fn id(&self) -> Option { + self.with_element(|element| element.as_widget().id()) + } + + fn set_id(&mut self, _id: iced_accessibility::Id) { + self.with_element_mut(|element| element.as_widget_mut().set_id(_id)); + } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + layout: Layout<'_>, + tree: &Tree, + cursor_position: Point, + ) -> iced_accessibility::A11yTree { + let tree = tree.state.downcast_ref::>>>(); + self.with_element(|element| { + if let Some(tree) = tree.borrow().as_ref() { + element.as_widget().a11y_nodes( + layout, + &tree.children[0], + cursor_position, + ) + } else { + iced_accessibility::A11yTree::default() + } + }) + } } struct Overlay<'a, 'b, Message, Renderer, Event, S>( diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index adc3937a98..6149c17857 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -73,7 +73,7 @@ where self.element = view(new_size); self.size = new_size; - tree.diff(&self.element); + tree.diff(&mut self.element); self.layout = self .element @@ -306,6 +306,40 @@ where has_overlay .map(|position| overlay::Element::new(position, Box::new(overlay))) } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + layout: Layout<'_>, + tree: &Tree, + + cursor_position: Point, + ) -> iced_accessibility::A11yTree { + use std::rc::Rc; + + let tree = tree.state.downcast_ref::>>>(); + if let Some(tree) = tree.borrow().as_ref() { + self.content.borrow().element.as_widget().a11y_nodes( + layout, + &tree.children[0], + cursor_position, + ) + } else { + iced_accessibility::A11yTree::default() + } + } + + fn id(&self) -> Option { + self.content.borrow().element.as_widget().id() + } + + fn set_id(&mut self, _id: iced_accessibility::Id) { + self.content + .borrow_mut() + .element + .as_widget_mut() + .set_id(_id); + } } impl<'a, Message, Renderer> From> diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index d49c467a05..7308325e43 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -108,8 +108,8 @@ where vec![Tree::new(&self.content)] } - fn diff(&self, tree: &mut Tree) { - tree.diff_children(std::slice::from_ref(&self.content)); + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(std::slice::from_mut(&mut self.content)); } fn width(&self) -> Length { diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index c322c8bae4..8361c096b3 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -171,7 +171,7 @@ where style, } = menu; - let container = Container::new(Scrollable::new(List { + let mut container = Container::new(Scrollable::new(List { options, hovered_option, last_selection, @@ -181,7 +181,7 @@ where style: style.clone(), })); - state.tree.diff(&container as &dyn Widget<_, _>); + state.tree.diff(&mut container as &mut dyn Widget<_, _>); Self { state: &mut state.tree, diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 3e12ff85b1..dbb65c63e1 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -250,15 +250,20 @@ where .collect() } - fn diff(&self, tree: &mut Tree) { - match &self.contents { - Contents::All(contents, _) => tree.diff_children_custom( - contents, - |state, (_, content)| content.diff(state), - |(_, content)| content.state(), - ), + fn diff(&mut self, tree: &mut Tree) { + match &mut self.contents { + Contents::All(contents, _) => { + let ids = contents.iter().map(|_| None).collect(); // TODO + tree.diff_children_custom( + contents, + ids, + |state, (_, content)| content.diff(state), + |(_, content)| content.state(), + ) + } Contents::Maximized(_, content, _) => tree.diff_children_custom( - &[content], + &mut [content], + vec![None], // TODO |state, content| content.diff(state), |content| content.state(), ), diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs index a5ddaa3513..ce6227b38a 100644 --- a/widget/src/pane_grid/content.rs +++ b/widget/src/pane_grid/content.rs @@ -75,13 +75,13 @@ where } } - pub(super) fn diff(&self, tree: &mut Tree) { + pub(super) fn diff(&mut self, tree: &mut Tree) { if tree.children.len() == 2 { - if let Some(title_bar) = self.title_bar.as_ref() { + if let Some(title_bar) = self.title_bar.as_mut() { title_bar.diff(&mut tree.children[1]); } - tree.children[0].diff(&self.body); + tree.children[0].diff(&mut self.body); } else { *tree = self.state(); } diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs index c1f7d4e62c..3d2c46659e 100644 --- a/widget/src/pane_grid/title_bar.rs +++ b/widget/src/pane_grid/title_bar.rs @@ -102,13 +102,13 @@ where } } - pub(super) fn diff(&self, tree: &mut Tree) { + pub(super) fn diff(&mut self, tree: &mut Tree) { if tree.children.len() == 2 { - if let Some(controls) = self.controls.as_ref() { + if let Some(controls) = self.controls.as_mut() { tree.children[1].diff(controls); } - tree.children[0].diff(&self.content); + tree.children[0].diff(&mut self.content); } else { *tree = self.state(); } diff --git a/widget/src/row.rs b/widget/src/row.rs index dab6eb2f32..09ed330b0e 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -102,8 +102,8 @@ where self.children.iter().map(Tree::new).collect() } - fn diff(&self, tree: &mut Tree) { - tree.diff_children(&self.children) + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(&mut self.children) } fn width(&self) -> Length { diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index cdae39ce54..f5c5b68f4c 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -12,8 +12,8 @@ use crate::core::touch; use crate::core::widget::operation::{self, Operation}; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Background, Clipboard, Color, Element, Layout, Length, Pixels, Point, - Rectangle, Shell, Size, Vector, Widget, + id::Internal, Background, Clipboard, Color, Element, Layout, Length, + Pixels, Point, Rectangle, Shell, Size, Vector, Widget, }; use crate::runtime::Command; @@ -222,8 +222,8 @@ where vec![Tree::new(&self.content)] } - fn diff(&self, tree: &mut Tree) { - tree.diff_children(std::slice::from_ref(&self.content)) + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(std::slice::from_mut(&mut self.content)) } fn width(&self) -> Length { @@ -517,13 +517,20 @@ where } fn id(&self) -> Option { - use iced_accessibility::Internal; - Some(Id(Internal::Set(vec![ self.id.0.clone(), self.scrollbar_id.0.clone(), ]))) } + + fn set_id(&mut self, id: Id) { + if let Id(Internal::Set(list)) = id { + if list.len() == 2 { + self.id.0 = list[0].clone(); + self.scrollbar_id.0 = list[1].clone(); + } + } + } } impl<'a, Message, Renderer> From> diff --git a/widget/src/slider.rs b/widget/src/slider.rs index 94036a8df2..3f4d5d30f0 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -366,6 +366,10 @@ where fn id(&self) -> Option { Some(self.id.clone()) } + + fn set_id(&mut self, id: Id) { + self.id = id; + } } impl<'a, T, Message, Renderer> From> diff --git a/widget/src/svg.rs b/widget/src/svg.rs index e5f93496e0..442c4bbf0c 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -289,6 +289,10 @@ where fn id(&self) -> Option { Some(self.id.clone()) } + + fn set_id(&mut self, id: Id) { + self.id = id; + } } impl<'a, Message, Renderer> From> diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 7599e4bba4..50a9a5e095 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -21,9 +21,9 @@ use crate::core::renderer; use crate::core::text::{self, Text}; use crate::core::time::{Duration, Instant}; use crate::core::touch; -use crate::core::widget; use crate::core::widget::operation::{self, Operation}; use crate::core::widget::tree::{self, Tree}; +use crate::core::widget::Id; use crate::core::window; use crate::core::{ Clipboard, Color, Element, Layout, Length, Padding, Pixels, Point, @@ -233,7 +233,7 @@ where tree::State::new(State::new()) } - fn diff(&self, tree: &mut Tree) { + fn diff(&mut self, tree: &mut Tree) { let state = tree.state.downcast_mut::(); // Unfocus text input if it becomes disabled @@ -277,8 +277,8 @@ where ) { let state = tree.state.downcast_mut::(); - operation.focusable(state, self.id.as_ref().map(|id| &id.0)); - operation.text_input(state, self.id.as_ref().map(|id| &id.0)); + operation.focusable(state, self.id.as_ref()); + operation.text_input(state, self.id.as_ref()); } fn on_event( @@ -386,45 +386,21 @@ pub enum Side { Right, } -/// The identifier of a [`TextInput`]. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Id(widget::Id); - -impl Id { - /// Creates a custom [`Id`]. - pub fn new(id: impl Into>) -> Self { - Self(widget::Id::new(id)) - } - - /// Creates a unique [`Id`]. - /// - /// This function produces a different [`Id`] every time it is called. - pub fn unique() -> Self { - Self(widget::Id::unique()) - } -} - -impl From for widget::Id { - fn from(id: Id) -> Self { - id.0 - } -} - /// Produces a [`Command`] that focuses the [`TextInput`] with the given [`Id`]. pub fn focus(id: Id) -> Command { - Command::widget(operation::focusable::focus(id.0)) + Command::widget(operation::focusable::focus(id)) } /// Produces a [`Command`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the /// end. pub fn move_cursor_to_end(id: Id) -> Command { - Command::widget(operation::text_input::move_cursor_to_end(id.0)) + Command::widget(operation::text_input::move_cursor_to_end(id)) } /// Produces a [`Command`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the /// front. pub fn move_cursor_to_front(id: Id) -> Command { - Command::widget(operation::text_input::move_cursor_to_front(id.0)) + Command::widget(operation::text_input::move_cursor_to_front(id)) } /// Produces a [`Command`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the @@ -433,12 +409,12 @@ pub fn move_cursor_to( id: Id, position: usize, ) -> Command { - Command::widget(operation::text_input::move_cursor_to(id.0, position)) + Command::widget(operation::text_input::move_cursor_to(id, position)) } /// Produces a [`Command`] that selects all the content of the [`TextInput`] with the given [`Id`]. pub fn select_all(id: Id) -> Command { - Command::widget(operation::text_input::select_all(id.0)) + Command::widget(operation::text_input::select_all(id)) } /// Computes the layout of a [`TextInput`]. diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index 643f969438..b02cb82c94 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -10,7 +10,7 @@ use crate::core::text; use crate::core::widget::Id; use crate::core::widget::Tree; use crate::core::{ - Alignment, Clipboard, Element, Event, Layout, Length, Pixels, Point, + id, Alignment, Clipboard, Element, Event, Layout, Length, Pixels, Point, Rectangle, Shell, Widget, }; use crate::{Row, Text}; @@ -456,6 +456,17 @@ where Some(self.id.clone()) } } + + fn set_id(&mut self, id: Id) { + if let Id(id::Internal::Set(list)) = id { + if list.len() == 2 && self.label.is_some() { + self.id.0 = list[0].clone(); + self.label_id = Some(Id(list[1].clone())); + } + } else if self.label.is_none() { + self.id = id; + } + } } impl<'a, Message, Renderer> From> diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs index 084650d104..a47370a435 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -111,8 +111,8 @@ where vec![Tree::new(&self.content)] } - fn diff(&self, tree: &mut Tree) { - tree.diff_children(std::slice::from_ref(&self.content)) + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(std::slice::from_mut(&mut self.content)) } fn width(&self) -> Length { diff --git a/winit/src/application.rs b/winit/src/application.rs index 0a3fa8f65f..62213730ea 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -784,8 +784,8 @@ where { // XXX Ashley: for multi-window this should be moved to build_user_interfaces // TODO refactor: - #[cfg(feature = "a11y")] - core::widget::Id::reset(); + // #[cfg(feature = "a11y")] + // core::widget::Id::reset(); #[cfg(feature = "trace")] let view_span = info_span!("Application", "VIEW").entered(); From c0b2e1b5d251ccb5fad8202d23392a85100c7a24 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 12 May 2023 10:29:23 -0400 Subject: [PATCH 3/7] fix: websocket example --- examples/websocket/src/main.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index 920189f541..c5b843dd5d 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -13,6 +13,7 @@ use once_cell::sync::Lazy; pub fn main() -> iced::Result { WebSocket::run(Settings::default()) } +use iced::id::Id; #[derive(Default)] struct WebSocket { @@ -168,4 +169,4 @@ impl Default for State { } } -static MESSAGE_LOG: Lazy = Lazy::new(scrollable::Id::unique); +static MESSAGE_LOG: Lazy = Lazy::new(|| Id::new("message_log")); From 572eb85997f3c25e5560acc380b935ea0d2a6cdd Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 12 May 2023 11:02:52 -0400 Subject: [PATCH 4/7] fix: modal example --- examples/modal/src/main.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index f48afb69d5..afc70e4aa4 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -179,7 +179,9 @@ mod modal { use iced::advanced::layout::{self, Layout}; use iced::advanced::overlay; use iced::advanced::renderer; - use iced::advanced::widget::{self, Widget}; + use iced::advanced::widget::{ + self, Operation, OperationOutputWrapper, Widget, + }; use iced::advanced::{self, Clipboard, Shell}; use iced::alignment::Alignment; use iced::event; @@ -229,8 +231,8 @@ mod modal { ] } - fn diff(&self, tree: &mut widget::Tree) { - tree.diff_children(&[&self.base, &self.modal]); + fn diff(&mut self, tree: &mut widget::Tree) { + tree.diff_children(&mut [&mut self.base, &mut self.modal]); } fn width(&self) -> Length { @@ -330,7 +332,7 @@ mod modal { state: &mut widget::Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { self.base.as_widget().operate( &mut state.children[0], @@ -443,7 +445,7 @@ mod modal { &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { self.content.as_widget().operate( self.tree, From c02409a4fc363d7f27d00e1f0c027806e7cedb6c Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 12 May 2023 11:07:20 -0400 Subject: [PATCH 5/7] fix: scrollable example --- examples/scrollable/src/main.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index 97344c9494..c9967582fc 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -1,3 +1,4 @@ +use iced::id::Id; use iced::widget::scrollable::{Properties, Scrollbar, Scroller}; use iced::widget::{ button, column, container, horizontal_space, progress_bar, radio, row, @@ -7,7 +8,7 @@ use iced::{executor, theme, Alignment, Color}; use iced::{Application, Command, Element, Length, Settings, Theme}; use once_cell::sync::Lazy; -static SCROLLABLE_ID: Lazy = Lazy::new(scrollable::Id::unique); +static SCROLLABLE_ID: Lazy = Lazy::new(|| Id::new("scrollable")); pub fn main() -> iced::Result { ScrollableDemo::run(Settings::default()) From 251fecd441bd6882c459e129c83491391a0104b0 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 12 May 2023 11:14:57 -0400 Subject: [PATCH 6/7] fix: toast example --- examples/toast/src/main.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index 9d8592580a..0c67d7de33 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -182,7 +182,9 @@ mod toast { use iced::advanced::layout::{self, Layout}; use iced::advanced::overlay; use iced::advanced::renderer; - use iced::advanced::widget::{self, Operation, Tree}; + use iced::advanced::widget::{ + self, Operation, OperationOutputWrapper, Tree, + }; use iced::advanced::{Clipboard, Shell, Widget}; use iced::event::{self, Event}; use iced::mouse; @@ -347,7 +349,7 @@ mod toast { .collect() } - fn diff(&self, tree: &mut Tree) { + fn diff(&mut self, tree: &mut Tree) { let instants = tree.state.downcast_mut::>>(); // Invalidating removed instants to None allows us to remove @@ -368,8 +370,8 @@ mod toast { } tree.diff_children( - &std::iter::once(&self.content) - .chain(self.toasts.iter()) + &mut std::iter::once(&mut self.content) + .chain(self.toasts.iter_mut()) .collect::>(), ); } @@ -379,7 +381,7 @@ mod toast { state: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation>, ) { operation.container(None, &mut |operation| { self.content.as_widget().operate( @@ -621,7 +623,7 @@ mod toast { &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { operation.container(None, &mut |operation| { self.toasts From bf0deae0f291698eef2dc1330131bd2984ba56aa Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 12 May 2023 12:17:02 -0400 Subject: [PATCH 7/7] fix: don't enable a11y feature for web --- .github/workflows/test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a9a9b3f9ee..779142b420 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,10 +32,10 @@ jobs: targets: wasm32-unknown-unknown - uses: actions/checkout@master - name: Run checks - run: cargo check --package iced --target wasm32-unknown-unknown + run: cargo check --package iced --target wasm32-unknown-unknown --no-default-features --features "wgpu" - name: Check compilation of `tour` example - run: cargo build --package tour --target wasm32-unknown-unknown + run: cargo build --package tour --target wasm32-unknown-unknown --no-default-features --features "wgpu" - name: Check compilation of `todos` example - run: cargo build --package todos --target wasm32-unknown-unknown + run: cargo build --package todos --target wasm32-unknown-unknown --no-default-features --features "wgpu" - name: Check compilation of `integration` example - run: cargo build --package integration --target wasm32-unknown-unknown + run: cargo build --package integration --target wasm32-unknown-unknown --no-default-features --features "wgpu"