From 60c49cff47d88944ec81c045cf6d673f276b505c Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 2 May 2023 15:48:20 -0700 Subject: [PATCH] feat: accessibility with some widget impls feat: stable ids a11y: Don't unconditionally pull winit (#43) --- Cargo.toml | 37 +- accessibility/Cargo.toml | 19 ++ accessibility/src/a11y_tree.rs | 80 +++++ accessibility/src/id.rs | 220 ++++++++++++ accessibility/src/lib.rs | 19 ++ accessibility/src/node.rs | 46 +++ accessibility/src/traits.rs | 19 ++ core/Cargo.toml | 7 + core/src/element.rs | 77 ++++- core/src/event.rs | 10 + core/src/id.rs | 130 +++++++ core/src/lib.rs | 4 + core/src/overlay.rs | 6 +- core/src/overlay/element.rs | 6 +- core/src/overlay/group.rs | 8 +- core/src/widget.rs | 30 +- core/src/widget/id.rs | 43 --- core/src/widget/operation.rs | 222 ++++++++++-- core/src/widget/text.rs | 47 +++ core/src/widget/tree.rs | 103 +++++- examples/multi_window/src/main.rs | 8 +- examples/todos/Cargo.toml | 3 +- examples/todos/src/main.rs | 10 +- runtime/Cargo.toml | 4 + runtime/src/command.rs | 1 + runtime/src/command/action.rs | 8 + runtime/src/command/platform_specific/mod.rs | 35 ++ runtime/src/multi_window/state.rs | 10 +- runtime/src/overlay/nested.rs | 8 +- runtime/src/program/state.rs | 11 +- runtime/src/user_interface.rs | 21 +- src/lib.rs | 2 +- widget/Cargo.toml | 3 + widget/src/button.rs | 242 +++++++++++++- widget/src/checkbox.rs | 136 +++++++- widget/src/column.rs | 28 +- widget/src/container.rs | 53 ++- widget/src/helpers.rs | 5 +- widget/src/image.rs | 125 ++++++- widget/src/keyed/column.rs | 10 +- widget/src/lazy.rs | 30 +- widget/src/lazy/component.rs | 42 ++- widget/src/lazy/responsive.rs | 39 ++- widget/src/mouse_area.rs | 7 +- widget/src/overlay/menu.rs | 4 +- widget/src/pane_grid.rs | 25 +- widget/src/pane_grid/content.rs | 10 +- widget/src/pane_grid/title_bar.rs | 12 +- widget/src/pick_list.rs | 6 +- widget/src/row.rs | 28 +- widget/src/scrollable.rs | 269 +++++++++++---- widget/src/slider.rs | 131 ++++++++ widget/src/svg.rs | 112 +++++++ widget/src/text_input.rs | 45 +-- widget/src/themer.rs | 10 +- widget/src/toggler.rs | 172 +++++++++- winit/Cargo.toml | 5 +- winit/src/application.rs | 304 +++++++++++++++-- winit/src/application/state.rs | 5 + winit/src/conversion.rs | 10 + winit/src/multi_window.rs | 335 ++++++++++++++++--- 61 files changed, 3039 insertions(+), 418 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 ef7eae6130..7a8be37e07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ all-features = true maintenance = { status = "actively-developed" } [features] -default = ["wgpu"] +default = ["wgpu", "winit", "multi-window", "a11y"] # Enable the `wgpu` GPU-accelerated renderer backend wgpu = ["iced_renderer/wgpu", "iced_widget/wgpu"] # Enables the `Image` widget @@ -53,6 +53,10 @@ highlighter = ["iced_highlighter"] multi-window = ["iced_winit/multi-window"] # Enables the advanced module advanced = [] +# Enables the `accesskit` accessibility library +a11y = ["iced_accessibility", "iced_core/a11y", "iced_widget/a11y", "iced_winit?/a11y"] +# Enables the winit shell. Conflicts with `wayland` and `glutin`. +winit = ["iced_winit", "iced_accessibility?/accesskit_winit"] [dependencies] iced_core.workspace = true @@ -61,41 +65,43 @@ iced_renderer.workspace = true iced_widget.workspace = true iced_winit.features = ["application"] iced_winit.workspace = true - +iced_winit.optional = true iced_highlighter.workspace = true iced_highlighter.optional = true - +iced_accessibility.workspace = true +iced_accessibility.optional = true thiserror.workspace = true image.workspace = true image.optional = true -[profile.release-opt] -inherits = "release" -codegen-units = 1 -debug = false -lto = true -incremental = false -opt-level = 3 -overflow-checks = false -strip = "debuginfo" [workspace] members = [ "core", "futures", "graphics", - "highlighter", - "renderer", "runtime", + "renderer", "style", "tiny_skia", "wgpu", "widget", "winit", "examples/*", + "accessibility", ] +[profile.release-opt] +inherits = "release" +codegen-units = 1 +debug = false +lto = true +incremental = false +opt-level = 3 +overflow-checks = false +strip = "debuginfo" + [workspace.package] version = "0.12.0" authors = ["Héctor Ramón Jiménez "] @@ -118,7 +124,8 @@ iced_style = { version = "0.12", path = "style" } iced_tiny_skia = { version = "0.12", path = "tiny_skia" } iced_wgpu = { version = "0.12", path = "wgpu" } iced_widget = { version = "0.12", path = "widget" } -iced_winit = { version = "0.12", path = "winit" } +iced_winit = { version = "0.12", path = "winit", features = ["application"] } +iced_accessibility = { version = "0.1", path = "accessibility" } async-std = "1.0" bitflags = "2.0" diff --git a/accessibility/Cargo.toml b/accessibility/Cargo.toml new file mode 100644 index 0000000000..59965df28f --- /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", branch = "winit-0.29" } +accesskit_unix = { git = "https://github.com/wash2/accesskit.git", branch = "winit-0.29", optional = true } +accesskit_windows = { git = "https://github.com/wash2/accesskit.git", branch = "winit-0.29", optional = true} +accesskit_macos = { git = "https://github.com/wash2/accesskit.git", branch = "winit-0.29", optional = true} +accesskit_winit = { git = "https://github.com/wash2/accesskit.git", branch = "winit-0.29", 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..d012f4da19 --- /dev/null +++ b/accessibility/src/id.rs @@ -0,0 +1,220 @@ +//! 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)] +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 { + assert!(!matches!(id.0, Internal::Set(_))); + Self::Widget(id) + } +} + +impl IdEq for A11yId { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (A11yId::Widget(self_), A11yId::Widget(other)) => { + IdEq::eq(self_, other) + } + _ => self == other, + } + } +} + +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)) + } +} + +impl IdEq for Id { + fn eq(&self, other: &Self) -> bool { + IdEq::eq(&self.0, &other.0) + } +} +// 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 From for NonZeroU128 { + fn from(val: Id) -> NonZeroU128 { + match &val.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)] +/// 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(_, l1), Self::Custom(_, r1)) => l1 == r1, + (Self::Set(l0), Self::Set(r0)) => l0 == r0, + _ => false, + } + } +} + +/// Similar to PartialEq, but only intended for use when comparing Ids +pub trait IdEq { + fn eq(&self, other: &Self) -> bool; +} + +impl IdEq 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) + } + } + } +} + +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; + + #[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..be5ebb825e --- /dev/null +++ b/accessibility/src/traits.rs @@ -0,0 +1,19 @@ +use std::borrow::Cow; + +use crate::A11yId; + +#[derive(Debug, Clone, PartialEq, 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 2360e82291..c5f780f062 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -10,6 +10,9 @@ homepage.workspace = true categories.workspace = true keywords.workspace = true +[features] +a11y = ["iced_accessibility"] + [dependencies] bitflags.workspace = true glam.workspace = true @@ -28,3 +31,7 @@ raw-window-handle.workspace = true [dev-dependencies] approx = "0.5" +[dependencies.iced_accessibility] +version = "0.1.0" +path = "../accessibility" +optional = true diff --git a/core/src/element.rs b/core/src/element.rs index 8eea90ca98..3532f570f4 100644 --- a/core/src/element.rs +++ b/core/src/element.rs @@ -1,6 +1,7 @@ use crate::event::{self, Event}; +use crate::id::Id; use crate::layout; -use crate::mouse; +use crate::mouse::{self, Cursor}; use crate::overlay; use crate::renderer; use crate::widget; @@ -11,7 +12,7 @@ use crate::{ }; use std::any::Any; -use std::borrow::Borrow; +use std::borrow::{Borrow, BorrowMut}; /// A generic [`Widget`]. /// @@ -261,6 +262,37 @@ impl<'a, Message, Theme, Renderer> } } +impl<'a, Message, Theme, Renderer> + Borrow + 'a> + for &mut Element<'a, Message, Theme, Renderer> +{ + fn borrow(&self) -> &(dyn Widget + 'a) { + self.widget.borrow() + } +} + +impl<'a, Message, Theme, Renderer> + BorrowMut + 'a> + for &mut Element<'a, Message, Theme, Renderer> +{ + fn borrow_mut( + &mut self, + ) -> &mut (dyn Widget + 'a) { + self.widget.borrow_mut() + } +} + +impl<'a, Message, Theme, Renderer> + BorrowMut + 'a> + for Element<'a, Message, Theme, Renderer> +{ + fn borrow_mut( + &mut self, + ) -> &mut (dyn Widget + 'a) { + self.widget.borrow_mut() + } +} + struct Map<'a, A, B, Theme, Renderer> { widget: Box + 'a>, mapper: Box B + 'a>, @@ -300,8 +332,8 @@ where self.widget.children() } - fn diff(&self, tree: &mut Tree) { - self.widget.diff(tree); + fn diff(&mut self, tree: &mut Tree) { + self.widget.diff(tree) } fn size(&self) -> Size { @@ -326,7 +358,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, @@ -454,6 +488,24 @@ where .overlay(tree, layout, renderer, translation) .map(move |overlay| overlay.map(mapper)) } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + _layout: Layout<'_>, + _state: &Tree, + _cursor_position: Cursor, + ) -> 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, Theme, Renderer: crate::Renderer> { @@ -498,7 +550,7 @@ where self.element.widget.children() } - fn diff(&self, tree: &mut Tree) { + fn diff(&mut self, tree: &mut Tree) { self.element.widget.diff(tree); } @@ -516,7 +568,9 @@ where state: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn widget::Operation< + widget::OperationOutputWrapper, + >, ) { self.element .widget @@ -603,4 +657,13 @@ where .widget .overlay(state, layout, renderer, translation) } + + 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/event.rs b/core/src/event.rs index 870b3074e4..9e28e9ea48 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 002336ee5f..5246b542e6 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -41,6 +41,8 @@ mod color; mod content_fit; mod element; mod hasher; +#[cfg(not(feature = "a11y"))] +pub mod id; mod length; mod padding; mod pixels; @@ -64,6 +66,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 03076a30b2..713950df6b 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, Vector}; /// An interactive component that can be displayed on top of other widgets. @@ -41,7 +41,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 695b88b3a5..f2a62db805 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; @@ -94,7 +94,7 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { self.overlay.operate(layout, renderer, operation); } @@ -146,7 +146,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 7e4bebd078..25b9ab1051 100644 --- a/core/src/overlay/group.rs +++ b/core/src/overlay/group.rs @@ -4,7 +4,11 @@ use crate::mouse; use crate::overlay; use crate::renderer; use crate::widget; -use crate::{Clipboard, Event, Layout, Overlay, Point, Rectangle, Shell, Size}; +use crate::widget::Operation; +use crate::widget::OperationOutputWrapper; +use crate::{ + Clipboard, Event, Layout, Overlay, Point, Rectangle, Shell, Size, Vector, +}; /// An [`Overlay`] container that displays multiple overlay [`overlay::Element`] /// children. @@ -132,7 +136,7 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.children.iter_mut().zip(layout.children()).for_each( diff --git a/core/src/widget.rs b/core/src/widget.rs index 51326f1246..f05b5c38ab 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; @@ -97,7 +95,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( @@ -105,7 +103,7 @@ where _state: &mut Tree, _layout: Layout<'_>, _renderer: &Renderer, - _operation: &mut dyn Operation, + _operation: &mut dyn Operation>, ) { } @@ -150,4 +148,24 @@ 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: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + iced_accessibility::A11yTree::default() + } + + /// Returns the id of the widget + 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/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 b91cf9ac94..ab4e74b74e 100644 --- a/core/src/widget/operation.rs +++ b/core/src/widget/operation.rs @@ -10,9 +10,171 @@ pub use text_input::TextInput; use crate::widget::Id; use crate::{Rectangle, Vector}; -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>, + bounds: Rectangle, + operate_on_children: &mut dyn FnMut( + &mut dyn Operation>, + ), + ) { + match self { + OperationWrapper::Message(operation) => { + operation.container(id, bounds, &mut |operation| { + operate_on_children(&mut MapOperation { operation }); + }); + } + OperationWrapper::Id(operation) => { + operation.container(id, bounds, &mut |operation| { + operate_on_children(&mut MapOperation { operation }); + }); + } + OperationWrapper::Wrapper(operation) => { + operation.container(id, bounds, 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>, + bounds: Rectangle, + translation: Vector, + ) { + match self { + OperationWrapper::Message(operation) => { + operation.scrollable(state, id, bounds, translation); + } + OperationWrapper::Id(operation) => { + operation.scrollable(state, id, bounds, translation); + } + OperationWrapper::Wrapper(operation) => { + operation.scrollable(state, id, bounds, translation); + } + } + } + + 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>, + bounds: Rectangle, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + self.operation.container(id, bounds, &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>, + bounds: Rectangle, + translation: Vector, + ) { + self.operation.scrollable(state, id, bounds, translation); + } + + 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. @@ -44,7 +206,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`]. @@ -53,31 +215,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>, @@ -201,9 +338,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 0796c4e4bb..d54149297b 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -20,6 +20,7 @@ where Theme: StyleSheet, Renderer: text::Renderer, { + id: crate::widget::Id, content: Cow<'a, str>, size: Option, line_height: LineHeight, @@ -40,6 +41,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, line_height: LineHeight::default(), @@ -184,6 +186,50 @@ where viewport, ); } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + layout: Layout<'_>, + _state: &Tree, + _: mouse::Cursor, + ) -> 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()) + } + + fn id(&self) -> Option { + Some(self.id.clone().into()) + } + + fn set_id(&mut self, id: crate::widget::Id) { + self.id = id + } } /// Produces the [`layout::Node`] of a [`Text`] widget. @@ -290,6 +336,7 @@ where { fn clone(&self) -> Self { Self { + id: self.id.clone(), content: self.content.clone(), size: self.size, line_height: self.line_height, diff --git a/core/src/widget/tree.rs b/core/src/widget/tree.rs index 6b1a130964..3578d45af7 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,28 @@ impl Tree { /// [`Widget::diff`]: crate::Widget::diff pub fn diff<'a, Message, Theme, 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,32 +93,78 @@ impl Tree { /// Reconciles the children of the tree with the provided list of widgets. pub fn diff_children<'a, Message, Theme, Renderer>( &mut self, - new_children: &[impl Borrow + 'a>], + new_children: &mut [impl BorrowMut< + dyn Widget + '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) + }, + ) } /// Reconciliates the children of the tree with the provided list of widgets using custom /// 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); } @@ -114,8 +183,8 @@ impl Tree { /// `maybe_changed` closure. pub fn diff_children_custom_with_search( current_children: &mut Vec, - new_children: &[T], - diff: impl Fn(&mut Tree, &T), + new_children: &mut [T], + diff: impl Fn(&mut Tree, &mut T), maybe_changed: impl Fn(usize) -> bool, new_state: impl Fn(&T) -> Tree, ) { @@ -183,7 +252,7 @@ pub fn diff_children_custom_with_search( // TODO: Merge loop with extend logic (?) for (child_state, new) in - current_children.iter_mut().zip(new_children.iter()) + current_children.iter_mut().zip(new_children.iter_mut()) { diff(child_state, new); } diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index 5a5e70c16c..ddfa5286b8 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -4,8 +4,8 @@ use iced::multi_window::{self, Application}; use iced::widget::{button, column, container, scrollable, text, text_input}; use iced::window; use iced::{ - Alignment, Command, Element, Length, Point, Settings, Subscription, Theme, - Vector, + id::Id, Alignment, Command, Element, Length, Point, Settings, Subscription, + Theme, Vector, }; use std::collections::HashMap; @@ -26,7 +26,7 @@ struct Window { scale_input: String, current_scale: f64, theme: Theme, - input_id: iced::widget::text_input::Id, + input_id: Id, } #[derive(Debug, Clone)] @@ -178,7 +178,7 @@ impl Window { } else { Theme::Dark }, - input_id: text_input::Id::unique(), + input_id: Id::unique(), } } diff --git a/examples/todos/Cargo.toml b/examples/todos/Cargo.toml index 3c62bfbc8f..0d5f8c38a7 100644 --- a/examples/todos/Cargo.toml +++ b/examples/todos/Cargo.toml @@ -7,7 +7,8 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["async-std", "debug"] +iced_core.workspace = true +iced.features = ["async-std", "debug", "wgpu"] once_cell.workspace = true serde = { version = "1.0", features = ["derive"] } diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index eae127f7a4..dc7b2fa673 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -1,6 +1,7 @@ use iced::alignment::{self, Alignment}; use iced::font::{self, Font}; -use iced::keyboard; +use iced::keyboard::{self, Modifiers}; +use iced::subscription; use iced::theme::{self, Theme}; use iced::widget::{ self, button, checkbox, column, container, keyed_column, row, scrollable, @@ -9,12 +10,13 @@ use iced::widget::{ use iced::window; use iced::{Application, Element}; use iced::{Color, Command, Length, Settings, Size, Subscription}; +use iced_core::widget::Id; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; use uuid::Uuid; -static INPUT_ID: Lazy = Lazy::new(text_input::Id::unique); +static INPUT_ID: Lazy = Lazy::new(Id::unique); pub fn main() -> iced::Result { #[cfg(not(target_arch = "wasm32"))] @@ -316,8 +318,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/Cargo.toml b/runtime/Cargo.toml index 3a47a971ae..49182ba253 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -13,6 +13,8 @@ keywords.workspace = true [features] debug = [] multi-window = [] +a11y = ["iced_accessibility", "iced_core/a11y"] + [dependencies] iced_core.workspace = true @@ -21,3 +23,5 @@ iced_futures.features = ["thread-pool"] thiserror.workspace = true raw-window-handle.workspace = true +iced_accessibility.workspace = true +iced_accessibility.optional = true diff --git a/runtime/src/command.rs b/runtime/src/command.rs index f70da915fb..176c2b1e57 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 c9ffe801c4..6f62e71d26 100644 --- a/runtime/src/command/action.rs +++ b/runtime/src/command/action.rs @@ -46,6 +46,8 @@ pub enum Action { /// A custom action supported by a specific runtime. Custom(Box), + /// Run a platform specific action + PlatformSpecific(crate::command::platform_specific::Action), } impl Action { @@ -76,6 +78,9 @@ impl Action { tagger: Box::new(move |result| f(tagger(result))), }, Self::Custom(custom) => Action::Custom(custom), + Self::PlatformSpecific(action) => { + Action::PlatformSpecific(action.map(f)) + } } } } @@ -95,6 +100,9 @@ impl fmt::Debug for Action { Self::Widget(_action) => write!(f, "Action::Widget"), Self::LoadFont { .. } => write!(f, "Action::LoadFont"), Self::Custom(_) => write!(f, "Action::Custom"), + 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..c259f40b07 --- /dev/null +++ b/runtime/src/command/platform_specific/mod.rs @@ -0,0 +1,35 @@ +//! Platform specific actions defined for wayland + +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/multi_window/state.rs b/runtime/src/multi_window/state.rs index afd0451965..fc6d7265d4 100644 --- a/runtime/src/multi_window/state.rs +++ b/runtime/src/multi_window/state.rs @@ -6,6 +6,7 @@ use crate::core::widget::operation::{self, Operation}; use crate::core::{Clipboard, Size}; use crate::user_interface::{self, UserInterface}; use crate::{Command, Debug, Program}; +use iced_core::widget::OperationOutputWrapper; /// The execution state of a multi-window [`Program`]. It leverages caching, event /// processing, and rendering primitive storage. @@ -205,7 +206,9 @@ where pub fn operate( &mut self, renderer: &mut P::Renderer, - operations: impl Iterator>>, + operations: impl Iterator< + Item = Box>>, + >, bounds: Size, debug: &mut Debug, ) { @@ -227,12 +230,15 @@ where match operation.finish() { operation::Outcome::None => {} - operation::Outcome::Some(message) => { + operation::Outcome::Some( + OperationOutputWrapper::Message(message), + ) => { self.queued_messages.push(message); } operation::Outcome::Chain(next) => { current_operation = Some(next); } + _ => {} }; } } diff --git a/runtime/src/overlay/nested.rs b/runtime/src/overlay/nested.rs index ddb9532b9f..122f8916d4 100644 --- a/runtime/src/overlay/nested.rs +++ b/runtime/src/overlay/nested.rs @@ -1,3 +1,5 @@ +use iced_core::widget::OperationOutputWrapper; + use crate::core::event; use crate::core::layout; use crate::core::mouse; @@ -131,13 +133,15 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn widget::Operation>, ) { fn recurse( element: &mut overlay::Element<'_, Message, Theme, Renderer>, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn widget::Operation< + OperationOutputWrapper, + >, ) where Renderer: renderer::Renderer, { diff --git a/runtime/src/program/state.rs b/runtime/src/program/state.rs index d685b07c86..dad260c9b3 100644 --- a/runtime/src/program/state.rs +++ b/runtime/src/program/state.rs @@ -1,3 +1,5 @@ +use iced_core::widget::OperationOutputWrapper; + use crate::core::event::{self, Event}; use crate::core::mouse; use crate::core::renderer; @@ -179,7 +181,9 @@ where pub fn operate( &mut self, renderer: &mut P::Renderer, - operations: impl Iterator>>, + operations: impl Iterator< + Item = Box>>, + >, bounds: Size, debug: &mut Debug, ) { @@ -199,12 +203,15 @@ where match operation.finish() { operation::Outcome::None => {} - operation::Outcome::Some(message) => { + operation::Outcome::Some( + OperationOutputWrapper::Message(message), + ) => { self.queued_messages.push(message); } operation::Outcome::Chain(next) => { current_operation = Some(next); } + _ => {} }; } } diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index 08431cedfa..87b96c3b68 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; @@ -90,10 +92,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 = root.as_widget().layout( &mut state, @@ -566,7 +568,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, @@ -609,6 +611,19 @@ where pub fn into_cache(self) -> Cache { Cache { state: self.state } } + + /// get a11y nodes + #[cfg(feature = "a11y")] + pub fn a11y_nodes( + &self, + cursor: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + self.root.as_widget().a11y_nodes( + Layout::new(&self.base), + &self.state, + cursor, + ) + } } /// Reusable data of a specific [`UserInterface`]. diff --git a/src/lib.rs b/src/lib.rs index 0804c9ef82..84f63d08e9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -193,7 +193,7 @@ pub use crate::core::border; pub use crate::core::color; pub use crate::core::gradient; pub use crate::core::{ - Alignment, Background, Border, Color, ContentFit, Degrees, Gradient, + id, Alignment, Background, Border, Color, ContentFit, Degrees, Gradient, Length, Padding, Pixels, Point, Radians, Rectangle, Shadow, Size, Transformation, Vector, }; diff --git a/widget/Cargo.toml b/widget/Cargo.toml index e8e363c41d..0880477a0b 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -21,11 +21,14 @@ svg = ["iced_renderer/svg"] canvas = ["iced_renderer/geometry"] qr_code = ["canvas", "qrcode"] wgpu = ["iced_renderer/wgpu"] +a11y = ["iced_accessibility"] [dependencies] iced_renderer.workspace = true iced_runtime.workspace = true iced_style.workspace = true +iced_accessibility.workspace = true +iced_accessibility.optional = true num-traits.workspace = true thiserror.workspace = true diff --git a/widget/src/button.rs b/widget/src/button.rs index 867fbfaf59..ebfa3cb4aa 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,7 +18,8 @@ use crate::core::{ Shell, Size, Vector, Widget, }; -pub use crate::style::button::{Appearance, StyleSheet}; +use iced_renderer::core::widget::{operation, OperationOutputWrapper}; +pub use iced_style::button::{Appearance, StyleSheet}; /// A generic widget that produces a message when pressed. /// @@ -57,6 +62,13 @@ where Renderer: crate::core::Renderer, { content: Element<'a, Message, Theme, Renderer>, + id: Id, + #[cfg(feature = "a11y")] + name: Option>, + #[cfg(feature = "a11y")] + description: Option>, + #[cfg(feature = "a11y")] + label: Option>, on_press: Option, width: Length, height: Length, @@ -79,6 +91,13 @@ where Button { content, + id: Id::unique(), + #[cfg(feature = "a11y")] + name: None, + #[cfg(feature = "a11y")] + description: None, + #[cfg(feature = "a11y")] + label: None, on_press: None, width: size.width.fluid(), height: size.height.fluid(), @@ -135,6 +154,47 @@ where self.clip = clip; 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, Theme, Renderer> Widget @@ -156,8 +216,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 size(&self) -> Size { @@ -187,7 +248,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.content.as_widget().operate( @@ -223,9 +284,15 @@ where return event::Status::Captured; } - update(event, layout, cursor, shell, &self.on_press, || { - tree.state.downcast_mut::() - }) + update( + self.id.clone(), + event, + layout, + cursor, + shell, + &self.on_press, + || tree.state.downcast_mut::(), + ) } fn draw( @@ -295,6 +362,90 @@ where translation, ) } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + p: mouse::Cursor, + ) -> 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, + ) + } + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn set_id(&mut self, id: Id) { + self.id = id; + } } impl<'a, Message, Theme, Renderer> From> @@ -312,7 +463,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 { @@ -320,11 +473,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: mouse::Cursor, @@ -365,9 +539,42 @@ 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, .. }) => { + if let Some(on_press) = on_press.clone() { + let state = state(); + if state.is_focused + && matches!( + key, + keyboard::Key::Named(keyboard::key::Named::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; } _ => {} @@ -449,3 +656,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 0ff4d58b51..9582ed15c2 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -1,4 +1,8 @@ //! Show toggle controls using checkboxes. +use iced_renderer::core::Size; +use iced_runtime::core::widget::Id; +use std::borrow::Cow; + use crate::core::alignment; use crate::core::event::{self, Event}; use crate::core::layout; @@ -9,7 +13,8 @@ use crate::core::touch; use crate::core::widget; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, + id::Internal, Alignment, Clipboard, Element, Layout, Length, Pixels, Point, + Rectangle, Shell, Widget, }; pub use crate::style::checkbox::{Appearance, StyleSheet}; @@ -42,6 +47,12 @@ pub struct Checkbox< Theme: StyleSheet + crate::text::StyleSheet, Renderer: text::Renderer, { + id: Id, + label_id: Id, + #[cfg(feature = "a11y")] + name: Option>, + #[cfg(feature = "a11y")] + description: Option>, is_checked: bool, on_toggle: Option Message + 'a>>, label: String, @@ -74,6 +85,12 @@ where /// * a boolean describing whether the [`Checkbox`] is checked or not pub fn new(label: impl Into, is_checked: bool) -> Self { Checkbox { + id: Id::unique(), + label_id: Id::unique(), + #[cfg(feature = "a11y")] + name: None, + #[cfg(feature = "a11y")] + description: None, is_checked, on_toggle: None, label: label.into(), @@ -181,6 +198,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, Theme, Renderer> Widget @@ -213,7 +257,7 @@ where layout::next_to_each_other( &limits.width(self.width), self.spacing, - |_| layout::Node::new(Size::new(self.size, self.size)), + |_| layout::Node::new(crate::core::Size::new(self.size, self.size)), |limits| { let state = tree .state @@ -360,6 +404,94 @@ where ); } } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + _state: &Tree, + cursor: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::{ + accesskit::{ + Action, CheckedState, NodeBuilder, NodeId, Rect, Role, + }, + A11yNode, A11yTree, + }; + + let bounds = layout.bounds(); + let is_hovered = cursor.is_over(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::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 { + 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, Theme, Renderer> From> diff --git a/widget/src/column.rs b/widget/src/column.rs index 8154ad8532..f29dedd063 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; @@ -135,8 +137,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 size(&self) -> Size { @@ -173,7 +175,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.children @@ -288,6 +290,26 @@ where translation, ) } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + cursor: mouse::Cursor, + ) -> 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, cursor) + }), + ) + } } impl<'a, Message, Theme, Renderer> From> diff --git a/widget/src/container.rs b/widget/src/container.rs index e01741778d..4f23330938 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -6,13 +6,14 @@ use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; -use crate::core::widget::{self, Operation}; +use crate::core::widget::{self, Id, Operation}; use crate::core::{ Background, Clipboard, Color, Element, Layout, Length, Padding, Pixels, Point, Rectangle, Shell, Size, Vector, Widget, }; use crate::runtime::Command; +use iced_renderer::core::widget::OperationOutputWrapper; pub use iced_style::container::{Appearance, StyleSheet}; /// An element decorating some content. @@ -161,8 +162,8 @@ where self.content.as_widget().children() } - fn diff(&self, tree: &mut Tree) { - self.content.as_widget().diff(tree); + fn diff(&mut self, tree: &mut Tree) { + self.content.as_widget_mut().diff(tree); } fn size(&self) -> Size { @@ -196,10 +197,10 @@ 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), + self.id.as_ref(), layout.bounds(), &mut |operation| { self.content.as_widget().operate( @@ -301,6 +302,24 @@ where translation, ) } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + cursor: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + let c_layout = layout.children().next().unwrap(); + let c_state = state.children.get(0); + + self.content.as_widget().a11y_nodes( + c_layout, + c_state.unwrap_or(&Tree::empty()), + cursor, + ) + } } impl<'a, Message, Theme, Renderer> From> @@ -370,30 +389,6 @@ 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 - } -} - /// Produces a [`Command`] that queries the visible screen bounds of the /// [`Container`] with the given [`Id`]. pub fn visible_bounds(id: Id) -> Command> { diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index e9898d6743..bd0babb7fb 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -357,7 +357,10 @@ where /// /// [`Image`]: crate::Image #[cfg(feature = "image")] -pub fn image(handle: impl Into) -> crate::Image { +#[cfg_attr(docsrs, doc(cfg(feature = "image")))] +pub fn image<'a, Handle>( + handle: impl Into, +) -> crate::Image<'a, Handle> { crate::Image::new(handle.into()) } diff --git a/widget/src/image.rs b/widget/src/image.rs index ccf1f1754a..4442186b4b 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; @@ -11,6 +12,7 @@ use crate::core::{ ContentFit, Element, Layout, Length, Rectangle, Size, Vector, Widget, }; +use std::borrow::Cow; use std::hash::Hash; pub use image::{FilterMethod, Handle}; @@ -32,23 +34,39 @@ 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, filter_method: FilterMethod, + 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, filter_method: FilterMethod::default(), + phantom_data: std::marker::PhantomData, } } @@ -77,6 +95,41 @@ impl Image { self.filter_method = filter_method; 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`]. @@ -159,8 +212,8 @@ pub fn draw( } } -impl Widget - for Image +impl<'a, Message, Theme, Renderer, Handle> Widget + for Image<'a, Handle> where Renderer: image::Renderer, Handle: Clone + Hash, @@ -206,15 +259,75 @@ where self.filter_method, ); } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + layout: Layout<'_>, + _state: &Tree, + _cursor: mouse::Cursor, + ) -> 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()) + } + + fn set_id(&mut self, id: Id) { + self.id = id; + } } -impl<'a, Message, Theme, Renderer, Handle> From> +impl<'a, Message, Theme, Renderer, Handle> From> for Element<'a, Message, Theme, Renderer> where Renderer: image::Renderer, Handle: Clone + Hash + 'a, { - fn from(image: Image) -> Element<'a, Message, Theme, Renderer> { + fn from(image: Image<'a, Handle>) -> Element<'a, Message, Theme, Renderer> { Element::new(image) } } diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs index 88a6e503c8..9098f3faec 100644 --- a/widget/src/keyed/column.rs +++ b/widget/src/keyed/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; @@ -163,7 +165,7 @@ where self.children.iter().map(Tree::new).collect() } - fn diff(&self, tree: &mut Tree) { + fn diff(&mut self, tree: &mut Tree) { let Tree { state, children, .. } = tree; @@ -172,8 +174,8 @@ where tree::diff_children_custom_with_search( children, - &self.children, - |tree, child| child.as_widget().diff(tree), + &mut self.children, + |tree, child| child.as_widget_mut().diff(tree), |index| { self.keys.get(index).or_else(|| self.keys.last()).copied() != Some(state.keys[index]) @@ -223,7 +225,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.children diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index eb663ea589..fff79e5c23 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, Vector, @@ -122,7 +123,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::>(); @@ -138,8 +139,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()); @@ -175,7 +178,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( @@ -285,6 +288,23 @@ where Some(overlay::Element::new(Box::new(overlay))) } + + fn set_id(&mut self, _id: iced_runtime::core::id::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 edecbdaa6e..80f4f8eeee 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -12,6 +12,7 @@ use crate::core::{ }; use crate::runtime::overlay::Nested; +use iced_renderer::core::widget::{Operation, OperationOutputWrapper}; use ouroboros::self_referencing; use std::cell::RefCell; use std::marker::PhantomData; @@ -59,7 +60,7 @@ pub trait Component { fn operate( &self, _state: &mut Self::State, - _operation: &mut dyn widget::Operation, + _operation: &mut dyn Operation>, ) { } } @@ -117,13 +118,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)); }); } @@ -161,7 +162,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(); @@ -232,6 +233,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()], @@ -244,7 +246,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(); @@ -345,7 +347,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { self.rebuild_element_with_operation(operation); @@ -503,6 +505,34 @@ where overlay: Some(overlay), }))) } + 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: mouse::Cursor, + ) -> 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, + ) + } else { + iced_accessibility::A11yTree::default() + } + }) + } } struct Overlay<'a, 'b, Message, Theme, Renderer, Event, S>( diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index 44312a2158..4c59972b2f 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -3,7 +3,6 @@ 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, Vector, @@ -12,6 +11,7 @@ use crate::core::{ use crate::horizontal_space; use crate::runtime::overlay::Nested; +use iced_renderer::core::widget::{Operation, OperationOutputWrapper}; use ouroboros::self_referencing; use std::cell::{RefCell, RefMut}; use std::marker::PhantomData; @@ -90,7 +90,7 @@ where self.size = new_size; self.layout = None; - tree.diff(&self.element); + tree.diff(&mut self.element); } fn resolve( @@ -161,7 +161,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(); @@ -318,6 +318,39 @@ where Some(overlay::Element::new(Box::new(overlay))) } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + layout: Layout<'_>, + tree: &Tree, + cursor_position: mouse::Cursor, + ) -> 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, Theme, Renderer> diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index 9634e477d0..6c2f7a2d36 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -1,5 +1,6 @@ //! A container for capturing mouse events. +use iced_renderer::core::widget::OperationOutputWrapper; use iced_renderer::core::Point; use crate::core::event::{self, Event}; @@ -154,8 +155,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 size(&self) -> Size { @@ -178,7 +179,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/overlay/menu.rs b/widget/src/overlay/menu.rs index 8a4d6a98cd..5544ad3b3d 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -205,7 +205,7 @@ where style, } = menu; - let container = Container::new(Scrollable::new(List { + let mut container = Container::new(Scrollable::new(List { options, hovered_option, on_selected, @@ -218,7 +218,7 @@ where style: style.clone(), })); - state.tree.diff(&container as &dyn Widget<_, _, _>); + state.tree.diff(&mut container as &mut dyn Widget<_, _, _>); Self { position, diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 4f9a265a66..4935b6a394 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, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size, @@ -254,15 +254,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(), ), @@ -302,7 +307,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.contents diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs index dfe0fdcfbd..bfa9cf699d 100644 --- a/widget/src/pane_grid/content.rs +++ b/widget/src/pane_grid/content.rs @@ -1,3 +1,5 @@ +use iced_renderer::core::widget::{Operation, OperationOutputWrapper}; + use crate::container; use crate::core::event::{self, Event}; use crate::core::layout; @@ -76,13 +78,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(); } @@ -199,7 +201,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 5b57509b35..5978e4c2ef 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, Vector, }; @@ -101,13 +103,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(); } @@ -262,7 +264,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/pick_list.rs b/widget/src/pick_list.rs index 1f20e2bce2..7637a57e72 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -1,4 +1,6 @@ //! Display a dropdown list of selectable values. +use iced_renderer::core::text::LineHeight; + use crate::container; use crate::core::alignment; use crate::core::event::{self, Event}; @@ -711,8 +713,8 @@ pub fn draw<'a, T, Theme, Renderer>( Renderer::ICON_FONT, Renderer::ARROW_DOWN_ICON, *size, - text::LineHeight::default(), - text::Shaping::Basic, + LineHeight::default(), + text::Shaping::Advanced, )), Handle::Static(Icon { font, diff --git a/widget/src/row.rs b/widget/src/row.rs index 735fbbc076..1609a94a75 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; @@ -126,8 +128,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 size(&self) -> Size { @@ -162,7 +164,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.children @@ -277,6 +279,26 @@ where translation, ) } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + cursor: mouse::Cursor, + ) -> 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, cursor) + }), + ) + } } impl<'a, Message, Theme, Renderer> From> diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 3814c59095..108e4403d8 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -1,5 +1,8 @@ //! Navigate an endless amount of content with a scrollbar. use crate::container; +use iced_runtime::core::widget::Id; +use std::borrow::Cow; + use crate::core::event::{self, Event}; use crate::core::keyboard; use crate::core::layout; @@ -7,18 +10,16 @@ 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::{ - 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; -pub use crate::style::scrollable::{ - Appearance, Scrollbar, Scroller, StyleSheet, -}; +pub use crate::style::scrollable::{Appearance, Scrollbar, Scroller, StyleSheet}; +use iced_renderer::core::widget::OperationOutputWrapper; pub use operation::scrollable::{AbsoluteOffset, RelativeOffset}; /// A widget that can vertically display an infinite amount of content with a @@ -33,7 +34,14 @@ pub struct Scrollable< Theme: StyleSheet, Renderer: crate::core::Renderer, { - 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, direction: Direction, @@ -52,7 +60,14 @@ where 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, direction: Direction::default(), @@ -64,7 +79,7 @@ where /// Sets the [`Id`] of the [`Scrollable`]. pub fn id(mut self, id: Id) -> Self { - self.id = Some(id); + self.id = id; self } @@ -99,6 +114,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 + } } /// The direction of [`Scrollable`]. @@ -222,8 +272,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 size(&self) -> Size { @@ -260,7 +310,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation>, ) { let state = tree.state.downcast_mut::(); @@ -270,25 +320,16 @@ where let translation = state.translation(self.direction, bounds, content_bounds); - operation.scrollable( - state, - self.id.as_ref().map(|id| &id.0), - bounds, - translation, - ); + operation.scrollable(state, Some(&self.id), bounds, translation); - operation.container( - self.id.as_ref().map(|id| &id.0), - bounds, - &mut |operation| { - self.content.as_widget().operate( - &mut tree.children[0], - layout.children().next().unwrap(), - renderer, - operation, - ); - }, - ); + operation.container(Some(&self.id), bounds, &mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + operation, + ); + }); } fn on_event( @@ -407,6 +448,146 @@ where translation - offset, ) } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + cursor: mouse::Cursor, + ) -> 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, + ); + + let window = layout.bounds(); + let is_hovered = cursor.is_over(window); + 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.direction, + content_bounds, + 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 = cursor.is_over(scrollbar_bounds); + 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 { + 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, Theme, Renderer> @@ -424,37 +605,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)) } /// Produces a [`Command`] that scrolls the [`Scrollable`] with the given [`Id`] @@ -463,7 +620,7 @@ pub fn scroll_to( id: Id, offset: AbsoluteOffset, ) -> Command { - Command::widget(operation::scrollable::scroll_to(id.0, offset)) + Command::widget(operation::scrollable::scroll_to(id, offset)) } /// Computes the layout of a [`Scrollable`]. diff --git a/widget/src/slider.rs b/widget/src/slider.rs index 65bc1772eb..3146b65c19 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -9,11 +9,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::{ Border, Clipboard, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size, Widget, }; +use std::borrow::Cow; use std::ops::RangeInclusive; pub use iced_style::slider::{ @@ -49,6 +51,13 @@ pub struct Slider<'a, T, Message, Theme = crate::Theme> where Theme: StyleSheet, { + id: Id, + #[cfg(feature = "a11y")] + name: Option>, + #[cfg(feature = "a11y")] + description: Option>, + #[cfg(feature = "a11y")] + label: Option>, range: RangeInclusive, step: T, shift_step: Option, @@ -95,6 +104,13 @@ where }; Slider { + id: Id::unique(), + #[cfg(feature = "a11y")] + name: None, + #[cfg(feature = "a11y")] + description: None, + #[cfg(feature = "a11y")] + label: None, value, default: None, range, @@ -158,6 +174,40 @@ where self.shift_step = Some(shift_step.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 + } } impl<'a, T, Message, Theme, Renderer> Widget @@ -251,6 +301,87 @@ where ) -> mouse::Interaction { mouse_interaction(layout, cursor, tree.state.downcast_ref::()) } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + layout: Layout<'_>, + _state: &Tree, + cursor: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::{ + accesskit::{NodeBuilder, NodeId, Rect, Role}, + A11yTree, + }; + + let bounds = layout.bounds(); + let is_hovered = cursor.is_over(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::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()) + } + + fn set_id(&mut self, id: Id) { + self.id = id; + } } impl<'a, T, Message, Theme, Renderer> From> diff --git a/widget/src/svg.rs b/widget/src/svg.rs index 12ef3d925c..eee5bf5a2d 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::mouse; use crate::core::renderer; @@ -8,6 +10,7 @@ use crate::core::{ ContentFit, Element, Layout, Length, Rectangle, Size, Vector, Widget, }; +use std::borrow::Cow; use std::path::PathBuf; pub use crate::style::svg::{Appearance, StyleSheet}; @@ -24,6 +27,13 @@ pub struct Svg where 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, @@ -38,6 +48,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, @@ -84,6 +101,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`]. + 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 @@ -179,6 +231,66 @@ where render(renderer); } } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + layout: Layout<'_>, + _state: &Tree, + _cursor: mouse::Cursor, + ) -> 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()) + } + + fn set_id(&mut self, id: Id) { + self.id = id; + } } impl<'a, Message, Theme, Renderer> From> diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index f5b57422d9..da5507bfa5 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; @@ -22,9 +23,9 @@ use crate::core::renderer; use crate::core::text::{self, Paragraph as _, 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::{ Element, Layout, Length, Padding, Pixels, Point, Rectangle, Shell, Size, @@ -274,7 +275,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 @@ -320,12 +321,12 @@ where tree: &mut Tree, _layout: Layout<'_>, _renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation>, ) { 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( @@ -433,45 +434,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 @@ -480,12 +457,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/themer.rs b/widget/src/themer.rs index e6ca6cfea2..6bcc144b2e 100644 --- a/widget/src/themer.rs +++ b/widget/src/themer.rs @@ -1,3 +1,5 @@ +use iced_renderer::core::widget::OperationOutputWrapper; + use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; @@ -57,8 +59,8 @@ where self.content.as_widget().children() } - fn diff(&self, tree: &mut Tree) { - self.content.as_widget().diff(tree); + fn diff(&mut self, tree: &mut Tree) { + self.content.as_widget_mut().diff(tree); } fn size(&self) -> Size { @@ -79,7 +81,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation>, ) { self.content .as_widget() @@ -191,7 +193,7 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation>, ) { self.content.operate(layout, renderer, operation); } diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index 4e3925bad5..aeee108ed0 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -1,4 +1,6 @@ //! Show toggle controls using togglers. +use std::borrow::Cow; + use crate::core::alignment; use crate::core::event; use crate::core::layout; @@ -6,10 +8,10 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::text; use crate::core::touch; -use crate::core::widget; use crate::core::widget::tree::{self, Tree}; +use crate::core::widget::{self, Id}; use crate::core::{ - Border, Clipboard, Element, Event, Layout, Length, Pixels, Rectangle, + id, Border, Clipboard, Element, Event, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, }; @@ -41,6 +43,14 @@ pub struct Toggler< Theme: StyleSheet, Renderer: text::Renderer, { + 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, @@ -79,10 +89,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, @@ -153,6 +173,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, Theme, Renderer> Widget @@ -187,7 +242,12 @@ where layout::next_to_each_other( &limits, self.spacing, - |_| layout::Node::new(Size::new(2.0 * self.size, self.size)), + |_| { + layout::Node::new(crate::core::Size::new( + 2.0 * self.size, + self.size, + )) + }, |limits| { if let Some(label) = self.label.as_deref() { let state = tree @@ -209,7 +269,7 @@ where self.text_shaping, ) } else { - layout::Node::new(Size::ZERO) + layout::Node::new(crate::core::Size::ZERO) } }, ) @@ -348,6 +408,108 @@ where style.foreground, ); } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + _state: &Tree, + cursor: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::{ + accesskit::{ + Action, CheckedState, NodeBuilder, NodeId, Rect, Role, + }, + A11yNode, A11yTree, + }; + + let bounds = layout.bounds(); + let is_hovered = cursor.is_over(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::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_runtime::core::id::Internal::Set(vec![ + self.id.0.clone(), + self.label_id.clone().unwrap().0, + ]))) + } else { + 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, Theme, Renderer> From> diff --git a/winit/Cargo.toml b/winit/Cargo.toml index 87e600aeba..c9da88e7d7 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -20,12 +20,15 @@ wayland = ["winit/wayland"] wayland-dlopen = ["winit/wayland-dlopen"] wayland-csd-adwaita = ["winit/wayland-csd-adwaita"] multi-window = ["iced_runtime/multi-window"] +a11y = ["iced_accessibility", "iced_runtime/a11y"] [dependencies] iced_graphics.workspace = true iced_runtime.workspace = true iced_style.workspace = true - +iced_accessibility.workspace = true +iced_accessibility.optional = true +iced_accessibility.features = ["accesskit_winit"] log.workspace = true thiserror.workspace = true tracing.workspace = true diff --git a/winit/src/application.rs b/winit/src/application.rs index 0c596b3f6e..c231bf027a 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -1,6 +1,10 @@ //! Create interactive, native cross-platform applications. 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; @@ -20,12 +24,41 @@ use crate::runtime::user_interface::{self, UserInterface}; use crate::runtime::{Command, Debug}; use crate::style::application::{Appearance, StyleSheet}; use crate::{Clipboard, Error, Proxy, Settings}; - use futures::channel::mpsc; +use futures::stream::StreamExt; use std::mem::ManuallyDrop; use std::sync::Arc; +#[cfg(feature = "trace")] +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 @@ -270,11 +303,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, + winit::event::Event>, >, mut control_sender: mpsc::UnboundedSender, init_command: Command, @@ -287,7 +324,6 @@ async fn run_instance( C: Compositor + 'static, A::Theme: StyleSheet, { - use futures::stream::StreamExt; use winit::event; use winit::event_loop::ControlFlow; @@ -323,7 +359,12 @@ async fn run_instance( &mut debug, &window, ); - 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, @@ -337,6 +378,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(); @@ -360,8 +434,38 @@ async fn run_instance( )), )); } + event::Event::PlatformSpecific(event::PlatformSpecific::MacOS( + event::MacOS::ReceivedUrl(url), + )) => { + use crate::core::event; + + events.push(Event::PlatformSpecific( + event::PlatformSpecific::MacOS(event::MacOS::ReceivedUrl( + url, + )), + )); + } 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::WindowEvent { event: event::WindowEvent::RedrawRequested { .. }, @@ -439,7 +543,7 @@ async fn run_instance( }, state.cursor(), ); - redraw_pending = false; + debug.draw_finished(); if new_mouse_interaction != mouse_interaction { @@ -450,6 +554,88 @@ async fn run_instance( mouse_interaction = new_mouse_interaction; } + window.request_redraw(); + + redraw_pending = false; + + let physical_size = state.physical_size(); + + if physical_size.width == 0 || physical_size.height == 0 { + 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()); + 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(); match compositor.present( &mut renderer, @@ -621,6 +807,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( @@ -630,10 +826,14 @@ pub fn update( cache: &mut user_interface::Cache, state: &mut 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, @@ -667,8 +867,12 @@ pub fn update( state.synchronize(application, window); - let subscription = application.subscription(); - runtime.track(subscription.into_recipes()); + runtime.track( + application + .subscription() + .map(subscription_map::) + .into_recipes(), + ); } /// Runs the actions of a [`Command`]. @@ -680,10 +884,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, ) where @@ -699,17 +907,21 @@ 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::Stream(stream) => { - runtime.run(stream); + runtime.run(Box::pin( + stream.map(|e| UserEventWrapper::Message(e)), + )); } command::Action::Clipboard(action) => match action { clipboard::Action::Read(tag, kind) => { let message = tag(clipboard.read(kind)); proxy - .send_event(message) + .send_event(UserEventWrapper::Message(message)) .expect("Send message to event loop"); } clipboard::Action::Write(contents, kind) => { @@ -741,15 +953,16 @@ pub fn run_command( window.inner_size().to_logical(window.scale_factor()); proxy - .send_event(callback(Size::new( - size.width, - size.height, + .send_event(UserEventWrapper::Message(callback( + Size::new(size.width, size.height), ))) .expect("Send message to event loop"); } window::Action::FetchMaximized(_id, callback) => { proxy - .send_event(callback(window.is_maximized())) + .send_event(UserEventWrapper::Message(callback( + window.is_maximized(), + ))) .expect("Send message to event loop"); } window::Action::Maximize(_id, maximized) => { @@ -757,7 +970,9 @@ pub fn run_command( } window::Action::FetchMinimized(_id, callback) => { proxy - .send_event(callback(window.is_minimized())) + .send_event(UserEventWrapper::Message(callback( + window.is_minimized(), + ))) .expect("Send message to event loop"); } window::Action::Minimize(_id, minimized) => { @@ -787,7 +1002,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(_id) => { @@ -817,7 +1032,9 @@ pub fn run_command( } window::Action::FetchId(_id, tag) => { proxy - .send_event(tag(window.id().into())) + .send_event(UserEventWrapper::Message(tag(window + .id() + .into()))) .expect("Send message to event loop"); } window::Action::RunWithHandle(_id, tag) => { @@ -840,9 +1057,11 @@ pub fn run_command( ); proxy - .send_event(tag(window::Screenshot::new( - bytes, - state.physical_size(), + .send_event(UserEventWrapper::Message(tag( + window::Screenshot::new( + bytes, + state.physical_size(), + ), ))) .expect("Send message to event loop."); } @@ -861,16 +1080,16 @@ pub fn run_command( let message = _tag(information); proxy - .send_event(message) - .expect("Send message to event loop"); + .send_event(UserEventWrapper::Message(message)) + .expect("Send message to event loop") }); } } }, 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, @@ -885,12 +1104,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))); } } } @@ -905,12 +1136,13 @@ 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::Custom(_) => { log::warn!("Unsupported custom action in `iced_winit` shell"); } + command::Action::PlatformSpecific(_) => todo!(), } } } diff --git a/winit/src/application/state.rs b/winit/src/application/state.rs index c17a3bcc13..94f2af7032 100644 --- a/winit/src/application/state.rs +++ b/winit/src/application/state.rs @@ -110,6 +110,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 ef789296e2..c601570f36 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -816,3 +816,13 @@ pub fn icon(icon: window::Icon) -> Option { winit::window::Icon::from_rgba(pixels, size.width, size.height).ok() } + +#[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) +} diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index c63dd43332..b9dddf7d35 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -2,19 +2,21 @@ mod state; mod window_manager; -pub use state::State; - +use crate::application::UserEventWrapper; use crate::conversion; use crate::core; use crate::core::mouse; use crate::core::renderer; use crate::core::widget::operation; +use crate::core::widget::Operation; use crate::core::window; use crate::core::Size; use crate::futures::futures::channel::mpsc; use crate::futures::futures::{task, Future, StreamExt}; use crate::futures::{Executor, Runtime, Subscription}; use crate::graphics::{compositor, Compositor}; +use crate::multi_window::operation::focusable::focus; +use crate::multi_window::operation::OperationWrapper; use crate::multi_window::window_manager::WindowManager; use crate::runtime::command::{self, Command}; use crate::runtime::multi_window::Program; @@ -22,12 +24,25 @@ use crate::runtime::user_interface::{self, UserInterface}; use crate::runtime::Debug; use crate::style::application::StyleSheet; use crate::{Clipboard, Error, Proxy, Settings}; +use iced_runtime::futures::futures::FutureExt; +use iced_style::Theme; +pub use state::State; use std::collections::HashMap; use std::mem::ManuallyDrop; use std::sync::Arc; use std::time::Instant; +/// subscription mapper helper +pub fn subscription_map(e: A::Message) -> UserEventWrapper +where + A: Application, + E: Executor, + ::Theme: StyleSheet, +{ + UserEventWrapper::Message(e) +} + /// An interactive, native, cross-platform, multi-windowed application. /// /// This trait is the main entrypoint of multi-window Iced. Once implemented, you can run @@ -337,10 +352,16 @@ enum Control { async fn run_instance( mut application: A, mut compositor: C, - 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>, + mut event_receiver: mpsc::UnboundedReceiver< + Event>, + >, mut control_sender: mpsc::UnboundedSender, init_command: Command, mut window_manager: WindowManager, @@ -363,6 +384,39 @@ async fn run_instance( } let mut clipboard = Clipboard::connect(&main_window.raw); + + #[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 = main_window.raw.title().to_string(); + let proxy_clone = proxy.clone(); + ( + node_id, + Adapter::new( + &main_window.raw, + 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, + ) + }; let mut events = { vec![( Some(window::Id::MAIN), @@ -400,7 +454,12 @@ async fn run_instance( &mut ui_caches, ); - runtime.track(application.subscription().into_recipes()); + runtime.track( + application + .subscription() + .map(subscription_map::) + .into_recipes(), + ); let mut messages = Vec::new(); @@ -475,9 +534,6 @@ async fn run_instance( ), )); } - event::Event::UserEvent(message) => { - messages.push(message); - } event::Event::WindowEvent { window_id: id, event: event::WindowEvent::RedrawRequested, @@ -799,6 +855,168 @@ async fn run_instance( cached_interfaces, )); } + + debug.draw_started(); + + for (id, window) in window_manager.iter_mut() { + // TODO: Avoid redrawing all the time by forcing widgets to + // request redraws on state changes + // + // Then, we can use the `interface_state` here to decide if a redraw + // is needed right away, or simply wait until a specific time. + let redraw_event = core::Event::Window( + id, + window::Event::RedrawRequested(Instant::now()), + ); + + let cursor = window.state.cursor(); + + let ui = user_interfaces + .get_mut(&id) + .expect("Get user interface"); + + let (ui_state, _) = ui.update( + &[redraw_event.clone()], + cursor, + &mut window.renderer, + &mut clipboard, + &mut messages, + ); + + let new_mouse_interaction = { + let state = &window.state; + + ui.draw( + &mut window.renderer, + state.theme(), + &renderer::Style { + text_color: state.text_color(), + }, + cursor, + ) + }; + + if new_mouse_interaction != window.mouse_interaction + { + window.raw.set_cursor_icon( + conversion::mouse_interaction( + new_mouse_interaction, + ), + ); + + window.mouse_interaction = + new_mouse_interaction; + } + + // TODO once widgets can request to be redrawn, we can avoid always requesting a + // redraw + window.raw.request_redraw(); + + runtime.broadcast( + redraw_event.clone(), + core::event::Status::Ignored, + ); + + let _ = control_sender.start_send( + Control::ChangeFlow(match ui_state { + user_interface::State::Updated { + redraw_request: Some(redraw_request), + } => match redraw_request { + window::RedrawRequest::NextFrame => { + ControlFlow::Poll + } + window::RedrawRequest::At(at) => { + ControlFlow::WaitUntil(at) + } + }, + _ => ControlFlow::Wait, + }), + ); + } + + debug.draw_finished(); + } + event::Event::PlatformSpecific( + event::PlatformSpecific::MacOS( + event::MacOS::ReceivedUrl(url), + ), + ) => { + use crate::core::event; + + events.push(( + None, + event::Event::PlatformSpecific( + event::PlatformSpecific::MacOS( + event::MacOS::ReceivedUrl(url), + ), + ), + )); + } + event::Event::UserEvent(message) => { + match message { + UserEventWrapper::Message(m) => messages.push(m), + #[cfg(feature = "a11y")] + UserEventWrapper::A11y(request) => { + match request.request.action { + iced_accessibility::accesskit::Action::Focus => { + // TODO send a command for this + } + _ => {} + } + events.push(( + None, + conversion::a11y(request.request), + )); + } + #[cfg(feature = "a11y")] + UserEventWrapper::A11yEnabled => { + a11y_enabled = true + } + }; + } + event::Event::WindowEvent { + event: window_event, + window_id, + } => { + let Some((id, window)) = + window_manager.get_mut_alias(window_id) + else { + continue; + }; + + if matches!( + window_event, + winit::event::WindowEvent::CloseRequested + ) && window.exit_on_close_request + { + let _ = window_manager.remove(id); + let _ = user_interfaces.remove(&id); + let _ = ui_caches.remove(&id); + + events.push(( + None, + core::Event::Window(id, window::Event::Closed), + )); + + if window_manager.is_empty() { + break 'main; + } + } else { + window.state.update( + &window.raw, + &window_event, + &mut debug, + ); + + if let Some(event) = conversion::window_event( + id, + window_event, + window.state.scale_factor(), + window.state.modifiers(), + ) { + events.push((Some(id), event)); + } + } } _ => {} } @@ -837,10 +1055,14 @@ where fn update( application: &mut A, compositor: &mut C, - runtime: &mut Runtime, A::Message>, + runtime: &mut Runtime< + E, + Proxy>, + UserEventWrapper, + >, clipboard: &mut Clipboard, control_sender: &mut mpsc::UnboundedSender, - proxy: &mut winit::event_loop::EventLoopProxy, + proxy: &mut winit::event_loop::EventLoopProxy>, debug: &mut Debug, messages: &mut Vec, window_manager: &mut WindowManager, @@ -870,8 +1092,11 @@ fn update( ); } - let subscription = application.subscription(); - runtime.track(subscription.into_recipes()); + let subscription = application + .subscription() + .map(subscription_map::) + .into_recipes(); + runtime.track(subscription); } /// Runs the actions of a [`Command`]. @@ -879,10 +1104,15 @@ fn run_command( application: &A, compositor: &mut C, command: Command, - runtime: &mut Runtime, A::Message>, + runtime: &mut Runtime< + E, + Proxy>, + UserEventWrapper, + >, clipboard: &mut Clipboard, control_sender: &mut mpsc::UnboundedSender, - proxy: &mut winit::event_loop::EventLoopProxy, + proxy: &mut winit::event_loop::EventLoopProxy>, + debug: &mut Debug, window_manager: &mut WindowManager, ui_caches: &mut HashMap, @@ -899,17 +1129,21 @@ fn run_command( for action in command.actions() { match action { command::Action::Future(future) => { - runtime.spawn(Box::pin(future)); + runtime.spawn(Box::pin( + future.map(|e| UserEventWrapper::Message(e)), + )); } command::Action::Stream(stream) => { - runtime.run(Box::pin(stream)); + runtime.run(Box::pin( + stream.map(|e| UserEventWrapper::Message(e)), + )); } command::Action::Clipboard(action) => match action { clipboard::Action::Read(tag, kind) => { let message = tag(clipboard.read(kind)); proxy - .send_event(message) + .send_event(UserEventWrapper::Message(message)) .expect("Send message to event loop"); } clipboard::Action::Write(contents, kind) => { @@ -962,9 +1196,8 @@ fn run_command( .to_logical(window.raw.scale_factor()); proxy - .send_event(callback(Size::new( - size.width, - size.height, + .send_event(UserEventWrapper::Message(callback( + Size::new(size.width, size.height), ))) .expect("Send message to event loop"); } @@ -972,7 +1205,9 @@ fn run_command( window::Action::FetchMaximized(id, callback) => { if let Some(window) = window_manager.get_mut(id) { proxy - .send_event(callback(window.raw.is_maximized())) + .send_event(UserEventWrapper::Message(callback( + window.raw.is_maximized(), + ))) .expect("Send message to event loop"); } } @@ -984,7 +1219,9 @@ fn run_command( window::Action::FetchMinimized(id, callback) => { if let Some(window) = window_manager.get_mut(id) { proxy - .send_event(callback(window.raw.is_minimized())) + .send_event(UserEventWrapper::Message(callback( + window.raw.is_minimized(), + ))) .expect("Send message to event loop"); } } @@ -1026,7 +1263,7 @@ fn run_command( }; proxy - .send_event(tag(mode)) + .send_event(UserEventWrapper::Message(tag(mode))) .expect("Send message to event loop"); } } @@ -1076,7 +1313,10 @@ fn run_command( window::Action::FetchId(id, tag) => { if let Some(window) = window_manager.get_mut(id) { proxy - .send_event(tag(window.raw.id().into())) + .send_event(UserEventWrapper::Message(tag(window + .raw + .id() + .into()))) .expect("Send message to event loop"); } } @@ -1103,9 +1343,11 @@ fn run_command( ); proxy - .send_event(tag(window::Screenshot::new( - bytes, - window.state.physical_size(), + .send_event(UserEventWrapper::Message(tag( + window::Screenshot::new( + bytes, + window.state.physical_size(), + ), ))) .expect("Event loop doesn't exist."); } @@ -1125,15 +1367,15 @@ fn run_command( let message = _tag(information); proxy - .send_event(message) + .send_event(UserEventWrapper::Message(message)) .expect("Event loop doesn't exist."); }); } } }, command::Action::Widget(action) => { - let mut current_operation = Some(action); - + let mut current_operation = + Some(Box::new(OperationWrapper::Message(action))); let mut uis = build_user_interfaces( application, debug, @@ -1141,9 +1383,7 @@ fn run_command( std::mem::take(ui_caches), ); - 'operate: while let Some(mut operation) = - current_operation.take() - { + while let Some(mut operation) = current_operation.take() { for (id, ui) in uis.iter_mut() { if let Some(window) = window_manager.get_mut(*id) { ui.operate(&window.renderer, operation.as_mut()); @@ -1151,15 +1391,25 @@ fn run_command( match operation.finish() { operation::Outcome::None => {} operation::Outcome::Some(message) => { + match message { + operation::OperationOutputWrapper::Message( + m, + ) => { proxy - .send_event(message) - .expect("Event loop doesn't exist."); - - // operation completed, don't need to try to operate on rest of UIs - break 'operate; + .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), + )); } } } @@ -1179,12 +1429,15 @@ fn run_command( } proxy - .send_event(tagger(Ok(()))) + .send_event(UserEventWrapper::Message(tagger(Ok(())))) .expect("Send message to event loop"); } command::Action::Custom(_) => { log::warn!("Unsupported custom action in `iced_winit` shell"); } + command::Action::PlatformSpecific(_) => { + tracing::warn!("Platform specific commands are not supported yet in multi-window winit mode."); + } } } }