From 27174aaaedc2a250ebe607f5bea59fa735aa657d Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 2 May 2023 15:48:20 -0700 Subject: [PATCH 01/93] 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 | 4 +- 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 | 9 + 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 | 4 +- 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 | 8 +- widget/src/overlay/menu.rs | 4 +- widget/src/pane_grid.rs | 25 +- widget/src/pane_grid/content.rs | 12 +- widget/src/pane_grid/title_bar.rs | 12 +- widget/src/pick_list.rs | 6 +- widget/src/row.rs | 28 +- widget/src/scrollable.rs | 265 ++++++++++++--- widget/src/slider.rs | 132 ++++++++ widget/src/svg.rs | 112 +++++++ widget/src/text_input.rs | 45 +-- widget/src/themer.rs | 10 +- widget/src/toggler.rs | 172 +++++++++- widget/src/tooltip.rs | 8 +- 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 ++++++++++++++++--- 62 files changed, 3044 insertions(+), 420 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 c9dee6b75d..56ba17326e 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 = "1.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 32dd3df26a..67f7137e62 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 log.workspace = true @@ -27,3 +30,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 fa07ad69dd..61dc3b3b08 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 { @@ -322,7 +354,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, @@ -449,6 +483,24 @@ where .overlay(tree, layout, renderer) .map(move |overlay| overlay.map(mapper)) } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + _layout: Layout<'_>, + _state: &Tree, + _cursor_position: 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> { @@ -489,7 +541,7 @@ where self.element.widget.children() } - fn diff(&self, tree: &mut Tree) { + fn diff(&mut self, tree: &mut Tree) { self.element.widget.diff(tree); } @@ -507,7 +559,9 @@ where state: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn widget::Operation< + widget::OperationOutputWrapper, + >, ) { self.element .widget @@ -591,4 +645,13 @@ where ) -> Option> { self.element.widget.overlay(state, layout, renderer) } + + fn id(&self) -> Option { + self.element.widget.id() + } + + fn set_id(&mut self, id: Id) { + self.element.widget.set_id(id); + } + // TODO maybe a11y_nodes } diff --git a/core/src/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 bbc973f053..8f4c1c0519 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; @@ -63,6 +65,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 6b8cf2a66e..d440e553b2 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. @@ -47,7 +47,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 c34ab86289..e3ef2fd351 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; @@ -121,7 +121,7 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { self.overlay.operate(layout, renderer, operation); } @@ -179,7 +179,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 4e54a002c8..19f818ae7d 100644 --- a/core/src/overlay/group.rs +++ b/core/src/overlay/group.rs @@ -4,6 +4,8 @@ use crate::mouse; use crate::overlay; use crate::renderer; use crate::widget; +use crate::widget::Operation; +use crate::widget::OperationOutputWrapper; use crate::{ Clipboard, Event, Layout, Overlay, Point, Rectangle, Shell, Size, Vector, }; @@ -140,7 +142,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 d5e2ec6fda..136eeae471 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>, ) { } @@ -149,4 +147,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 3d79f08789..9d7f6af2d2 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 8089d545a1..895c0efc66 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 @@ -20,3 +22,5 @@ iced_futures.workspace = true iced_futures.features = ["thread-pool"] thiserror.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 cb0936df74..b8ed8d4b2b 100644 --- a/runtime/src/command/action.rs +++ b/runtime/src/command/action.rs @@ -43,6 +43,9 @@ pub enum Action { /// The message to produce when the font has been loaded. tagger: Box) -> T>, }, + + /// Run a platform specific action + PlatformSpecific(crate::command::platform_specific::Action), } impl Action { @@ -72,6 +75,9 @@ impl Action { bytes, tagger: Box::new(move |result| f(tagger(result))), }, + Self::PlatformSpecific(action) => { + Action::PlatformSpecific(action.map(f)) + } } } } @@ -90,6 +96,9 @@ impl fmt::Debug for Action { Self::System(action) => write!(f, "Action::System({action:?})"), Self::Widget(_action) => write!(f, "Action::Widget"), Self::LoadFont { .. } => write!(f, "Action::LoadFont"), + Self::PlatformSpecific(action) => { + write!(f, "Action::PlatformSpecific({:?})", action) + } } } } diff --git a/runtime/src/command/platform_specific/mod.rs b/runtime/src/command/platform_specific/mod.rs new file mode 100644 index 0000000000..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 60e2eb875e..00ea22d626 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; @@ -144,13 +146,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 054d56b781..e831a7f65c 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; @@ -92,10 +94,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, @@ -562,7 +564,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, @@ -605,6 +607,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 86207d6e2b..d82bc8868c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -192,8 +192,8 @@ 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, - Length, Padding, Pixels, Point, Radians, Rectangle, Shadow, Size, Vector, + id, Alignment, Background, Color, ContentFit, Degrees, Gradient, Length, + Padding, Pixels, Point, Radians, Rectangle, Size, Vector, }; pub mod clipboard { 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 3b11e8a7aa..f370d729c0 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, 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, @@ -78,6 +90,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(), @@ -126,6 +145,47 @@ where self.style = style.into(); 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 @@ -147,8 +207,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 { @@ -178,7 +239,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( @@ -214,9 +275,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( @@ -278,6 +345,90 @@ where renderer, ) } + + #[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> @@ -295,7 +446,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 { @@ -303,11 +456,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, @@ -348,9 +522,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; } _ => {} @@ -432,3 +639,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 6f559ccc72..b7f5878cac 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: Box Message + 'a>, label: String, @@ -80,6 +91,12 @@ where F: 'a + Fn(bool) -> Message, { Checkbox { + id: Id::unique(), + label_id: Id::unique(), + #[cfg(feature = "a11y")] + name: None, + #[cfg(feature = "a11y")] + description: None, is_checked, on_toggle: Box::new(f), label: label.into(), @@ -162,6 +179,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 @@ -194,7 +238,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 @@ -337,6 +381,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 faac0e48d8..f16caac998 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; @@ -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(self.children.as_mut_slice()); } fn size(&self) -> Size { @@ -164,7 +166,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 @@ -262,6 +264,26 @@ where ) -> Option> { overlay::from_children(&mut self.children, tree, layout, renderer) } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + 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 78ec19788d..6305b87bf1 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. @@ -152,8 +153,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 { @@ -187,10 +188,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( @@ -286,6 +287,24 @@ where renderer, ) } + + #[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> @@ -355,30 +374,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 444eb4c2ea..2f22858786 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -355,7 +355,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 6203d2c587..32b9299bc7 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 dda6162b27..bd35bdcd00 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( @@ -288,6 +291,23 @@ where has_overlay .map(|position| overlay::Element::new(position, 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 30b1efedd3..f4ba3f1bbf 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); @@ -510,6 +512,34 @@ where ) }) } + fn id(&self) -> Option { + self.with_element(|element| element.as_widget().id()) + } + + fn set_id(&mut self, _id: iced_accessibility::Id) { + self.with_element_mut(|element| element.as_widget_mut().set_id(_id)); + } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + layout: Layout<'_>, + tree: &Tree, + cursor: 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 9875b24ef0..7eb28fab58 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(); @@ -321,6 +321,39 @@ where has_overlay .map(|position| overlay::Element::new(position, Box::new(overlay))) } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + layout: Layout<'_>, + tree: &Tree, + cursor_position: 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 62bb45d827..2729e2f0a0 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -1,5 +1,7 @@ //! A container for capturing mouse events. +use iced_renderer::core::widget::OperationOutputWrapper; + use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; @@ -113,8 +115,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 { @@ -137,7 +139,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 a7b7ec9406..161c2a1ce5 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -202,7 +202,7 @@ where style, } = menu; - let container = Container::new(Scrollable::new(List { + let mut container = Container::new(Scrollable::new(List { options, hovered_option, on_selected, @@ -215,7 +215,7 @@ where style: style.clone(), })); - state.tree.diff(&container as &dyn Widget<_, _, _>); + state.tree.diff(&mut container as &mut dyn Widget<_, _, _>); Self { state: &mut state.tree, diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index c20d959a98..6cb64a4c6c 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 415dcc3ef0..ad50e41c03 100644 --- a/widget/src/pane_grid/content.rs +++ b/widget/src/pane_grid/content.rs @@ -1,10 +1,12 @@ +use iced_renderer::core::widget::{Operation, OperationOutputWrapper}; + use crate::container; use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; -use crate::core::widget::{self, Tree}; +use crate::core::widget::Tree; use crate::core::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size}; use crate::pane_grid::{Draggable, TitleBar}; @@ -74,13 +76,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(); } @@ -197,7 +199,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 3cca6b33da..85b4843fe6 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, }; @@ -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 70b1d4c07a..ec8cf37cda 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}; @@ -669,8 +671,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 89f610c990..9970c047ac 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; @@ -117,8 +119,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 { @@ -153,7 +155,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 @@ -251,6 +253,26 @@ where ) -> Option> { overlay::from_children(&mut self.children, tree, layout, renderer) } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + 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 509a6b342e..d8eb2fd34a 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -1,4 +1,7 @@ //! Navigate an endless amount of content with a scrollbar. +use iced_runtime::core::widget::Id; +use std::borrow::Cow; + use crate::core::event::{self, Event}; use crate::core::keyboard; use crate::core::layout; @@ -6,16 +9,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::{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 @@ -30,7 +33,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, @@ -49,7 +59,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(), @@ -61,7 +78,7 @@ where /// Sets the [`Id`] of the [`Scrollable`]. pub fn id(mut self, id: Id) -> Self { - self.id = Some(id); + self.id = id; self } @@ -96,6 +113,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`]. @@ -219,8 +271,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 { @@ -257,7 +309,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation>, ) { let state = tree.state.downcast_mut::(); @@ -267,25 +319,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( @@ -405,6 +448,146 @@ where overlay.translate(Vector::new(-translation.x, -translation.y)) }) } + + #[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> @@ -422,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`] @@ -461,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 5c3b63841e..b913f4c133 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -7,11 +7,13 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; +use crate::core::widget::Id; use crate::core::{ Border, Clipboard, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size, Widget, }; +use std::borrow::Cow; use std::ops::RangeInclusive; pub use iced_style::slider::{ @@ -47,6 +49,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, value: T, @@ -91,6 +100,13 @@ where }; Slider { + id: Id::unique(), + #[cfg(feature = "a11y")] + name: None, + #[cfg(feature = "a11y")] + description: None, + #[cfg(feature = "a11y")] + label: None, value, range, step: T::from(1), @@ -136,6 +152,41 @@ where self.step = 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 @@ -227,6 +278,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 0a7ed01493..b5e45ea868 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; @@ -21,9 +22,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::{ Clipboard, Element, Layout, Length, Padding, Pixels, Point, Rectangle, @@ -273,7 +274,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 @@ -319,12 +320,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( @@ -432,45 +433,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 @@ -479,12 +456,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 ee96a49362..3c2e4862a5 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() @@ -196,7 +198,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 1d313df3ba..260df6ddea 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/widget/src/tooltip.rs b/widget/src/tooltip.rs index 44cfc4b39e..cc81cc343d 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -123,10 +123,6 @@ where ] } - fn diff(&self, tree: &mut widget::Tree) { - tree.diff_children(&[self.content.as_widget(), &self.tooltip]); - } - fn state(&self) -> widget::tree::State { widget::tree::State::new(State::default()) } @@ -139,6 +135,10 @@ where self.content.as_widget().size() } + fn diff(&mut self, tree: &mut crate::core::widget::Tree) { + tree.diff_children(std::slice::from_mut(&mut self.content)) + } + fn layout( &self, tree: &mut widget::Tree, 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 21a985e8b1..80de7eded5 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 @@ -241,11 +274,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, @@ -258,7 +295,6 @@ async fn run_instance( C: Compositor + 'static, A::Theme: StyleSheet, { - use futures::stream::StreamExt; use winit::event; use winit::event_loop::ControlFlow; @@ -294,7 +330,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, @@ -308,6 +349,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(); @@ -331,8 +405,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 { .. }, @@ -410,7 +514,7 @@ async fn run_instance( }, state.cursor(), ); - redraw_pending = false; + debug.draw_finished(); if new_mouse_interaction != mouse_interaction { @@ -421,6 +525,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, @@ -592,6 +778,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( @@ -601,10 +797,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, @@ -638,8 +838,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`]. @@ -651,10 +855,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 @@ -670,17 +878,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) => { let message = tag(clipboard.read()); proxy - .send_event(message) + .send_event(UserEventWrapper::Message(message)) .expect("Send message to event loop"); } clipboard::Action::Write(contents) => { @@ -712,15 +924,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) => { @@ -728,7 +941,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) => { @@ -758,7 +973,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) => { @@ -780,7 +995,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::Screenshot(_id, tag) => { @@ -793,9 +1010,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."); } @@ -814,16 +1033,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, @@ -838,12 +1057,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))); } } } @@ -858,9 +1089,10 @@ pub fn run_command( renderer.load_font(bytes); proxy - .send_event(tagger(Ok(()))) + .send_event(UserEventWrapper::Message(tagger(Ok(())))) .expect("Send message to event loop"); } + command::Action::PlatformSpecific(_) => todo!(), } } } diff --git a/winit/src/application/state.rs b/winit/src/application/state.rs index 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 eceb160487..489c9e8902 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -815,3 +815,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 1c45ce37e0..b806e4b0da 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -2,18 +2,20 @@ mod state; mod window_manager; -pub use state::State; - +use crate::application::UserEventWrapper; use crate::conversion; use crate::core; 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; @@ -21,12 +23,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 @@ -308,10 +323,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, @@ -334,6 +355,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), @@ -371,7 +425,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(); @@ -446,9 +505,6 @@ async fn run_instance( ), )); } - event::Event::UserEvent(message) => { - messages.push(message); - } event::Event::WindowEvent { window_id: id, event: event::WindowEvent::RedrawRequested, @@ -770,6 +826,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)); + } + } } _ => {} } @@ -808,10 +1026,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, @@ -841,8 +1063,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`]. @@ -850,10 +1075,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, @@ -870,17 +1100,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) => { let message = tag(clipboard.read()); proxy - .send_event(message) + .send_event(UserEventWrapper::Message(message)) .expect("Send message to event loop"); } clipboard::Action::Write(contents) => { @@ -933,9 +1167,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"); } @@ -943,7 +1176,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"); } } @@ -955,7 +1190,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"); } } @@ -997,7 +1234,7 @@ fn run_command( }; proxy - .send_event(tag(mode)) + .send_event(UserEventWrapper::Message(tag(mode))) .expect("Event loop doesn't exist."); } } @@ -1033,7 +1270,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("Event loop doesn't exist."); } } @@ -1048,9 +1288,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."); } @@ -1070,15 +1312,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, @@ -1086,9 +1328,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()); @@ -1096,15 +1336,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), + )); } } } @@ -1124,9 +1374,12 @@ fn run_command( } proxy - .send_event(tagger(Ok(()))) + .send_event(UserEventWrapper::Message(tagger(Ok(())))) .expect("Send message to event loop"); } + command::Action::PlatformSpecific(_) => { + tracing::warn!("Platform specific commands are not supported yet in multi-window winit mode."); + } } } } From 1bc1604d586a6829cfb00a2aa7f0213bb7decdab Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 8 May 2023 12:31:32 -0400 Subject: [PATCH 02/93] feat: sctk shell fix: quad rendering including border only inside of the bounds fix: better slider drawing (it allows just the border part of the handle quad outside of the layout bouds, which isn't great, but is ok for our purposes due to being transparent) cleanup: fix & format fix: use iced_core::Font cleanup fix: allow leaving out winit & iced-sctk fix: settings fix: slider draw improvements fix: websocket example fix: modal example fix: scrollable example fix: toast example fix: avoid panicking in iced_sctk with lazy widgets in auto-size surfaces fix: todos panic fix: only diff auto-sized surfaces in iced_sctk build_user_interface & improve sctk examples wip (iced-sctk): window resize with icons feat (iced-sctk): support for setting cursor refactor: default decorations to client fix: set window geometry after receiving configure fix: size limits with no max bound must be cut off fix: send size update when autosized surface resizes fix: use ceil size for positioner cleanup: remove dbg statement fix: remove a destroyed surface from compositor surfaces fix errors after rebase and wip scaling support fix: handling of scale factor in set_logical_size fix (sctk_drag example): add .into for border radius fix: fractional scaling sctk: Fire RedrawRequests wip: animations via frame event fix / refactor iced-sctk redraw & frame event handling cleanup: note about frame request in iced-sctk fix: send resize when necessary for layer surface and popups too fix: always request redraw for a new surface fix: scaling and autosize surface improvements refactor: sctk_lazy keyboard interactivity feat(sctk): configurable natural_scroll property feat: send state and capabilities events when there are changes fix: redraw when an update is needed and clean up the logic Update sctk to latest commit Fix compilation of sctk drag example fix(sctk): update interface before checking if it has a redraw request refactor: after autosize surface resize wait to redraw until the resize has been applied refactor: better handling of autosize surfaces chore: update sctk chore: update sctk fixes sctk_drag example fix: default to ControlFlow::Wait for applications with no surface this seems to help CPU usage for app library and launcher default to 250ms timeout in the event loop Update sctk sctk: Implement xdg-activation support fix: don't require Flags to be clone for settings on wayland chore: error if neither winit or wayland feature is set chore: Allow compiling without windowing system (#65) fix(iced-sctk): handle exit_on_close_request fix: make sure that each widget operation operates on every interface This should be ok even for widget actions like focus next because there can only ever be a single focused widget cargo fmt cleanup: dbg statement fix(iced-sctk): replace panic with handling for remaining enum variants refactor: use iced clipboard for interacting with the selection refactor: allow passing an activation token when creating a window sctk: Add support for `ext-session-lock` protocol fix(sctk): build and use tree for layout of autosize surfaces Update winit to latest commit used by upstream iced fix(sctk): send key characters fix(sctk): check if key is a named key first refactor(sctk): keep compositor surface in state --- CHANGELOG.md | 4 + Cargo.toml | 18 +- core/Cargo.toml | 3 + core/src/event.rs | 6 +- core/src/event/wayland/data_device.rs | 141 ++ core/src/event/wayland/layer.rs | 10 + core/src/event/wayland/mod.rs | 45 + core/src/event/wayland/output.rs | 34 + core/src/event/wayland/popup.rs | 21 + core/src/event/wayland/seat.rs | 9 + core/src/event/wayland/session_lock.rs | 19 + core/src/event/wayland/window.rs | 12 + core/src/overlay.rs | 2 +- core/src/overlay/group.rs | 2 +- examples/modal/src/main.rs | 12 +- examples/multi_window/Cargo.toml | 2 +- examples/scrollable/src/main.rs | 3 +- examples/sctk_drag/Cargo.toml | 13 + examples/sctk_drag/src/main.rs | 249 ++ examples/sctk_lazy/Cargo.toml | 9 + examples/sctk_lazy/src/main.rs | 282 +++ examples/sctk_session_lock/Cargo.toml | 11 + examples/sctk_session_lock/src/main.rs | 102 + examples/sctk_todos/Cargo.toml | 33 + examples/sctk_todos/README.md | 20 + examples/sctk_todos/fonts/icons.ttf | Bin 0 -> 5596 bytes examples/sctk_todos/iced-todos.desktop | 4 + examples/sctk_todos/index.html | 12 + examples/sctk_todos/src/main.rs | 660 +++++ examples/system_information/Cargo.toml | 12 - examples/system_information/src/main.rs | 159 -- examples/toast/src/main.rs | 14 +- examples/todos/Cargo.toml | 2 +- examples/todos/src/main.rs | 3 +- examples/websocket/src/main.rs | 3 +- futures/Cargo.toml | 1 + renderer/Cargo.toml | 1 + runtime/Cargo.toml | 5 +- runtime/src/command.rs | 1 + runtime/src/command/platform_specific/mod.rs | 11 + .../platform_specific/wayland/activation.rs | 67 + .../platform_specific/wayland/data_device.rs | 137 + .../wayland/layer_surface.rs | 224 ++ .../command/platform_specific/wayland/mod.rs | 76 + .../platform_specific/wayland/popup.rs | 178 ++ .../platform_specific/wayland/session_lock.rs | 80 + .../platform_specific/wayland/window.rs | 311 +++ runtime/src/program/state.rs | 8 + runtime/src/window.rs | 28 +- sctk/Cargo.toml | 54 + sctk/LICENSE.md | 359 +++ sctk/src/adaptor.rs | 42 + sctk/src/application.rs | 2196 +++++++++++++++++ sctk/src/clipboard.rs | 81 + sctk/src/commands/activation.rs | 30 + sctk/src/commands/data_device.rs | 119 + sctk/src/commands/layer_surface.rs | 123 + sctk/src/commands/mod.rs | 8 + sctk/src/commands/popup.rs | 54 + sctk/src/commands/session_lock.rs | 48 + sctk/src/commands/window.rs | 87 + sctk/src/conversion.rs | 89 + sctk/src/dpi.rs | 613 +++++ sctk/src/error.rs | 23 + sctk/src/event_loop/adapter.rs | 34 + sctk/src/event_loop/control_flow.rs | 56 + sctk/src/event_loop/mod.rs | 1373 +++++++++++ sctk/src/event_loop/proxy.rs | 66 + sctk/src/event_loop/state.rs | 851 +++++++ sctk/src/handlers/activation.rs | 60 + sctk/src/handlers/compositor.rs | 45 + sctk/src/handlers/data_device/data_device.rs | 140 ++ sctk/src/handlers/data_device/data_offer.rs | 57 + sctk/src/handlers/data_device/data_source.rs | 200 ++ sctk/src/handlers/data_device/mod.rs | 9 + sctk/src/handlers/mod.rs | 41 + sctk/src/handlers/output.rs | 48 + sctk/src/handlers/seat/keyboard.rs | 200 ++ sctk/src/handlers/seat/mod.rs | 5 + sctk/src/handlers/seat/pointer.rs | 163 ++ sctk/src/handlers/seat/seat.rs | 191 ++ sctk/src/handlers/seat/touch.rs | 1 + sctk/src/handlers/session_lock.rs | 57 + sctk/src/handlers/shell/layer.rs | 113 + sctk/src/handlers/shell/mod.rs | 3 + sctk/src/handlers/shell/xdg_popup.rs | 86 + sctk/src/handlers/shell/xdg_window.rs | 116 + sctk/src/handlers/wp_fractional_scaling.rs | 97 + sctk/src/handlers/wp_viewporter.rs | 80 + sctk/src/keymap.rs | 475 ++++ sctk/src/lib.rs | 25 + sctk/src/result.rs | 6 + sctk/src/sctk_event.rs | 960 +++++++ sctk/src/settings.rs | 32 + sctk/src/system.rs | 41 + sctk/src/util.rs | 128 + sctk/src/widget.rs | 232 ++ sctk/src/window.rs | 3 + src/application.rs | 2 + src/error.rs | 7 + src/lib.rs | 43 +- src/settings.rs | 108 +- src/wayland/mod.rs | 196 ++ src/wayland/sandbox.rs | 207 ++ src/window.rs | 5 + widget/Cargo.toml | 4 +- widget/src/dnd_listener.rs | 511 ++++ widget/src/dnd_source.rs | 423 ++++ widget/src/helpers.rs | 31 +- widget/src/lazy/component.rs | 4 +- widget/src/lazy/responsive.rs | 2 +- widget/src/lib.rs | 6 + widget/src/slider.rs | 33 +- widget/src/svg.rs | 11 +- widget/src/text_input/mod.rs | 10 + widget/src/{ => text_input}/text_input.rs | 14 +- winit/src/conversion.rs | 3 + 117 files changed, 14365 insertions(+), 234 deletions(-) create mode 100644 core/src/event/wayland/data_device.rs create mode 100644 core/src/event/wayland/layer.rs create mode 100644 core/src/event/wayland/mod.rs create mode 100644 core/src/event/wayland/output.rs create mode 100644 core/src/event/wayland/popup.rs create mode 100644 core/src/event/wayland/seat.rs create mode 100644 core/src/event/wayland/session_lock.rs create mode 100644 core/src/event/wayland/window.rs create mode 100644 examples/sctk_drag/Cargo.toml create mode 100644 examples/sctk_drag/src/main.rs create mode 100644 examples/sctk_lazy/Cargo.toml create mode 100644 examples/sctk_lazy/src/main.rs create mode 100644 examples/sctk_session_lock/Cargo.toml create mode 100644 examples/sctk_session_lock/src/main.rs create mode 100644 examples/sctk_todos/Cargo.toml create mode 100644 examples/sctk_todos/README.md create mode 100644 examples/sctk_todos/fonts/icons.ttf create mode 100644 examples/sctk_todos/iced-todos.desktop create mode 100644 examples/sctk_todos/index.html create mode 100644 examples/sctk_todos/src/main.rs delete mode 100644 examples/system_information/Cargo.toml delete mode 100644 examples/system_information/src/main.rs create mode 100644 runtime/src/command/platform_specific/wayland/activation.rs create mode 100644 runtime/src/command/platform_specific/wayland/data_device.rs create mode 100644 runtime/src/command/platform_specific/wayland/layer_surface.rs create mode 100644 runtime/src/command/platform_specific/wayland/mod.rs create mode 100644 runtime/src/command/platform_specific/wayland/popup.rs create mode 100644 runtime/src/command/platform_specific/wayland/session_lock.rs create mode 100644 runtime/src/command/platform_specific/wayland/window.rs create mode 100644 sctk/Cargo.toml create mode 100644 sctk/LICENSE.md create mode 100644 sctk/src/adaptor.rs create mode 100644 sctk/src/application.rs create mode 100644 sctk/src/clipboard.rs create mode 100644 sctk/src/commands/activation.rs create mode 100644 sctk/src/commands/data_device.rs create mode 100644 sctk/src/commands/layer_surface.rs create mode 100644 sctk/src/commands/mod.rs create mode 100644 sctk/src/commands/popup.rs create mode 100644 sctk/src/commands/session_lock.rs create mode 100644 sctk/src/commands/window.rs create mode 100644 sctk/src/conversion.rs create mode 100644 sctk/src/dpi.rs create mode 100644 sctk/src/error.rs create mode 100644 sctk/src/event_loop/adapter.rs create mode 100644 sctk/src/event_loop/control_flow.rs create mode 100644 sctk/src/event_loop/mod.rs create mode 100644 sctk/src/event_loop/proxy.rs create mode 100644 sctk/src/event_loop/state.rs create mode 100644 sctk/src/handlers/activation.rs create mode 100644 sctk/src/handlers/compositor.rs create mode 100644 sctk/src/handlers/data_device/data_device.rs create mode 100644 sctk/src/handlers/data_device/data_offer.rs create mode 100644 sctk/src/handlers/data_device/data_source.rs create mode 100644 sctk/src/handlers/data_device/mod.rs create mode 100644 sctk/src/handlers/mod.rs create mode 100644 sctk/src/handlers/output.rs create mode 100644 sctk/src/handlers/seat/keyboard.rs create mode 100644 sctk/src/handlers/seat/mod.rs create mode 100644 sctk/src/handlers/seat/pointer.rs create mode 100644 sctk/src/handlers/seat/seat.rs create mode 100644 sctk/src/handlers/seat/touch.rs create mode 100644 sctk/src/handlers/session_lock.rs create mode 100644 sctk/src/handlers/shell/layer.rs create mode 100644 sctk/src/handlers/shell/mod.rs create mode 100644 sctk/src/handlers/shell/xdg_popup.rs create mode 100644 sctk/src/handlers/shell/xdg_window.rs create mode 100644 sctk/src/handlers/wp_fractional_scaling.rs create mode 100644 sctk/src/handlers/wp_viewporter.rs create mode 100644 sctk/src/keymap.rs create mode 100644 sctk/src/lib.rs create mode 100644 sctk/src/result.rs create mode 100755 sctk/src/sctk_event.rs create mode 100644 sctk/src/settings.rs create mode 100644 sctk/src/system.rs create mode 100644 sctk/src/util.rs create mode 100644 sctk/src/widget.rs create mode 100644 sctk/src/window.rs create mode 100644 src/wayland/mod.rs create mode 100644 src/wayland/sandbox.rs create mode 100644 widget/src/dnd_listener.rs create mode 100644 widget/src/dnd_source.rs create mode 100644 widget/src/text_input/mod.rs rename widget/src/{ => text_input}/text_input.rs (99%) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdd832e495..3226c1ba30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,10 @@ Many thanks to... - @akshayr-mecha - @dtzxporter +Many thanks to... +- @jackpot51 +- @wash2 + ## [0.10.0] - 2023-07-28 ### Added - Text shaping, font fallback, and `iced_wgpu` overhaul. [#1697](https://github.com/iced-rs/iced/pull/1697) diff --git a/Cargo.toml b/Cargo.toml index 56ba17326e..f7098325b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ all-features = true maintenance = { status = "actively-developed" } [features] -default = ["wgpu", "winit", "multi-window", "a11y"] +default = ["wgpu", "winit", "multi-window"] # Enable the `wgpu` GPU-accelerated renderer backend wgpu = ["iced_renderer/wgpu", "iced_widget/wgpu"] # Enables the `Image` widget @@ -32,7 +32,7 @@ qr_code = ["iced_widget/qr_code"] # Enables lazy widgets lazy = ["iced_widget/lazy"] # Enables a debug view in native platforms (press F12) -debug = ["iced_winit/debug"] +debug = ["iced_winit?/debug", "iced_sctk?/debug"] # Enables `tokio` as the `executor::Default` on native platforms tokio = ["iced_futures/tokio"] # Enables `async-std` as the `executor::Default` on native platforms @@ -41,8 +41,6 @@ async-std = ["iced_futures/async-std"] smol = ["iced_futures/smol"] # Enables advanced color conversion via `palette` palette = ["iced_core/palette"] -# Enables querying system information -system = ["iced_winit/system"] # Enables broken "sRGB linear" blending to reproduce color management of the Web web-colors = ["iced_renderer/web-colors"] # Enables the WebGL backend, replacing WebGPU @@ -50,13 +48,15 @@ webgl = ["iced_renderer/webgl"] # Enables the syntax `highlighter` module highlighter = ["iced_highlighter"] # Enables experimental multi-window support. -multi-window = ["iced_winit/multi-window"] +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"] +a11y = ["iced_accessibility", "iced_core/a11y", "iced_widget/a11y", "iced_winit?/a11y", "iced_sctk?/a11y"] # Enables the winit shell. Conflicts with `wayland` and `glutin`. winit = ["iced_winit", "iced_accessibility?/accesskit_winit"] +# Enables the sctk shell. COnflicts with `winit` and `glutin`. +wayland = ["iced_sctk", "iced_widget/wayland", "iced_accessibility?/accesskit_unix", "iced_core/wayland", "multi-window"] [dependencies] iced_core.workspace = true @@ -66,6 +66,8 @@ iced_widget.workspace = true iced_winit.features = ["application"] iced_winit.workspace = true iced_winit.optional = true +iced_sctk.workspace = true +iced_sctk.optional = true iced_highlighter.workspace = true iced_highlighter.optional = true iced_accessibility.workspace = true @@ -90,6 +92,7 @@ members = [ "winit", "examples/*", "accessibility", + "sctk" ] [profile.release-opt] @@ -125,6 +128,7 @@ 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", features = ["application"] } +iced_sctk = { version = "0.1", path = "sctk" } iced_accessibility = { version = "0.1", path = "accessibility" } async-std = "1.0" @@ -150,6 +154,7 @@ qrcode = { version = "0.12", default-features = false } raw-window-handle = "0.6" resvg = "0.36" rustc-hash = "1.0" +sctk = { package = "smithay-client-toolkit", git = "https://github.com/smithay/client-toolkit", rev = "2e9bf9f" } smol = "1.0" smol_str = "0.2" softbuffer = "0.4" @@ -163,6 +168,7 @@ xxhash-rust = { version = "0.8", features = ["xxh3"] } unicode-segmentation = "1.0" wasm-bindgen-futures = "0.4" wasm-timer = "0.2" +wayland-protocols = { version = "0.31.0", features = [ "staging"]} web-sys = "0.3" web-time = "0.2" wgpu = "0.19" diff --git a/core/Cargo.toml b/core/Cargo.toml index 67f7137e62..bb49c232d7 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -12,6 +12,7 @@ keywords.workspace = true [features] a11y = ["iced_accessibility"] +wayland = ["iced_accessibility?/accesskit_unix", "sctk"] [dependencies] bitflags.workspace = true @@ -22,6 +23,8 @@ thiserror.workspace = true web-time.workspace = true xxhash-rust.workspace = true +sctk.workspace = true +sctk.optional = true palette.workspace = true palette.optional = true diff --git a/core/src/event.rs b/core/src/event.rs index 9e28e9ea48..f029b40d14 100644 --- a/core/src/event.rs +++ b/core/src/event.rs @@ -3,7 +3,9 @@ use crate::keyboard; use crate::mouse; use crate::touch; use crate::window; - +#[cfg(feature = "wayland")] +/// A platform specific event for wayland +pub mod wayland; /// A user interface event. /// /// _**Note:** This type is largely incomplete! If you need to track @@ -36,7 +38,7 @@ pub enum Event { } /// A platform specific event -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq)] pub enum PlatformSpecific { /// A Wayland specific event #[cfg(feature = "wayland")] diff --git a/core/src/event/wayland/data_device.rs b/core/src/event/wayland/data_device.rs new file mode 100644 index 0000000000..efd4ed1c12 --- /dev/null +++ b/core/src/event/wayland/data_device.rs @@ -0,0 +1,141 @@ +use sctk::{ + data_device_manager::{data_offer::DragOffer, ReadPipe}, + reexports::client::protocol::wl_data_device_manager::DndAction, +}; +use std::{ + os::fd::{AsRawFd, OwnedFd, RawFd}, + sync::{Arc, Mutex}, +}; + +/// Dnd Offer events +#[derive(Debug, Clone, PartialEq)] +pub enum DndOfferEvent { + /// A DnD offer has been introduced with the given mime types. + Enter { + /// x coordinate of the offer + x: f64, + /// y coordinate of the offer + y: f64, + /// The offered mime types + mime_types: Vec, + }, + /// The DnD device has left. + Leave, + /// Drag and Drop Motion event. + Motion { + /// x coordinate of the pointer + x: f64, + /// y coordinate of the pointer + y: f64, + }, + /// The selected DnD action + SelectedAction(DndAction), + /// The offered actions for the current DnD offer + SourceActions(DndAction), + /// Dnd Drop event + DropPerformed, + /// Raw DnD Data + DndData { + /// The data + data: Vec, + /// The mime type of the data + mime_type: String, + }, + /// Raw Selection Data + SelectionData { + /// The data + data: Vec, + /// The mime type of the data + mime_type: String, + }, + /// Selection Offer + /// a selection offer has been introduced with the given mime types. + SelectionOffer(Vec), +} + +/// Selection Offer events +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SelectionOfferEvent { + /// a selection offer has been introduced with the given mime types. + Offer(Vec), + /// Read the Selection data + Data { + /// The mime type that the selection should be converted to. + mime_type: String, + /// The data + data: Vec, + }, +} + +/// A ReadPipe and the mime type of the data. +#[derive(Debug, Clone)] +pub struct ReadData { + /// mime type of the data + pub mime_type: String, + /// The pipe to read the data from + pub fd: Arc>, +} + +impl ReadData { + /// Create a new ReadData + pub fn new(mime_type: String, fd: Arc>) -> Self { + Self { mime_type, fd } + } +} + +/// Data Source events +/// Includes drag and drop events and clipboard events +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DataSourceEvent { + /// A Dnd action was selected by the compositor for your source. + DndActionAccepted(DndAction), + /// A mime type was accepted by a client for your source. + MimeAccepted(Option), + /// Some client has requested the DnD data. + /// This is used to send the data to the client. + SendDndData(String), + /// Some client has requested the selection data. + /// This is used to send the data to the client. + SendSelectionData(String), + /// The data source has been cancelled and is no longer valid. + /// This may be sent for multiple reasons + Cancelled, + /// Dnd Finished + DndFinished, + /// Dnd Drop event + DndDropPerformed, +} + +/// A WriteData and the mime type of the data to be written. +#[derive(Debug, Clone)] +pub struct WriteData { + /// mime type of the data + pub mime_type: String, + /// The fd to write the data to + pub fd: Arc>, +} + +impl WriteData { + /// Create a new WriteData + pub fn new(mime_type: String, fd: Arc>) -> Self { + Self { mime_type, fd } + } +} + +impl PartialEq for WriteData { + fn eq(&self, other: &Self) -> bool { + self.fd.lock().unwrap().as_raw_fd() + == other.fd.lock().unwrap().as_raw_fd() + } +} + +impl Eq for WriteData {} + +impl PartialEq for ReadData { + fn eq(&self, other: &Self) -> bool { + self.fd.lock().unwrap().as_raw_fd() + == other.fd.lock().unwrap().as_raw_fd() + } +} + +impl Eq for ReadData {} diff --git a/core/src/event/wayland/layer.rs b/core/src/event/wayland/layer.rs new file mode 100644 index 0000000000..c1928ad36e --- /dev/null +++ b/core/src/event/wayland/layer.rs @@ -0,0 +1,10 @@ +/// layer surface events +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum LayerEvent { + /// layer surface Done + Done, + /// layer surface focused + Focused, + /// layer_surface unfocused + Unfocused, +} diff --git a/core/src/event/wayland/mod.rs b/core/src/event/wayland/mod.rs new file mode 100644 index 0000000000..bf3ddcb1b0 --- /dev/null +++ b/core/src/event/wayland/mod.rs @@ -0,0 +1,45 @@ +mod data_device; +mod layer; +mod output; +mod popup; +mod seat; +mod session_lock; +mod window; + +use crate::{time::Instant, window::Id}; +use sctk::reexports::client::protocol::{ + wl_output::WlOutput, wl_seat::WlSeat, wl_surface::WlSurface, +}; + +pub use data_device::*; +pub use layer::*; +pub use output::*; +pub use popup::*; +pub use seat::*; +pub use session_lock::*; +pub use window::*; + +/// wayland events +#[derive(Debug, Clone, PartialEq)] +pub enum Event { + /// layer surface event + Layer(LayerEvent, WlSurface, Id), + /// popup event + Popup(PopupEvent, WlSurface, Id), + /// output event + Output(OutputEvent, WlOutput), + /// window event + Window(WindowEvent, WlSurface, Id), + /// Seat Event + Seat(SeatEvent, WlSeat), + /// Data Device event + DataSource(DataSourceEvent), + /// Dnd Offer events + DndOffer(DndOfferEvent), + /// Selection Offer events + SelectionOffer(SelectionOfferEvent), + /// Session lock events + SessionLock(SessionLockEvent), + /// Frame events + Frame(Instant, WlSurface, Id), +} diff --git a/core/src/event/wayland/output.rs b/core/src/event/wayland/output.rs new file mode 100644 index 0000000000..c5024e85b7 --- /dev/null +++ b/core/src/event/wayland/output.rs @@ -0,0 +1,34 @@ +use sctk::output::OutputInfo; + +/// output events +#[derive(Debug, Clone)] +pub enum OutputEvent { + /// created output + Created(Option), + /// removed output + Removed, + /// Output Info + InfoUpdate(OutputInfo), +} + +impl Eq for OutputEvent {} + +impl PartialEq for OutputEvent { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Created(l0), Self::Created(r0)) => { + if let Some((l0, r0)) = l0.as_ref().zip(r0.as_ref()) { + l0.id == r0.id && l0.make == r0.make && l0.model == r0.model + } else { + l0.is_none() && r0.is_none() + } + } + (Self::InfoUpdate(l0), Self::InfoUpdate(r0)) => { + l0.id == r0.id && l0.make == r0.make && l0.model == r0.model + } + _ => { + core::mem::discriminant(self) == core::mem::discriminant(other) + } + } + } +} diff --git a/core/src/event/wayland/popup.rs b/core/src/event/wayland/popup.rs new file mode 100644 index 0000000000..ff925870b2 --- /dev/null +++ b/core/src/event/wayland/popup.rs @@ -0,0 +1,21 @@ +/// popup events +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PopupEvent { + /// Done + Done, + /// repositioned, + Configured { + /// x position + x: i32, + /// y position + y: i32, + /// width + width: u32, + /// height + height: u32, + }, + /// popup focused + Focused, + /// popup unfocused + Unfocused, +} diff --git a/core/src/event/wayland/seat.rs b/core/src/event/wayland/seat.rs new file mode 100644 index 0000000000..3da4374e71 --- /dev/null +++ b/core/src/event/wayland/seat.rs @@ -0,0 +1,9 @@ +/// seat events +/// Only one seat can interact with an iced_sctk application at a time, but many may interact with the application over the lifetime of the application +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SeatEvent { + /// A new seat is interacting with the application + Enter, + /// A seat is not interacting with the application anymore + Leave, +} diff --git a/core/src/event/wayland/session_lock.rs b/core/src/event/wayland/session_lock.rs new file mode 100644 index 0000000000..db99566d95 --- /dev/null +++ b/core/src/event/wayland/session_lock.rs @@ -0,0 +1,19 @@ +use crate::window::Id; +use sctk::reexports::client::protocol::wl_surface::WlSurface; + +/// session lock events +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SessionLockEvent { + /// Compositor has activated lock + Locked, + /// Lock rejected / canceled by compositor + Finished, + /// Session lock protocol not supported + NotSupported, + /// Session lock surface focused + Focused(WlSurface, Id), + /// Session lock surface unfocused + Unfocused(WlSurface, Id), + /// Session unlock has been processed by server + Unlocked, +} diff --git a/core/src/event/wayland/window.rs b/core/src/event/wayland/window.rs new file mode 100644 index 0000000000..210b1ce1ca --- /dev/null +++ b/core/src/event/wayland/window.rs @@ -0,0 +1,12 @@ +#![allow(missing_docs)] + +use sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState}; + +/// window events +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum WindowEvent { + /// window manager capabilities + WmCapabilities(WindowManagerCapabilities), + /// window state + State(WindowState), +} diff --git a/core/src/overlay.rs b/core/src/overlay.rs index d440e553b2..cc32abc2b6 100644 --- a/core/src/overlay.rs +++ b/core/src/overlay.rs @@ -9,7 +9,7 @@ use crate::event::{self, Event}; use crate::layout; use crate::mouse; use crate::renderer; -use crate::widget::{self, Operation}; +use crate::widget::Operation; use crate::widget::{OperationOutputWrapper, Tree}; use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size, Vector}; diff --git a/core/src/overlay/group.rs b/core/src/overlay/group.rs index 19f818ae7d..9d2ec8afa1 100644 --- a/core/src/overlay/group.rs +++ b/core/src/overlay/group.rs @@ -3,7 +3,7 @@ use crate::layout; use crate::mouse; use crate::overlay; use crate::renderer; -use crate::widget; + use crate::widget::Operation; use crate::widget::OperationOutputWrapper; use crate::{ diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index c2a4132c66..628a033332 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -226,7 +226,9 @@ mod modal { use iced::advanced::layout::{self, Layout}; use iced::advanced::overlay; use iced::advanced::renderer; - use iced::advanced::widget::{self, Widget}; + use iced::advanced::widget::{ + self, Operation, OperationOutputWrapper, Widget, + }; use iced::advanced::{self, Clipboard, Shell}; use iced::alignment::Alignment; use iced::event; @@ -276,8 +278,8 @@ mod modal { ] } - fn diff(&self, tree: &mut widget::Tree) { - tree.diff_children(&[&self.base, &self.modal]); + fn diff(&mut self, tree: &mut widget::Tree) { + tree.diff_children(&mut [&mut self.base, &mut self.modal]); } fn size(&self) -> Size { @@ -380,7 +382,7 @@ mod modal { state: &mut widget::Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { self.base.as_widget().operate( &mut state.children[0], @@ -495,7 +497,7 @@ mod modal { &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { self.content.as_widget().operate( self.tree, diff --git a/examples/multi_window/Cargo.toml b/examples/multi_window/Cargo.toml index 2e222dfbb1..f7c25082dd 100644 --- a/examples/multi_window/Cargo.toml +++ b/examples/multi_window/Cargo.toml @@ -6,4 +6,4 @@ edition = "2021" publish = false [dependencies] -iced = { path = "../..", features = ["debug", "multi-window"] } +iced = { path = "../..", features = ["debug", "winit", "multi-window"] } diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index ff69191724..07348b35cb 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -1,4 +1,5 @@ use iced::executor; +use iced::id::Id; use iced::theme; use iced::widget::scrollable::{Properties, Scrollbar, Scroller}; use iced::widget::{ @@ -12,7 +13,7 @@ use iced::{ use once_cell::sync::Lazy; -static SCROLLABLE_ID: Lazy = Lazy::new(scrollable::Id::unique); +static SCROLLABLE_ID: Lazy = Lazy::new(|| Id::new("scrollable")); pub fn main() -> iced::Result { ScrollableDemo::run(Settings::default()) diff --git a/examples/sctk_drag/Cargo.toml b/examples/sctk_drag/Cargo.toml new file mode 100644 index 0000000000..83c0ad38c4 --- /dev/null +++ b/examples/sctk_drag/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "sctk_drag" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +iced = { path = "../..", default-features = false, features = ["wayland", "debug", "a11y"] } +iced_style = { path = "../../style" } +env_logger = "0.10" +# sctk = { package = "smithay-client-toolkit", path = "../../../fork/client-toolkit/" } +sctk.workspace = true diff --git a/examples/sctk_drag/src/main.rs b/examples/sctk_drag/src/main.rs new file mode 100644 index 0000000000..c5dcf2ad41 --- /dev/null +++ b/examples/sctk_drag/src/main.rs @@ -0,0 +1,249 @@ +use iced::{ + event::wayland::DataSourceEvent, + subscription, + wayland::{ + actions::data_device::DataFromMimeType, data_device::start_drag, + }, + wayland::{ + actions::data_device::DndIcon, + data_device::{ + accept_mime_type, finish_dnd, request_dnd_data, set_actions, + }, + layer_surface::destroy_layer_surface, + InitialSurface, + }, + widget::{self, column, container, dnd_listener, dnd_source, text}, + window, Application, Color, Command, Element, Length, Subscription, Theme, +}; +use iced_style::application; +use sctk::reexports::client::protocol::wl_data_device_manager::DndAction; +use sctk::shell::wlr_layer::Anchor; + +fn main() { + let mut settings = iced::Settings::default(); + match &mut settings.initial_surface { + InitialSurface::LayerSurface(s) => { + s.size_limits = s.size_limits.min_width(100.0).max_width(400.0); + s.size = Some((Some(400), None)); + s.anchor = Anchor::TOP.union(Anchor::BOTTOM); + } + _ => {} + }; + DndTest::run(settings).unwrap(); +} + +const SUPPORTED_MIME_TYPES: &'static [&'static str; 6] = &[ + "text/plain;charset=utf-8", + "text/plain;charset=UTF-8", + "UTF8_STRING", + "STRING", + "text/plain", + "TEXT", +]; + +#[derive(Debug, Clone, Default)] +enum DndState { + #[default] + None, + Some(Vec), + Drop, +} + +pub struct MyDndString(String); + +impl DataFromMimeType for MyDndString { + fn from_mime_type(&self, mime_type: &str) -> Option> { + if SUPPORTED_MIME_TYPES.contains(&mime_type) { + Some(self.0.as_bytes().to_vec()) + } else { + None + } + } +} + +#[derive(Debug, Clone, Default)] +pub struct DndTest { + /// option with the dragged text + source: Option, + /// is the dnd over the target + target: DndState, + current_text: String, +} + +#[derive(Debug, Clone)] +pub enum Message { + Enter(Vec), + Leave, + Drop, + DndData(Vec), + Ignore, + StartDnd, + SourceFinished, +} + +impl Application for DndTest { + type Executor = iced::executor::Default; + type Message = Message; + type Flags = (); + type Theme = Theme; + + fn new(_flags: ()) -> (DndTest, Command) { + let current_text = String::from("Hello, world!"); + + ( + DndTest { + current_text, + ..DndTest::default() + }, + Command::none(), + ) + } + + fn title(&self, id: window::Id) -> String { + String::from("DndTest") + } + + fn update(&mut self, message: Self::Message) -> Command { + match message { + Message::Enter(mut mime_types) => { + println!("Enter: {:?}", mime_types); + let mut cmds = + vec![set_actions(DndAction::Copy, DndAction::all())]; + mime_types.retain(|mime_type| { + SUPPORTED_MIME_TYPES.contains(&mime_type.as_str()) + }); + for m in &mime_types { + cmds.push(accept_mime_type(Some(m.clone()))); + } + + self.target = DndState::Some(mime_types); + return Command::batch(cmds); + } + Message::Leave => { + self.target = DndState::None; + return Command::batch(vec![ + accept_mime_type(None), + set_actions(DndAction::None, DndAction::empty()), + ]); + } + Message::Drop => { + if let DndState::Some(m) = &self.target { + let m = m[0].clone(); + println!("Drop: {:?}", self.target); + self.target = DndState::Drop; + return request_dnd_data(m.clone()); + } + } + Message::DndData(data) => { + println!("DndData: {:?}", data); + if data.is_empty() { + return Command::none(); + } + if matches!(self.target, DndState::Drop) { + self.current_text = String::from_utf8(data).unwrap(); + self.target = DndState::None; + // Sent automatically now after a successful read of data following a drop. + // No longer needed here + // return finish_dnd(); + } + } + Message::SourceFinished => { + println!("Removing source"); + self.source = None; + } + Message::StartDnd => { + println!("Starting DnD"); + self.source = Some(self.current_text.clone()); + return start_drag( + SUPPORTED_MIME_TYPES + .iter() + .map(|t| t.to_string()) + .collect(), + DndAction::Move, + window::Id::unique(), + Some(DndIcon::Custom(iced::window::Id::unique())), + Box::new(MyDndString( + self.current_text.chars().rev().collect::(), + )), + ); + } + Message::Ignore => {} + } + Command::none() + } + + fn view(&self, id: window::Id) -> Element { + if id != iced::window::Id::MAIN { + return text(&self.current_text).into(); + } + column![ + dnd_listener( + container(text(format!( + "Drag text here: {}", + &self.current_text + ))) + .width(Length::Fill) + .height(Length::FillPortion(1)) + .style(if matches!(self.target, DndState::Some(_)) { + ::Style::Custom( + Box::new(CustomTheme), + ) + } else { + Default::default() + }) + .padding(20) + ) + .on_enter(|_, mime_types: Vec, _| { + if mime_types.iter().any(|mime_type| { + SUPPORTED_MIME_TYPES.contains(&mime_type.as_str()) + }) { + Message::Enter(mime_types) + } else { + Message::Ignore + } + }) + .on_exit(Message::Leave) + .on_drop(Message::Drop) + .on_data(|mime_type, data| { + if matches!(self.target, DndState::Drop) { + Message::DndData(data) + } else { + Message::Ignore + } + }), + dnd_source( + container(text(format!( + "Drag me: {}", + &self.current_text.chars().rev().collect::() + ))) + .width(Length::Fill) + .height(Length::FillPortion(1)) + .style(if self.source.is_some() { + ::Style::Custom( + Box::new(CustomTheme), + ) + } else { + Default::default() + }) + .padding(20) + ) + .drag_threshold(5.0) + .on_drag(|_| Message::StartDnd) + .on_finished(Message::SourceFinished) + .on_cancelled(Message::SourceFinished) + ] + .into() + } +} + +pub struct CustomTheme; + +impl container::StyleSheet for CustomTheme { + type Style = iced::Theme; + + fn appearance(&self, style: &Self::Style) -> container::Appearance { + container::Appearance { + ..container::Appearance::default() + } + } +} diff --git a/examples/sctk_lazy/Cargo.toml b/examples/sctk_lazy/Cargo.toml new file mode 100644 index 0000000000..ed06b4a159 --- /dev/null +++ b/examples/sctk_lazy/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "sctk_lazy" +version = "0.1.0" +authors = ["Nick Senger "] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../..", features = ["debug", "lazy", "wayland", "advanced"], default-features = false } diff --git a/examples/sctk_lazy/src/main.rs b/examples/sctk_lazy/src/main.rs new file mode 100644 index 0000000000..72f559320d --- /dev/null +++ b/examples/sctk_lazy/src/main.rs @@ -0,0 +1,282 @@ +use iced::advanced::layout::Limits; +use iced::theme; +use iced::wayland::actions::layer_surface::SctkLayerSurfaceSettings; +use iced::wayland::layer_surface::KeyboardInteractivity; +use iced::wayland::InitialSurface; +use iced::widget::{ + button, column, horizontal_space, lazy, pick_list, row, scrollable, text, + text_input, +}; +use iced::window::Id; +use iced::{Element, Length, Sandbox, Settings}; + +use std::collections::HashSet; +use std::hash::Hash; + +pub fn main() -> iced::Result { + let mut initial_surface = SctkLayerSurfaceSettings::default(); + initial_surface.keyboard_interactivity = KeyboardInteractivity::OnDemand; + initial_surface.size_limits = Limits::NONE + .min_width(1.0) + .min_height(1.0) + .max_height(500.0) + .max_width(900.0); + let settings = Settings { + initial_surface: InitialSurface::LayerSurface(initial_surface), + ..Settings::default() + }; + App::run(settings) +} + +struct App { + version: u8, + items: HashSet, + input: String, + order: Order, +} + +impl Default for App { + fn default() -> Self { + Self { + version: 0, + items: ["Foo", "Bar", "Baz", "Qux", "Corge", "Waldo", "Fred"] + .into_iter() + .map(From::from) + .collect(), + input: Default::default(), + order: Order::Ascending, + } + } +} + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +enum Color { + #[default] + Black, + Red, + Orange, + Yellow, + Green, + Blue, + Purple, +} + +impl Color { + const ALL: &'static [Color] = &[ + Color::Black, + Color::Red, + Color::Orange, + Color::Yellow, + Color::Green, + Color::Blue, + Color::Purple, + ]; +} + +impl std::fmt::Display for Color { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Black => "Black", + Self::Red => "Red", + Self::Orange => "Orange", + Self::Yellow => "Yellow", + Self::Green => "Green", + Self::Blue => "Blue", + Self::Purple => "Purple", + }) + } +} + +impl From for iced::Color { + fn from(value: Color) -> Self { + match value { + Color::Black => iced::Color::from_rgb8(0, 0, 0), + Color::Red => iced::Color::from_rgb8(220, 50, 47), + Color::Orange => iced::Color::from_rgb8(203, 75, 22), + Color::Yellow => iced::Color::from_rgb8(181, 137, 0), + Color::Green => iced::Color::from_rgb8(133, 153, 0), + Color::Blue => iced::Color::from_rgb8(38, 139, 210), + Color::Purple => iced::Color::from_rgb8(108, 113, 196), + } + } +} + +#[derive(Clone, Debug, Eq)] +struct Item { + name: String, + color: Color, +} + +impl Hash for Item { + fn hash(&self, state: &mut H) { + self.name.hash(state); + } +} + +impl PartialEq for Item { + fn eq(&self, other: &Self) -> bool { + self.name.eq(&other.name) + } +} + +impl From<&str> for Item { + fn from(s: &str) -> Self { + Self { + name: s.to_owned(), + color: Default::default(), + } + } +} + +#[derive(Debug, Clone)] +enum Message { + InputChanged(String), + ToggleOrder, + DeleteItem(Item), + AddItem(String), + ItemColorChanged(Item, Color), +} + +impl Sandbox for App { + type Message = Message; + + fn new() -> Self { + Self::default() + } + + fn title(&self) -> String { + String::from("Lazy - Iced") + } + + fn update(&mut self, message: Message) { + match message { + Message::InputChanged(input) => { + self.input = input; + } + Message::ToggleOrder => { + self.version = self.version.wrapping_add(1); + self.order = match self.order { + Order::Ascending => Order::Descending, + Order::Descending => Order::Ascending, + } + } + Message::AddItem(name) => { + self.version = self.version.wrapping_add(1); + self.items.insert(name.as_str().into()); + self.input.clear(); + } + Message::DeleteItem(item) => { + self.version = self.version.wrapping_add(1); + self.items.remove(&item); + } + Message::ItemColorChanged(item, color) => { + self.version = self.version.wrapping_add(1); + if self.items.remove(&item) { + self.items.insert(Item { + name: item.name, + color, + }); + } + } + } + } + + fn view(&self, _: Id) -> Element { + let options = lazy(self.version, |_| { + let mut items: Vec<_> = self.items.iter().cloned().collect(); + + items.sort_by(|a, b| match self.order { + Order::Ascending => { + a.name.to_lowercase().cmp(&b.name.to_lowercase()) + } + Order::Descending => { + b.name.to_lowercase().cmp(&a.name.to_lowercase()) + } + }); + + column( + items + .into_iter() + .map(|item| { + let button = button("Delete") + .on_press(Message::DeleteItem(item.clone())) + .style(theme::Button::Destructive); + + row![ + text(&item.name) + .style(theme::Text::Color(item.color.into())), + horizontal_space(Length::Fill), + pick_list( + Color::ALL, + Some(item.color), + move |color| { + Message::ItemColorChanged( + item.clone(), + color, + ) + } + ), + button + ] + .spacing(20) + .into() + }) + .collect(), + ) + .spacing(10) + }); + + column![ + scrollable(options).height(Length::Fill), + row![ + text_input("Add a new option", &self.input) + .on_input(Message::InputChanged) + .on_submit(Message::AddItem(self.input.clone())), + button(text(format!("Toggle Order ({})", self.order))) + .on_press(Message::ToggleOrder) + ] + .spacing(10) + ] + .spacing(20) + .padding(20) + .into() + } + + fn theme(&self) -> iced::Theme { + iced::Theme::default() + } + + fn style(&self) -> theme::Application { + theme::Application::default() + } + + fn scale_factor(&self) -> f64 { + 1.0 + } + + fn run(settings: Settings<()>) -> Result<(), iced::Error> + where + Self: 'static + Sized, + { + ::run(settings) + } +} + +#[derive(Debug, Hash)] +enum Order { + Ascending, + Descending, +} + +impl std::fmt::Display for Order { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Ascending => "Ascending", + Self::Descending => "Descending", + } + ) + } +} diff --git a/examples/sctk_session_lock/Cargo.toml b/examples/sctk_session_lock/Cargo.toml new file mode 100644 index 0000000000..0ceb5bff98 --- /dev/null +++ b/examples/sctk_session_lock/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "sctk_session_lock" +version = "0.1.0" +edition = "2021" + +[dependencies] +sctk = { package = "smithay-client-toolkit", git = "https://github.com/smithay/client-toolkit", rev = "828b1eb" } +iced = { path = "../..", default-features = false, features = ["async-std", "wayland", "debug", "a11y"] } +iced_runtime = { path = "../../runtime" } +env_logger = "0.10" +async-std = "1.0" diff --git a/examples/sctk_session_lock/src/main.rs b/examples/sctk_session_lock/src/main.rs new file mode 100644 index 0000000000..1816af91ff --- /dev/null +++ b/examples/sctk_session_lock/src/main.rs @@ -0,0 +1,102 @@ +use iced::event::listen_raw; +use iced::wayland::session_lock; +use iced::{ + event::wayland::{Event as WaylandEvent, OutputEvent, SessionLockEvent}, + wayland::InitialSurface, + widget::text, + window, Application, Command, Element, Subscription, Theme, +}; +use iced_runtime::window::Id as SurfaceId; + +fn main() { + let mut settings = iced::Settings::default(); + settings.initial_surface = InitialSurface::None; + Locker::run(settings).unwrap(); +} + +#[derive(Debug, Clone, Default)] +struct Locker { + exit: bool, +} + +#[derive(Debug, Clone)] +pub enum Message { + WaylandEvent(WaylandEvent), + TimeUp, + Ignore, +} + +impl Application for Locker { + type Executor = iced::executor::Default; + type Message = Message; + type Flags = (); + type Theme = Theme; + + fn new(_flags: ()) -> (Locker, Command) { + ( + Locker { + ..Locker::default() + }, + session_lock::lock(), + ) + } + + fn title(&self, _id: window::Id) -> String { + String::from("Locker") + } + + fn update(&mut self, message: Self::Message) -> Command { + match message { + Message::WaylandEvent(evt) => match evt { + WaylandEvent::Output(evt, output) => match evt { + OutputEvent::Created(_) => { + return session_lock::get_lock_surface( + window::Id::unique(), + output, + ); + } + OutputEvent::Removed => {} + _ => {} + }, + WaylandEvent::SessionLock(evt) => match evt { + SessionLockEvent::Locked => { + return iced::Command::perform( + async_std::task::sleep( + std::time::Duration::from_secs(5), + ), + |_| Message::TimeUp, + ); + } + SessionLockEvent::Unlocked => { + // Server has processed unlock, so it's safe to exit + std::process::exit(0); + } + _ => {} + }, + _ => {} + }, + Message::TimeUp => { + return session_lock::unlock(); + } + Message::Ignore => {} + } + Command::none() + } + + fn view(&self, id: window::Id) -> Element { + text(format!("Lock Surface {:?}", id)).into() + } + + fn subscription(&self) -> Subscription { + listen_raw(|evt, _| { + if let iced::Event::PlatformSpecific( + iced::event::PlatformSpecific::Wayland(evt), + ) = evt + { + Some(Message::WaylandEvent(evt)) + } else { + None + } + }) + } +} diff --git a/examples/sctk_todos/Cargo.toml b/examples/sctk_todos/Cargo.toml new file mode 100644 index 0000000000..9e0e1f8e98 --- /dev/null +++ b/examples/sctk_todos/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "sctk_todos" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../..", default-features=false, features = ["async-std", "wayland", "debug"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +iced_core.workspace = true +once_cell = "1.15" +iced_style = { path = "../../style" } +sctk.workspace = true +log = "0.4.17" +env_logger = "0.10.0" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +async-std = "1.0" +directories-next = "2.0" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +web-sys = { version = "0.3", features = ["Window", "Storage"] } +wasm-timer = "0.2" + +[package.metadata.deb] +assets = [ + ["target/release-opt/todos", "usr/bin/iced-todos", "755"], + ["iced-todos.desktop", "usr/share/applications/", "644"], +] +[profile.release-opt] +debug = true diff --git a/examples/sctk_todos/README.md b/examples/sctk_todos/README.md new file mode 100644 index 0000000000..9c2598b95e --- /dev/null +++ b/examples/sctk_todos/README.md @@ -0,0 +1,20 @@ +## Todos + +A todos tracker inspired by [TodoMVC]. It showcases dynamic layout, text input, checkboxes, scrollables, icons, and async actions! It automatically saves your tasks in the background, even if you did not finish typing them. + +All the example code is located in the __[`main`]__ file. + + + +You can run the native version with `cargo run`: +``` +cargo run --package todos +``` +We have not yet implemented a `LocalStorage` version of the auto-save feature. Therefore, it does not work on web _yet_! + +[`main`]: src/main.rs +[TodoMVC]: http://todomvc.com/ diff --git a/examples/sctk_todos/fonts/icons.ttf b/examples/sctk_todos/fonts/icons.ttf new file mode 100644 index 0000000000000000000000000000000000000000..4498299db26811ff8490001ea6f681529a4118f4 GIT binary patch literal 5596 zcmd^CO>7&-6@Ih3q$ui#vPDs`8QVjxer$0`$&xI|w&c*HBs!7Y$g=E^c3g8sQ9q6( zDiV`AMGpat2C14RX%9hBwCJS&3oU>ym|BH?TQ2u`RGN`X*GHNLR@~m?aR>o8~5~+=ev5wtmzPu zm_++xcG+J2m73zgx)Jvw;tO{uYtj4}1Rt6jj6eMYJc-Ze3U|T(3U?L~duC zrF_;F-zSXiei2IVvVDD3Xf>ar{R-N0#a_<+=6i?Wulr4m|J&94dg1KPCZ;g|S70Ar zUCXWh?Q|HvG)<%Z67kx-)J>;I8yTCJrurqjutNLEfSxb5-;Kr6;=E0svPHngRsoG5 zcSWneCSE5O=Kr$BtA3><#b4>D(4Zxk4($W3$+^*4ihWr7v93>TU!zO<6uki&`%t6(!CLa0IgB(OS@0VE->+IH z0Da{!ASxe1!#BtHqfbgV$Ms{__%27E^qbnZsfiB6_WJ}0kt9uMd2|waaOV8Ye%;j^ z7XB*XZs`#1eUFL$ojY@(m3W zKF+S~qP|zJ!O1;DT{J+KDHXdgeq8gokA(K^sTOW*X_9KG%3WKP@d^*QJ=1kHn%mGc z-sX%;*F<%-m}V)eQ&cUgC(@}4Q%{~vjwNF4EsgDbnf0y%;kG?}?P1a4ZrbAyoD@C% z1E0|ry&dfxrn}133cRwX}EaFp!@^!5Y}2|UC>ucy`Hbsn$X zfvr4E6 z2-bQ|yM%C^$I$=zXLKYU)f$~CuQWX>4*IX)=-^Z#u4lDvoMu1mqgHw;)_hQCt^6%Wu5IFhCakY0c73(0E=E{?%W2<4pR>PQe3t<>y3PKo9ks*xnV61&Nlk&TX z>DXSPkbI=M!B>r4LuEuD!_5O7RZYE3qR(tW{xtb}dj>>*N3$@G3BONt3~(w1e%*7U z_l&q&>oT_9GwNK1=+Y0~-s--spY>n4eZ_w=aKm5Kd!u(Kskv*7t=}7Xim37I?X9j4 z#CHxfUHRbYrluPKPJFH=xZj|c8m{_={zWzVZC3yJqf<_WC`m->HZqXw3{Hb{p^sC$ zi22*wc=AYhUj|#8dk-Yu6wWnB?~8dLW*hW0=Ql2m9z{)C2U@J*`p?&1`peFk$Ivc~ z&lUJs8EaHU!)2^PKT^g9@I)EAsD`G?*bV*FGWJrK=F7N-8tGenwvB4cbB%76v7iRw zD`So7#i26Rp^ucY0X$sBE((aVW$cDNTgG0xDAvokhT6ri68Y5^))wa%3i5E`i0tV; zdR)%DAoEZyuGmY`ey*^PUt5@G%&p|s>_TpqM_+$_zNb)_lXGkNWjR(JSFWz*ujR6Z=t7~edZMeV(v#<-1m$U! zUZ6EvM5q?1K#~qadjzrvuhOG*9B2j%31*44NGoL15;QhFhaL-#WgYDp?m4tppv{4? z1RSL-p3A%RQ((-a{}M)7+hx6fl#5`mA$b;^(Ixzf!n^xfNw8KNrtNqz3x7(!uha9G ztq0lyda;*lj#rY#oDuK%D-jR2UBft8u%k{?3ecWFY3|xJXJviJs>-=R>3QH~2u0Aux2x|X7WwfblOxjnaZWp5v5ylR4Sv*hC{BzWJd7ge4)qFk1$N`yDDeNJfHXqs^oAvWW-(q`tA$YOlu>Wru=OR|$SiR{}3&42Aj2G!+V(p>$^`qUx-orj4pudnBUjEi6Dv zRhxK%*9Bn4)2fbJQ)tzp6;VD6)8K?eA_7^st?CmQxsj2o9zlz!25WpeRWxQt(ygj4 zXI_t}J=XZS)cE<5G8lrs(b4a*Ou;_;14aj!e9*22LSgvpP!HHIUq$tnt# z0mPYQvsKhtK4KLOmiw%ti{*Z+=w=zTid8HhwTd2=2drW>%YzoFy71?4;^}S?mn)l0uOYH^%VG#Q-eoNnS;eH(S(&8#k%3>1G{99wf0~{^;ps7p@{1JEGjZA z3wj^6f&y(aDwBLN5yHneHj-u%l^}(hjhct!+ABnpAM+nW2?-$k@#j!fbt0VGh?-Ik zZD6eaJ7yUzjiC&T36@kDKFqOmsau-VW$>2PuJ2FBxxjf)Dls2sG{&_Zk(o7>p0H<8W3+@F1kR*!Fz@eU!zEN*bIcwLnwVh>>w<7*!FUgt1debeG;q2R zdlwQ3b^AU~FrtmlZH^Oo;x)o0?9N=sk^zo^#O$v2atzENgl5oDD-TYulw)R+C*$2Z z?u3jNP>v`~r=oHQFFy9Tti)hv5QNUah5#+MQe(v%E9#F``bCJxElxCd2RE z`fs%=!>)9_hjYqO$HEoMJ%c`Gss8W= za)^^<1IKaK#MqXo3S<756E04`N_087Oq_}+4oS(!(L||Q=tJ~l zsI|i1sCvLjTB;A?3`cDgag}3uXI0|#xW(zH&LFH$Serzr0mcCYg9&R>IGVEnj^+!@ ziNo|Ha~MoAhrv1KFqmS_DS-3LVKB`c1{ava;39Kk08cT8L5evH(#&CSi8%>?%gkZ$ zG;R;?y#JKq-DUsc98 J@S+$Y`Y%9~)?EMq literal 0 HcmV?d00001 diff --git a/examples/sctk_todos/iced-todos.desktop b/examples/sctk_todos/iced-todos.desktop new file mode 100644 index 0000000000..dd7ce53dad --- /dev/null +++ b/examples/sctk_todos/iced-todos.desktop @@ -0,0 +1,4 @@ +[Desktop Entry] +Name=Todos - Iced +Exec=iced-todos +Type=Application diff --git a/examples/sctk_todos/index.html b/examples/sctk_todos/index.html new file mode 100644 index 0000000000..ee5570fb9e --- /dev/null +++ b/examples/sctk_todos/index.html @@ -0,0 +1,12 @@ + + + + + + Todos - Iced + + + + + + diff --git a/examples/sctk_todos/src/main.rs b/examples/sctk_todos/src/main.rs new file mode 100644 index 0000000000..356e383806 --- /dev/null +++ b/examples/sctk_todos/src/main.rs @@ -0,0 +1,660 @@ +use env_logger::Env; +use iced::alignment::{self, Alignment}; +use iced::event::{self, listen_raw, Event}; +use iced::subscription; +use iced::theme::{self, Theme}; +use iced::wayland::actions::data_device::ActionInner; +use iced::wayland::actions::window::SctkWindowSettings; +use iced::wayland::data_device::action as data_device_action; +use iced::wayland::InitialSurface; +use iced::widget::{ + self, button, checkbox, column, container, row, scrollable, text, + text_input, Text, +}; +use iced::{window, Application, Element}; +use iced::{Color, Command, Font, Length, Settings, Subscription}; +use iced_core::id::Id; +use iced_core::keyboard::key::Named; +use iced_core::layout::Limits; +use iced_core::{id, keyboard}; + +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; +use std::sync::Arc; + +static INPUT_ID: Lazy = Lazy::new(Id::unique); + +pub fn main() -> iced::Result { + let env = Env::default() + .filter_or("MY_LOG_LEVEL", "info") + .write_style_or("MY_LOG_STYLE", "always"); + + let mut settings = SctkWindowSettings::default(); + settings.size_limits = Limits::NONE.min_height(300.0).min_width(600.0); + env_logger::init_from_env(env); + Todos::run(Settings { + initial_surface: InitialSurface::XdgWindow(settings), + ..Settings::default() + }) +} + +#[derive(Debug)] +enum Todos { + Loading, + Loaded(State), +} + +#[derive(Debug, Default)] +struct State { + window_id_ctr: u128, + input_value: String, + filter: Filter, + tasks: Vec, + dirty: bool, + saving: bool, +} + +#[derive(Clone)] +enum Message { + Loaded(Result), + Saved(Result<(), SaveError>), + InputChanged(String), + CreateTask, + FilterChanged(Filter), + TaskMessage(usize, TaskMessage), + TabPressed { shift: bool }, + CloseRequested(window::Id), + Ignore, +} + +impl Debug for Message { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Message::Loaded(_) => write!(f, "Message::Loaded(_)"), + Message::Saved(_) => write!(f, "Message::Saved(_)"), + Message::InputChanged(_) => write!(f, "Message::InputChanged(_)"), + Message::CreateTask => write!(f, "Message::CreateTask"), + Message::FilterChanged(_) => write!(f, "Message::FilterChanged(_)"), + Message::TaskMessage(_, _) => { + write!(f, "Message::TaskMessage(_, _)") + } + Message::TabPressed { shift: _ } => { + write!(f, "Message::TabPressed {{ shift: _ }}") + } + Message::CloseRequested(_) => { + write!(f, "Message::CloseRequested(_)") + } + + Message::Ignore => write!(f, "Message::Ignore"), + } + } +} + +impl Application for Todos { + type Message = Message; + type Theme = Theme; + type Executor = iced::executor::Default; + type Flags = (); + + fn new(_flags: ()) -> (Todos, Command) { + ( + Todos::Loading, + Command::batch(vec![Command::perform( + SavedState::load(), + Message::Loaded, + )]), + ) + } + + fn title(&self, _id: window::Id) -> String { + let dirty = match self { + Todos::Loading => false, + Todos::Loaded(state) => state.dirty, + }; + + format!("Todos{} - Iced", if dirty { "*" } else { "" }) + } + + fn update(&mut self, message: Message) -> Command { + match self { + Todos::Loading => { + match message { + Message::Loaded(Ok(state)) => { + *self = Todos::Loaded(State { + input_value: state.input_value, + filter: state.filter, + tasks: state.tasks, + window_id_ctr: 1, + ..State::default() + }); + } + Message::Loaded(Err(_)) => { + *self = Todos::Loaded(State::default()); + } + _ => {} + } + + text_input::focus(INPUT_ID.clone()) + } + Todos::Loaded(state) => { + let mut saved = false; + + let command = match message { + Message::InputChanged(value) => { + state.input_value = value; + + Command::none() + } + Message::CreateTask => { + if !state.input_value.is_empty() { + state + .tasks + .push(Task::new(state.input_value.clone())); + state.input_value.clear(); + } + Command::none() + } + Message::FilterChanged(filter) => { + state.filter = filter; + + Command::none() + } + Message::TaskMessage(i, TaskMessage::Delete) => { + state.tasks.remove(i); + + Command::none() + } + Message::TaskMessage(i, task_message) => { + if let Some(task) = state.tasks.get_mut(i) { + let should_focus = + matches!(task_message, TaskMessage::Edit); + + task.update(task_message); + + if should_focus { + let id = Task::text_input_id(i); + Command::batch(vec![ + text_input::focus(id.clone()), + text_input::select_all(id), + ]) + } else { + Command::none() + } + } else { + Command::none() + } + } + Message::Saved(_) => { + state.saving = false; + saved = true; + + Command::none() + } + Message::TabPressed { shift } => { + if shift { + widget::focus_previous() + } else { + widget::focus_next() + } + } + Message::CloseRequested(_) => { + std::process::exit(0); + } + _ => Command::none(), + }; + + if !saved { + state.dirty = true; + } + + let save = if state.dirty && !state.saving { + state.dirty = false; + state.saving = true; + + Command::perform( + SavedState { + input_value: state.input_value.clone(), + filter: state.filter, + tasks: state.tasks.clone(), + } + .save(), + Message::Saved, + ) + } else { + Command::none() + }; + + Command::batch(vec![command, save]) + } + } + } + + fn view(&self, id: window::Id) -> Element { + match self { + Todos::Loading => loading_message(), + Todos::Loaded(State { + input_value, + filter, + tasks, + window_id_ctr, + .. + }) => { + if iced::window::Id::MAIN != id { + panic!("Wrong window id: {:?}", id) + } + + let title = text("todos") + .width(Length::Fill) + .size(100) + .style(Color::from([0.5, 0.5, 0.5])) + .horizontal_alignment(alignment::Horizontal::Center); + + let input = text_input("What needs to be done?", input_value) + .id(INPUT_ID.clone()) + .padding(15) + .size(30) + .on_submit(Message::CreateTask) + .on_input(Message::InputChanged) + .on_paste(Message::InputChanged); + + let controls = view_controls(tasks, *filter); + let filtered_tasks = + tasks.iter().filter(|task| filter.matches(task)); + + let tasks: Element<_> = if filtered_tasks.count() > 0 { + column( + tasks + .iter() + .enumerate() + .filter(|(_, task)| filter.matches(task)) + .map(|(i, task)| { + task.view(i).map(move |message| { + Message::TaskMessage(i, message) + }) + }) + .collect::>(), + ) + .spacing(10) + .into() + } else { + empty_message(match filter { + Filter::All => "You have not created a task yet...", + Filter::Active => "All your tasks are done! :D", + Filter::Completed => { + "You have not completed a task yet..." + } + }) + }; + + let content = column![title, input, controls, tasks] + .spacing(20) + .max_width(800); + + scrollable( + container(content) + .width(Length::Fill) + .padding(40) + .center_x(), + ) + .into() + } + } + } + + fn subscription(&self) -> Subscription { + listen_raw(|event, status| match (event, status) { + ( + Event::Keyboard(keyboard::Event::KeyPressed { + key: keyboard::Key::Named(Named::Tab), + modifiers, + .. + }), + event::Status::Ignored, + ) => Some(Message::TabPressed { + shift: modifiers.shift(), + }), + ( + Event::PlatformSpecific(event::PlatformSpecific::Wayland( + event::wayland::Event::Window(e, s, id), + )), + _, + ) => { + dbg!(&e); + None + } + _ => None, + }) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct Task { + description: String, + completed: bool, + + #[serde(skip)] + state: TaskState, +} + +#[derive(Debug, Clone)] +pub enum TaskState { + Idle, + Editing, +} + +impl Default for TaskState { + fn default() -> Self { + Self::Idle + } +} + +#[derive(Debug, Clone)] +pub enum TaskMessage { + Completed(bool), + Edit, + DescriptionEdited(String), + FinishEdition, + Delete, +} + +impl Task { + fn text_input_id(i: usize) -> id::Id { + id::Id::new(format!("task-{}", i)) + } + + fn new(description: String) -> Self { + Task { + description, + completed: false, + state: TaskState::Idle, + } + } + + fn update(&mut self, message: TaskMessage) { + match message { + TaskMessage::Completed(completed) => { + self.completed = completed; + } + TaskMessage::Edit => { + self.state = TaskState::Editing; + } + TaskMessage::DescriptionEdited(new_description) => { + self.description = new_description; + } + TaskMessage::FinishEdition => { + if !self.description.is_empty() { + self.state = TaskState::Idle; + } + } + TaskMessage::Delete => {} + } + } + + fn view(&self, i: usize) -> Element { + match &self.state { + TaskState::Idle => { + let checkbox = checkbox( + &self.description, + self.completed, + TaskMessage::Completed, + ) + .width(Length::Fill); + + row![ + checkbox, + button(edit_icon()) + .on_press(TaskMessage::Edit) + .padding(10) + .style(theme::Button::Text), + ] + .spacing(20) + .align_items(Alignment::Center) + .into() + } + TaskState::Editing => { + let text_input = + text_input("Describe your task...", &self.description) + .id(Self::text_input_id(i)) + .on_submit(TaskMessage::FinishEdition) + .on_input(TaskMessage::DescriptionEdited) + .on_paste(TaskMessage::DescriptionEdited) + .padding(10); + + row![ + text_input, + button(row![delete_icon(), "Delete"].spacing(10)) + .on_press(TaskMessage::Delete) + .padding(10) + .style(theme::Button::Destructive) + ] + .spacing(20) + .align_items(Alignment::Center) + .into() + } + } + } +} + +fn view_controls(tasks: &[Task], current_filter: Filter) -> Element { + let tasks_left = tasks.iter().filter(|task| !task.completed).count(); + + let filter_button = |label, filter, current_filter| { + let label = text(label).size(16); + + let button = button(label).style(if filter == current_filter { + theme::Button::Primary + } else { + theme::Button::Text + }); + + button.on_press(Message::FilterChanged(filter)).padding(8) + }; + + row![ + text(format!( + "{} {} left", + tasks_left, + if tasks_left == 1 { "task" } else { "tasks" } + )) + .width(Length::Fill) + .size(16), + row![ + filter_button("All", Filter::All, current_filter), + filter_button("Active", Filter::Active, current_filter), + filter_button("Completed", Filter::Completed, current_filter,), + ] + .width(Length::Shrink) + .spacing(10) + ] + .spacing(20) + .align_items(Alignment::Center) + .into() +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum Filter { + All, + Active, + Completed, +} + +impl Default for Filter { + fn default() -> Self { + Filter::All + } +} + +impl Filter { + fn matches(&self, task: &Task) -> bool { + match self { + Filter::All => true, + Filter::Active => !task.completed, + Filter::Completed => task.completed, + } + } +} + +fn loading_message<'a>() -> Element<'a, Message> { + container( + text("Loading...") + .horizontal_alignment(alignment::Horizontal::Center) + .size(50), + ) + .width(Length::Fill) + .height(Length::Fill) + .center_y() + .into() +} + +fn empty_message(message: &str) -> Element<'_, Message> { + container( + text(message) + .width(Length::Fill) + .size(25) + .horizontal_alignment(alignment::Horizontal::Center) + .style(Color::from([0.7, 0.7, 0.7])), + ) + .width(Length::Fill) + .height(Length::Fixed(200.0)) + .center_y() + .into() +} + +// Fonts +const ICONS: Font = Font::with_name("Iced-Todos-Icons"); + +fn icon(unicode: char) -> Text<'static> { + text(unicode.to_string()) + .font(ICONS) + .width(Length::Fixed(20.0)) + .horizontal_alignment(alignment::Horizontal::Center) + .size(20) +} + +fn edit_icon() -> Text<'static> { + icon('\u{F303}') +} + +fn delete_icon() -> Text<'static> { + icon('\u{F1F8}') +} + +// Persistence +#[derive(Debug, Clone, Serialize, Deserialize)] +struct SavedState { + input_value: String, + filter: Filter, + tasks: Vec, +} + +#[derive(Debug, Clone)] +enum LoadError { + File, + Format, +} + +#[derive(Debug, Clone)] +enum SaveError { + File, + Write, + Format, +} + +#[cfg(not(target_arch = "wasm32"))] +impl SavedState { + fn path() -> std::path::PathBuf { + let mut path = if let Some(project_dirs) = + directories_next::ProjectDirs::from("rs", "Iced", "Todos") + { + project_dirs.data_dir().into() + } else { + std::env::current_dir().unwrap_or_default() + }; + + path.push("todos.json"); + + path + } + + async fn load() -> Result { + use async_std::prelude::*; + + let mut contents = String::new(); + + let mut file = async_std::fs::File::open(Self::path()) + .await + .map_err(|_| LoadError::File)?; + + file.read_to_string(&mut contents) + .await + .map_err(|_| LoadError::File)?; + + serde_json::from_str(&contents).map_err(|_| LoadError::Format) + } + + async fn save(self) -> Result<(), SaveError> { + use async_std::prelude::*; + + let json = serde_json::to_string_pretty(&self) + .map_err(|_| SaveError::Format)?; + + let path = Self::path(); + + if let Some(dir) = path.parent() { + async_std::fs::create_dir_all(dir) + .await + .map_err(|_| SaveError::File)?; + } + + { + let mut file = async_std::fs::File::create(path) + .await + .map_err(|_| SaveError::File)?; + + file.write_all(json.as_bytes()) + .await + .map_err(|_| SaveError::Write)?; + } + + // This is a simple way to save at most once every couple seconds + async_std::task::sleep(std::time::Duration::from_secs(2)).await; + + Ok(()) + } +} + +#[cfg(target_arch = "wasm32")] +impl SavedState { + fn storage() -> Option { + let window = web_sys::window()?; + + window.local_storage().ok()? + } + + async fn load() -> Result { + let storage = Self::storage().ok_or(LoadError::File)?; + + let contents = storage + .get_item("state") + .map_err(|_| LoadError::File)? + .ok_or(LoadError::File)?; + + serde_json::from_str(&contents).map_err(|_| LoadError::Format) + } + + async fn save(self) -> Result<(), SaveError> { + let storage = Self::storage().ok_or(SaveError::File)?; + + let json = serde_json::to_string_pretty(&self) + .map_err(|_| SaveError::Format)?; + + storage + .set_item("state", &json) + .map_err(|_| SaveError::Write)?; + + let _ = wasm_timer::Delay::new(std::time::Duration::from_secs(2)).await; + + Ok(()) + } +} diff --git a/examples/system_information/Cargo.toml b/examples/system_information/Cargo.toml deleted file mode 100644 index 419031227e..0000000000 --- a/examples/system_information/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "system_information" -version = "0.1.0" -authors = ["Richard "] -edition = "2021" -publish = false - -[dependencies] -iced.workspace = true -iced.features = ["system"] - -bytesize = "1.1" diff --git a/examples/system_information/src/main.rs b/examples/system_information/src/main.rs deleted file mode 100644 index 507431eed1..0000000000 --- a/examples/system_information/src/main.rs +++ /dev/null @@ -1,159 +0,0 @@ -use iced::widget::{button, column, container, text}; -use iced::{ - executor, system, Application, Command, Element, Length, Settings, Theme, -}; - -use bytesize::ByteSize; - -pub fn main() -> iced::Result { - Example::run(Settings::default()) -} - -#[allow(clippy::large_enum_variant)] -enum Example { - Loading, - Loaded { information: system::Information }, -} - -#[derive(Clone, Debug)] -#[allow(clippy::large_enum_variant)] -enum Message { - InformationReceived(system::Information), - Refresh, -} - -impl Application for Example { - type Message = Message; - type Theme = Theme; - type Executor = executor::Default; - type Flags = (); - - fn new(_flags: ()) -> (Self, Command) { - ( - Self::Loading, - system::fetch_information(Message::InformationReceived), - ) - } - - fn title(&self) -> String { - String::from("System Information - Iced") - } - - fn update(&mut self, message: Message) -> Command { - match message { - Message::Refresh => { - *self = Self::Loading; - - return system::fetch_information(Message::InformationReceived); - } - Message::InformationReceived(information) => { - *self = Self::Loaded { information }; - } - } - - Command::none() - } - - fn view(&self) -> Element { - let content: Element<_> = match self { - Example::Loading => text("Loading...").size(40).into(), - Example::Loaded { information } => { - let system_name = text(format!( - "System name: {}", - information - .system_name - .as_ref() - .unwrap_or(&"unknown".to_string()) - )); - - let system_kernel = text(format!( - "System kernel: {}", - information - .system_kernel - .as_ref() - .unwrap_or(&"unknown".to_string()) - )); - - let system_version = text(format!( - "System version: {}", - information - .system_version - .as_ref() - .unwrap_or(&"unknown".to_string()) - )); - - let system_short_version = text(format!( - "System short version: {}", - information - .system_short_version - .as_ref() - .unwrap_or(&"unknown".to_string()) - )); - - let cpu_brand = - text(format!("Processor brand: {}", information.cpu_brand)); - - let cpu_cores = text(format!( - "Processor cores: {}", - information - .cpu_cores - .map_or("unknown".to_string(), |cores| cores - .to_string()) - )); - - let memory_readable = - ByteSize::kb(information.memory_total).to_string(); - - let memory_total = text(format!( - "Memory (total): {} kb ({memory_readable})", - information.memory_total, - )); - - let memory_text = if let Some(memory_used) = - information.memory_used - { - let memory_readable = ByteSize::kb(memory_used).to_string(); - - format!("{memory_used} kb ({memory_readable})") - } else { - String::from("None") - }; - - let memory_used = text(format!("Memory (used): {memory_text}")); - - let graphics_adapter = text(format!( - "Graphics adapter: {}", - information.graphics_adapter - )); - - let graphics_backend = text(format!( - "Graphics backend: {}", - information.graphics_backend - )); - - column![ - system_name.size(30), - system_kernel.size(30), - system_version.size(30), - system_short_version.size(30), - cpu_brand.size(30), - cpu_cores.size(30), - memory_total.size(30), - memory_used.size(30), - graphics_adapter.size(30), - graphics_backend.size(30), - button("Refresh").on_press(Message::Refresh) - ] - .spacing(10) - .into() - } - }; - - container(content) - .center_x() - .center_y() - .width(Length::Fill) - .height(Length::Fill) - .into() - } -} diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index cc9875d94d..e9de65a8ea 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -180,7 +180,9 @@ mod toast { use iced::advanced::layout::{self, Layout}; use iced::advanced::overlay; use iced::advanced::renderer; - use iced::advanced::widget::{self, Operation, Tree}; + use iced::advanced::widget::{ + self, Operation, OperationOutputWrapper, Tree, + }; use iced::advanced::{Clipboard, Shell, Widget}; use iced::event::{self, Event}; use iced::mouse; @@ -346,7 +348,7 @@ mod toast { .collect() } - fn diff(&self, tree: &mut Tree) { + fn diff(&mut self, tree: &mut Tree) { let instants = tree.state.downcast_mut::>>(); // Invalidating removed instants to None allows us to remove @@ -367,8 +369,8 @@ mod toast { } tree.diff_children( - &std::iter::once(&self.content) - .chain(self.toasts.iter()) + &mut std::iter::once(&mut self.content) + .chain(self.toasts.iter_mut()) .collect::>(), ); } @@ -378,7 +380,7 @@ mod toast { state: &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( @@ -623,7 +625,7 @@ mod toast { &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.toasts diff --git a/examples/todos/Cargo.toml b/examples/todos/Cargo.toml index 0d5f8c38a7..5926a39a7f 100644 --- a/examples/todos/Cargo.toml +++ b/examples/todos/Cargo.toml @@ -8,7 +8,7 @@ publish = false [dependencies] iced.workspace = true iced_core.workspace = true -iced.features = ["async-std", "debug", "wgpu"] +iced.features = ["async-std", "debug", "winit"] 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 9d7f6af2d2..2289fb7385 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -251,8 +251,9 @@ impl Application for Todos { } }) }; + let test = row![container(text("0000 0000 00000 000000000000 000000000000000 00000 0000 00000000 000000 000000000 l00000")).width(Length::Fill), container(text("a")).width(Length::Fixed(100.0))]; - let content = column![title, input, controls, tasks] + let content = column![title, input, controls, tasks, test] .spacing(20) .max_width(800); diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index 38a6db1e14..e837e8dd12 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -13,6 +13,7 @@ use once_cell::sync::Lazy; pub fn main() -> iced::Result { WebSocket::run(Settings::default()) } +use iced::id::Id; #[derive(Default)] struct WebSocket { @@ -161,4 +162,4 @@ impl Default for State { } } -static MESSAGE_LOG: Lazy = Lazy::new(scrollable::Id::unique); +static MESSAGE_LOG: Lazy = Lazy::new(|| Id::new("message_log")); diff --git a/futures/Cargo.toml b/futures/Cargo.toml index 69a915e43b..d928228dd6 100644 --- a/futures/Cargo.toml +++ b/futures/Cargo.toml @@ -16,6 +16,7 @@ all-features = true [features] thread-pool = ["futures/thread-pool"] +a11y = ["iced_core/a11y"] [dependencies] iced_core.workspace = true diff --git a/renderer/Cargo.toml b/renderer/Cargo.toml index a159978c27..dfbea8061a 100644 --- a/renderer/Cargo.toml +++ b/renderer/Cargo.toml @@ -11,6 +11,7 @@ categories.workspace = true keywords.workspace = true [features] +default = [] wgpu = ["iced_wgpu"] image = ["iced_tiny_skia/image", "iced_wgpu?/image"] svg = ["iced_tiny_skia/svg", "iced_wgpu?/svg"] diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 895c0efc66..3a06eaa594 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -14,13 +14,14 @@ keywords.workspace = true debug = [] multi-window = [] a11y = ["iced_accessibility", "iced_core/a11y"] - +wayland = ["iced_accessibility?/accesskit_unix", "iced_core/wayland", "sctk"] [dependencies] iced_core.workspace = true iced_futures.workspace = true iced_futures.features = ["thread-pool"] - +sctk.workspace = true +sctk.optional = true thiserror.workspace = true iced_accessibility.workspace = true iced_accessibility.optional = true diff --git a/runtime/src/command.rs b/runtime/src/command.rs index 176c2b1e57..4a9e0551e0 100644 --- a/runtime/src/command.rs +++ b/runtime/src/command.rs @@ -1,5 +1,6 @@ //! Run asynchronous actions. mod action; +/// A set of asynchronous actions to be performed by some platform specific runtime. pub mod platform_specific; pub use action::Action; diff --git a/runtime/src/command/platform_specific/mod.rs b/runtime/src/command/platform_specific/mod.rs index c259f40b07..1bb74473d1 100644 --- a/runtime/src/command/platform_specific/mod.rs +++ b/runtime/src/command/platform_specific/mod.rs @@ -4,10 +4,17 @@ use std::{fmt, marker::PhantomData}; use iced_futures::MaybeSend; +#[cfg(feature = "wayland")] +/// Platform specific actions defined for wayland +pub mod wayland; + /// Platform specific actions defined for wayland pub enum Action { /// phantom data variant in case the platform has not specific actions implemented Phantom(PhantomData), + /// Wayland Specific Actions + #[cfg(feature = "wayland")] + Wayland(wayland::Action), } impl Action { @@ -22,6 +29,8 @@ impl Action { { match self { Action::Phantom(_) => unimplemented!(), + #[cfg(feature = "wayland")] + Action::Wayland(action) => Action::Wayland(action.map(_f)), } } } @@ -30,6 +39,8 @@ impl fmt::Debug for Action { fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Action::Phantom(_) => unimplemented!(), + #[cfg(feature = "wayland")] + Action::Wayland(action) => action.fmt(_f), } } } diff --git a/runtime/src/command/platform_specific/wayland/activation.rs b/runtime/src/command/platform_specific/wayland/activation.rs new file mode 100644 index 0000000000..50f2c44b75 --- /dev/null +++ b/runtime/src/command/platform_specific/wayland/activation.rs @@ -0,0 +1,67 @@ +use iced_core::window::Id; +use iced_futures::MaybeSend; + +use std::fmt; + +/// xdg-activation Actions +pub enum Action { + /// request an activation token + RequestToken { + /// application id + app_id: Option, + /// window, if provided + window: Option, + /// message generation + message: Box) -> T + Send + Sync + 'static>, + }, + /// request a window to be activated + Activate { + /// window to activate + window: Id, + /// activation token + token: String, + }, +} + +impl Action { + /// Maps the output of a window [`Action`] using the provided closure. + pub fn map( + self, + mapper: impl Fn(T) -> A + 'static + MaybeSend + Sync, + ) -> Action + where + T: 'static, + { + match self { + Action::RequestToken { + app_id, + window, + message, + } => Action::RequestToken { + app_id, + window, + message: Box::new(move |token| mapper(message(token))), + }, + Action::Activate { window, token } => { + Action::Activate { window, token } + } + } + } +} + +impl fmt::Debug for Action { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Action::RequestToken { app_id, window, .. } => write!( + f, + "Action::ActivationAction::RequestToken {{ app_id: {:?}, window: {:?} }}", + app_id, window, + ), + Action::Activate { window, token } => write!( + f, + "Action::ActivationAction::Activate {{ window: {:?}, token: {:?} }}", + window, token, + ) + } + } +} diff --git a/runtime/src/command/platform_specific/wayland/data_device.rs b/runtime/src/command/platform_specific/wayland/data_device.rs new file mode 100644 index 0000000000..5a85eefcaf --- /dev/null +++ b/runtime/src/command/platform_specific/wayland/data_device.rs @@ -0,0 +1,137 @@ +use iced_core::window::Id; +use iced_futures::MaybeSend; +use sctk::reexports::client::protocol::wl_data_device_manager::DndAction; +use std::{any::Any, fmt, marker::PhantomData}; + +/// DataDevice Action +pub struct Action { + /// The inner action + pub inner: ActionInner, + /// The phantom data + _phantom: PhantomData, +} + +impl From for Action { + fn from(inner: ActionInner) -> Self { + Self { + inner, + _phantom: PhantomData, + } + } +} + +impl fmt::Debug for Action { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.inner.fmt(f) + } +} + +/// A trait for converting to data given a mime type. +pub trait DataFromMimeType { + /// Convert to data given a mime type. + fn from_mime_type(&self, mime_type: &str) -> Option>; +} + +/// DataDevice Action +pub enum ActionInner { + /// Start a drag and drop operation. When a client asks for the selection, an event will be delivered + /// This is used for internal drags, where the client is the source of the drag. + /// The client will be resposible for data transfer. + StartInternalDnd { + /// The window id of the window that is the source of the drag. + origin_id: Id, + /// An optional window id for the cursor icon surface. + icon_id: Option, + }, + /// Start a drag and drop operation. When a client asks for the selection, an event will be delivered + StartDnd { + /// The mime types that the dnd data can be converted to. + mime_types: Vec, + /// The actions that the client supports. + actions: DndAction, + /// The window id of the window that is the source of the drag. + origin_id: Id, + /// An optional window id for the cursor icon surface. + icon_id: Option, + /// The data to send. + data: Box, + }, + /// Set the accepted drag and drop mime type. + Accept(Option), + /// Set accepted and preferred drag and drop actions. + SetActions { + /// The preferred action. + preferred: DndAction, + /// The accepted actions. + accepted: DndAction, + }, + /// Read the Drag and Drop data with a mime type. An event will be delivered with a pipe to read the data from. + RequestDndData(String), + /// The drag and drop operation has finished. + DndFinished, + /// The drag and drop operation has been cancelled. + DndCancelled, +} + +/// DndIcon +#[derive(Debug)] +pub enum DndIcon { + /// The id of the widget which will draw the dnd icon. + Widget(Id, Box), + /// A custom icon. + Custom(Id), +} + +impl Action { + /// Maps the output of a window [`Action`] using the provided closure. + pub fn map( + self, + _: impl Fn(T) -> A + 'static + MaybeSend + Sync, + ) -> Action + where + T: 'static, + { + Action::from(self.inner) + } +} + +impl fmt::Debug for ActionInner { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Accept(mime_type) => { + f.debug_tuple("Accept").field(mime_type).finish() + } + Self::StartInternalDnd { origin_id, icon_id } => f + .debug_tuple("StartInternalDnd") + .field(origin_id) + .field(icon_id) + .finish(), + Self::StartDnd { + mime_types, + actions, + origin_id, + icon_id, + .. + } => f + .debug_tuple("StartDnd") + .field(mime_types) + .field(actions) + .field(origin_id) + .field(icon_id) + .finish(), + Self::SetActions { + preferred, + accepted, + } => f + .debug_tuple("SetActions") + .field(preferred) + .field(accepted) + .finish(), + Self::RequestDndData(mime_type) => { + f.debug_tuple("RequestDndData").field(mime_type).finish() + } + Self::DndFinished => f.debug_tuple("DndFinished").finish(), + Self::DndCancelled => f.debug_tuple("DndCancelled").finish(), + } + } +} diff --git a/runtime/src/command/platform_specific/wayland/layer_surface.rs b/runtime/src/command/platform_specific/wayland/layer_surface.rs new file mode 100644 index 0000000000..56b0df7ebe --- /dev/null +++ b/runtime/src/command/platform_specific/wayland/layer_surface.rs @@ -0,0 +1,224 @@ +use std::fmt; +use std::marker::PhantomData; + +use iced_core::layout::Limits; +use iced_futures::MaybeSend; +use sctk::{ + reexports::client::protocol::wl_output::WlOutput, + shell::wlr_layer::{Anchor, KeyboardInteractivity, Layer}, +}; + +use iced_core::window::Id; + +/// output for layer surface +#[derive(Debug, Clone)] +pub enum IcedOutput { + /// show on all outputs + All, + /// show on active output + Active, + /// show on a specific output + Output(WlOutput), +} + +impl Default for IcedOutput { + fn default() -> Self { + Self::Active + } +} + +/// margins of the layer surface +#[derive(Debug, Clone, Copy, Default)] +pub struct IcedMargin { + /// top + pub top: i32, + /// right + pub right: i32, + /// bottom + pub bottom: i32, + /// left + pub left: i32, +} + +/// layer surface +#[derive(Debug, Clone)] +pub struct SctkLayerSurfaceSettings { + /// XXX id must be unique for every surface, window, and popup + pub id: Id, + /// layer + pub layer: Layer, + /// keyboard interactivity + pub keyboard_interactivity: KeyboardInteractivity, + /// pointer interactivity + pub pointer_interactivity: bool, + /// anchor, if a surface is anchored to two opposite edges, it will be stretched to fit between those edges, regardless of the specified size in that dimension. + pub anchor: Anchor, + /// output + pub output: IcedOutput, + /// namespace + pub namespace: String, + /// margin + pub margin: IcedMargin, + /// XXX size, providing None will autosize the layer surface to its contents + /// If Some size is provided, None in a given dimension lets the compositor decide for that dimension, usually this would be done with a layer surface that is anchored to left & right or top & bottom + pub size: Option<(Option, Option)>, + /// exclusive zone + pub exclusive_zone: i32, + /// Limits of the popup size + pub size_limits: Limits, +} + +impl Default for SctkLayerSurfaceSettings { + fn default() -> Self { + Self { + id: Id::MAIN, + layer: Layer::Top, + keyboard_interactivity: Default::default(), + pointer_interactivity: true, + anchor: Anchor::empty(), + output: Default::default(), + namespace: Default::default(), + margin: Default::default(), + size: Default::default(), + exclusive_zone: Default::default(), + size_limits: Limits::NONE + .min_height(1.0) + .min_width(1.0) + .max_width(1920.0) + .max_height(1080.023), + } + } +} + +#[derive(Clone)] +/// LayerSurface Action +pub enum Action { + /// create a layer surface and receive a message with its Id + LayerSurface { + /// surface builder + builder: SctkLayerSurfaceSettings, + /// phantom + _phantom: PhantomData, + }, + /// Set size of the layer surface. + Size { + /// id of the layer surface + id: Id, + /// The new logical width of the window + width: Option, + /// The new logical height of the window + height: Option, + }, + /// Destroy the layer surface + Destroy(Id), + /// The edges which the layer surface is anchored to + Anchor { + /// id of the layer surface + id: Id, + /// anchor of the layer surface + anchor: Anchor, + }, + /// exclusive zone of the layer surface + ExclusiveZone { + /// id of the layer surface + id: Id, + /// exclusive zone of the layer surface + exclusive_zone: i32, + }, + /// margin of the layer surface, ignored for un-anchored edges + Margin { + /// id of the layer surface + id: Id, + /// margins of the layer surface + margin: IcedMargin, + }, + /// keyboard interactivity of the layer surface + KeyboardInteractivity { + /// id of the layer surface + id: Id, + /// keyboard interactivity of the layer surface + keyboard_interactivity: KeyboardInteractivity, + }, + /// layer of the layer surface + Layer { + /// id of the layer surface + id: Id, + /// layer of the layer surface + layer: Layer, + }, +} + +impl Action { + /// Maps the output of a window [`Action`] using the provided closure. + pub fn map( + self, + _: impl Fn(T) -> A + 'static + MaybeSend + Sync, + ) -> Action + where + T: 'static, + { + match self { + Action::LayerSurface { builder, .. } => Action::LayerSurface { + builder, + _phantom: PhantomData::default(), + }, + Action::Size { id, width, height } => { + Action::Size { id, width, height } + } + Action::Destroy(id) => Action::Destroy(id), + Action::Anchor { id, anchor } => Action::Anchor { id, anchor }, + Action::ExclusiveZone { id, exclusive_zone } => { + Action::ExclusiveZone { id, exclusive_zone } + } + Action::Margin { id, margin } => Action::Margin { id, margin }, + Action::KeyboardInteractivity { + id, + keyboard_interactivity, + } => Action::KeyboardInteractivity { + id, + keyboard_interactivity, + }, + Action::Layer { id, layer } => Action::Layer { id, layer }, + } + } +} + +impl fmt::Debug for Action { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Action::LayerSurface { builder, .. } => write!( + f, + "Action::LayerSurfaceAction::LayerSurface {{ builder: {:?} }}", + builder + ), + Action::Size { id, width, height } => write!( + f, + "Action::LayerSurfaceAction::Size {{ id: {:#?}, width: {:?}, height: {:?} }}", id, width, height + ), + Action::Destroy(id) => write!( + f, + "Action::LayerSurfaceAction::Destroy {{ id: {:#?} }}", id + ), + Action::Anchor { id, anchor } => write!( + f, + "Action::LayerSurfaceAction::Anchor {{ id: {:#?}, anchor: {:?} }}", id, anchor + ), + Action::ExclusiveZone { id, exclusive_zone } => write!( + f, + "Action::LayerSurfaceAction::ExclusiveZone {{ id: {:#?}, exclusive_zone: {exclusive_zone} }}", id + ), + Action::Margin { id, margin } => write!( + f, + "Action::LayerSurfaceAction::Margin {{ id: {:#?}, margin: {:?} }}", id, margin + ), + Action::KeyboardInteractivity { id, keyboard_interactivity } => write!( + f, + "Action::LayerSurfaceAction::Margin {{ id: {:#?}, keyboard_interactivity: {:?} }}", id, keyboard_interactivity + ), + Action::Layer { id, layer } => write!( + f, + "Action::LayerSurfaceAction::Margin {{ id: {:#?}, layer: {:?} }}", id, layer + ), + } + } +} diff --git a/runtime/src/command/platform_specific/wayland/mod.rs b/runtime/src/command/platform_specific/wayland/mod.rs new file mode 100644 index 0000000000..efde438d1a --- /dev/null +++ b/runtime/src/command/platform_specific/wayland/mod.rs @@ -0,0 +1,76 @@ +//! Wayland specific actions + +use std::fmt::Debug; + +use iced_futures::MaybeSend; + +/// activation Actions +pub mod activation; +/// data device Actions +pub mod data_device; +/// layer surface actions +pub mod layer_surface; +/// popup actions +pub mod popup; +/// session locks +pub mod session_lock; +/// window actions +pub mod window; + +/// Platform specific actions defined for wayland +pub enum Action { + /// LayerSurface Actions + LayerSurface(layer_surface::Action), + /// Window Actions + Window(window::Action), + /// popup + Popup(popup::Action), + /// data device + DataDevice(data_device::Action), + /// activation + Activation(activation::Action), + /// session lock + SessionLock(session_lock::Action), +} + +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::LayerSurface(a) => Action::LayerSurface(a.map(f)), + Action::Window(a) => Action::Window(a.map(f)), + Action::Popup(a) => Action::Popup(a.map(f)), + Action::DataDevice(a) => Action::DataDevice(a.map(f)), + Action::Activation(a) => Action::Activation(a.map(f)), + Action::SessionLock(a) => Action::SessionLock(a.map(f)), + } + } +} + +impl Debug for Action { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::LayerSurface(arg0) => { + f.debug_tuple("LayerSurface").field(arg0).finish() + } + Self::Window(arg0) => f.debug_tuple("Window").field(arg0).finish(), + Self::Popup(arg0) => f.debug_tuple("Popup").field(arg0).finish(), + Self::DataDevice(arg0) => { + f.debug_tuple("DataDevice").field(arg0).finish() + } + Self::Activation(arg0) => { + f.debug_tuple("Activation").field(arg0).finish() + } + Self::SessionLock(arg0) => { + f.debug_tuple("SessionLock").field(arg0).finish() + } + } + } +} diff --git a/runtime/src/command/platform_specific/wayland/popup.rs b/runtime/src/command/platform_specific/wayland/popup.rs new file mode 100644 index 0000000000..87e95a31eb --- /dev/null +++ b/runtime/src/command/platform_specific/wayland/popup.rs @@ -0,0 +1,178 @@ +use std::fmt; +use std::hash::{Hash, Hasher}; +use std::marker::PhantomData; + +use iced_core::layout::Limits; +use iced_core::window::Id; +use iced_core::Rectangle; +use iced_futures::MaybeSend; +use sctk::reexports::protocols::xdg::shell::client::xdg_positioner::{ + Anchor, Gravity, +}; +/// Popup creation details +#[derive(Debug, Clone)] +pub struct SctkPopupSettings { + /// XXX must be unique, id of the parent + pub parent: Id, + /// XXX must be unique, id of the popup + pub id: Id, + /// positioner of the popup + pub positioner: SctkPositioner, + /// optional parent size, must be correct if specified or the behavior is undefined + pub parent_size: Option<(u32, u32)>, + /// whether a grab should be requested for the popup after creation + pub grab: bool, +} + +impl Hash for SctkPopupSettings { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + +/// Positioner of a popup +#[derive(Debug, Clone)] +pub struct SctkPositioner { + /// size of the popup (if it is None, the popup will be autosized) + pub size: Option<(u32, u32)>, + /// Limits of the popup size + pub size_limits: Limits, + /// the rectangle which the popup will be anchored to + pub anchor_rect: Rectangle, + /// the anchor location on the popup + pub anchor: Anchor, + /// the gravity of the popup + pub gravity: Gravity, + /// the constraint adjustment, + /// Specify how the window should be positioned if the originally intended position caused the surface to be constrained, meaning at least partially outside positioning boundaries set by the compositor. The adjustment is set by constructing a bitmask describing the adjustment to be made when the surface is constrained on that axis. + /// If no bit for one axis is set, the compositor will assume that the child surface should not change its position on that axis when constrained. + /// + /// If more than one bit for one axis is set, the order of how adjustments are applied is specified in the corresponding adjustment descriptions. + /// + /// The default adjustment is none. + pub constraint_adjustment: u32, + /// offset of the popup + pub offset: (i32, i32), + /// whether the popup is reactive + pub reactive: bool, +} + +impl Hash for SctkPositioner { + fn hash(&self, state: &mut H) { + self.size.hash(state); + self.anchor_rect.x.hash(state); + self.anchor_rect.y.hash(state); + self.anchor_rect.width.hash(state); + self.anchor_rect.height.hash(state); + self.anchor.hash(state); + self.gravity.hash(state); + self.constraint_adjustment.hash(state); + self.offset.hash(state); + self.reactive.hash(state); + } +} + +impl Default for SctkPositioner { + fn default() -> Self { + Self { + size: None, + size_limits: Limits::NONE + .min_height(1.0) + .min_width(1.0) + .max_width(300.0) + .max_height(1080.0), + anchor_rect: Rectangle { + x: 0, + y: 0, + width: 1, + height: 1, + }, + anchor: Anchor::None, + gravity: Gravity::None, + constraint_adjustment: 15, + offset: Default::default(), + reactive: true, + } + } +} + +#[derive(Clone)] +/// Window Action +pub enum Action { + /// create a window and receive a message with its Id + Popup { + /// popup + popup: SctkPopupSettings, + /// phantom + _phantom: PhantomData, + }, + /// destroy the popup + Destroy { + /// id of the popup + id: Id, + }, + /// request that the popup make an explicit grab + Grab { + /// id of the popup + id: Id, + }, + /// set the size of the popup + Size { + /// id of the popup + id: Id, + /// width + width: u32, + /// height + height: u32, + }, +} + +impl Action { + /// Maps the output of a window [`Action`] using the provided closure. + pub fn map( + self, + _: impl Fn(T) -> A + 'static + MaybeSend + Sync, + ) -> Action + where + T: 'static, + { + match self { + Action::Popup { popup, .. } => Action::Popup { + popup, + _phantom: PhantomData::default(), + }, + Action::Destroy { id } => Action::Destroy { id }, + Action::Grab { id } => Action::Grab { id }, + Action::Size { id, width, height } => { + Action::Size { id, width, height } + } + } + } +} + +impl fmt::Debug for Action { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Action::Popup { popup, .. } => write!( + f, + "Action::PopupAction::Popup {{ popup: {:?} }}", + popup + ), + Action::Destroy { id } => write!( + f, + "Action::PopupAction::Destroy {{ id: {:?} }}", + id + ), + Action::Size { id, width, height } => write!( + f, + "Action::PopupAction::Size {{ id: {:?}, width: {:?}, height: {:?} }}", + id, width, height + ), + Action::Grab { id } => write!( + f, + "Action::PopupAction::Grab {{ id: {:?} }}", + id + ), + } + } +} diff --git a/runtime/src/command/platform_specific/wayland/session_lock.rs b/runtime/src/command/platform_specific/wayland/session_lock.rs new file mode 100644 index 0000000000..fbd0032278 --- /dev/null +++ b/runtime/src/command/platform_specific/wayland/session_lock.rs @@ -0,0 +1,80 @@ +use std::{fmt, marker::PhantomData}; + +use iced_core::window::Id; +use iced_futures::MaybeSend; + +use sctk::reexports::client::protocol::wl_output::WlOutput; + +/// Session lock action +#[derive(Clone)] +pub enum Action { + /// Request a session lock + Lock, + /// Destroy lock + Unlock, + /// Create lock surface for output + LockSurface { + /// unique id for surface + id: Id, + /// output + output: WlOutput, + /// phantom + _phantom: PhantomData, + }, + /// Destroy lock surface + DestroyLockSurface { + /// unique id for surface + id: Id, + }, +} + +impl Action { + /// Maps the output of a window [`Action`] using the provided closure. + pub fn map( + self, + _: impl Fn(T) -> A + 'static + MaybeSend + Sync, + ) -> Action + where + T: 'static, + { + match self { + Action::Lock => Action::Lock, + Action::Unlock => Action::Unlock, + Action::LockSurface { + id, + output, + _phantom, + } => Action::LockSurface { + id, + output, + _phantom: PhantomData, + }, + Action::DestroyLockSurface { id } => { + Action::DestroyLockSurface { id } + } + } + } +} + +impl fmt::Debug for Action { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Action::Lock => write!(f, "Action::SessionLock::Lock"), + Action::Unlock => write!(f, "Action::SessionLock::Unlock"), + Action::LockSurface { + id, + output, + _phantom, + } => write!( + f, + "Action::SessionLock::LockSurface {{ id: {:?}, output: {:?} }}", + id, output + ), + Action::DestroyLockSurface { id } => write!( + f, + "Action::SessionLock::DestroyLockSurface {{ id: {:?} }}", + id + ), + } + } +} diff --git a/runtime/src/command/platform_specific/wayland/window.rs b/runtime/src/command/platform_specific/wayland/window.rs new file mode 100644 index 0000000000..5f7764093d --- /dev/null +++ b/runtime/src/command/platform_specific/wayland/window.rs @@ -0,0 +1,311 @@ +use std::fmt; +use std::marker::PhantomData; + +use iced_core::layout::Limits; +use iced_core::window::Mode; +use iced_futures::MaybeSend; +use sctk::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge; + +use iced_core::window::Id; + +/// window settings +#[derive(Debug, Clone)] +pub struct SctkWindowSettings { + /// window id + pub window_id: Id, + /// optional app id + pub app_id: Option, + /// optional window title + pub title: Option, + /// optional window parent + pub parent: Option, + /// autosize the window to fit its contents + pub autosize: bool, + /// Limits of the window size + pub size_limits: Limits, + + /// The initial size of the window. + pub size: (u32, u32), + + /// Whether the window should be resizable or not. + /// and the size of the window border which can be dragged for a resize + pub resizable: Option, + + /// Whether the window should have a border, a title bar, etc. or not. + pub client_decorations: bool, + + /// Whether the window should be transparent. + pub transparent: bool, + + /// xdg-activation token + pub xdg_activation_token: Option, +} + +impl Default for SctkWindowSettings { + fn default() -> Self { + Self { + window_id: Id::MAIN, + app_id: Default::default(), + title: Default::default(), + parent: Default::default(), + autosize: Default::default(), + size_limits: Limits::NONE + .min_height(1.0) + .min_width(1.0) + .max_width(1920.0) + .max_height(1080.0), + size: (1024, 768), + resizable: Some(8.0), + client_decorations: true, + transparent: false, + xdg_activation_token: Default::default(), + } + } +} + +#[derive(Clone)] +/// Window Action +pub enum Action { + /// create a window and receive a message with its Id + Window { + /// window builder + builder: SctkWindowSettings, + /// phanton + _phantom: PhantomData, + }, + /// Destroy the window + Destroy(Id), + /// Set size of the window. + Size { + /// id of the window + id: Id, + /// The new logical width of the window + width: u32, + /// The new logical height of the window + height: u32, + }, + /// Set min size of the window. + MinSize { + /// id of the window + id: Id, + /// optional size + size: Option<(u32, u32)>, + }, + /// Set max size of the window. + MaxSize { + /// id of the window + id: Id, + /// optional size + size: Option<(u32, u32)>, + }, + /// Set title of the window. + Title { + /// id of the window + id: Id, + /// The new logical width of the window + title: String, + }, + /// Minimize the window. + Minimize { + /// id of the window + id: Id, + }, + /// Toggle maximization of the window. + ToggleMaximized { + /// id of the window + id: Id, + }, + /// Maximize the window. + Maximize { + /// id of the window + id: Id, + }, + /// UnsetMaximize the window. + UnsetMaximize { + /// id of the window + id: Id, + }, + /// Toggle fullscreen of the window. + ToggleFullscreen { + /// id of the window + id: Id, + }, + /// Fullscreen the window. + Fullscreen { + /// id of the window + id: Id, + }, + /// UnsetFullscreen the window. + UnsetFullscreen { + /// id of the window + id: Id, + }, + /// Start an interactive move of the window. + InteractiveResize { + /// id of the window + id: Id, + /// edge being resized + edge: ResizeEdge, + }, + /// Start an interactive move of the window. + InteractiveMove { + /// id of the window + id: Id, + }, + /// Show the window context menu + ShowWindowMenu { + /// id of the window + id: Id, + /// x location of popup + x: i32, + /// y location of popup + y: i32, + }, + /// Set the mode of the window + Mode(Id, Mode), + /// Set the app id of the window + AppId { + /// id of the window + id: Id, + /// app id of the window + app_id: String, + }, +} + +impl Action { + /// Maps the output of a window [`Action`] using the provided closure. + pub fn map( + self, + _: impl Fn(T) -> A + 'static + MaybeSend + Sync, + ) -> Action + where + T: 'static, + { + match self { + Action::Window { builder, .. } => Action::Window { + builder, + _phantom: PhantomData::default(), + }, + Action::Size { id, width, height } => { + Action::Size { id, width, height } + } + Action::MinSize { id, size } => Action::MinSize { id, size }, + Action::MaxSize { id, size } => Action::MaxSize { id, size }, + Action::Title { id, title } => Action::Title { id, title }, + Action::Minimize { id } => Action::Minimize { id }, + Action::Maximize { id } => Action::Maximize { id }, + Action::UnsetMaximize { id } => Action::UnsetMaximize { id }, + Action::Fullscreen { id } => Action::Fullscreen { id }, + Action::UnsetFullscreen { id } => Action::UnsetFullscreen { id }, + Action::InteractiveMove { id } => Action::InteractiveMove { id }, + Action::ShowWindowMenu { id, x, y } => { + Action::ShowWindowMenu { id, x, y } + } + Action::InteractiveResize { id, edge } => { + Action::InteractiveResize { id, edge } + } + Action::Destroy(id) => Action::Destroy(id), + Action::Mode(id, m) => Action::Mode(id, m), + Action::ToggleMaximized { id } => Action::ToggleMaximized { id }, + Action::ToggleFullscreen { id } => Action::ToggleFullscreen { id }, + Action::AppId { id, app_id } => Action::AppId { id, app_id }, + } + } +} + +impl fmt::Debug for Action { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Action::Window { builder, .. } => write!( + f, + "Action::Window::LayerSurface {{ builder: {:?} }}", + builder + ), + Action::Size { id, width, height } => write!( + f, + "Action::Window::Size {{ id: {:?}, width: {:?}, height: {:?} }}", + id, width, height + ), + Action::MinSize { id, size } => write!( + f, + "Action::Window::MinSize {{ id: {:?}, size: {:?} }}", + id, size + ), + Action::MaxSize { id, size } => write!( + f, + "Action::Window::MaxSize {{ id: {:?}, size: {:?} }}", + id, size + ), + Action::Title { id, title } => write!( + f, + "Action::Window::Title {{ id: {:?}, title: {:?} }}", + id, title + ), + Action::Minimize { id } => write!( + f, + "Action::Window::Minimize {{ id: {:?} }}", + id + ), + Action::Maximize { id } => write!( + f, + "Action::Window::Maximize {{ id: {:?} }}", + id + ), + Action::UnsetMaximize { id } => write!( + f, + "Action::Window::UnsetMaximize {{ id: {:?} }}", + id + ), + Action::Fullscreen { id } => write!( + f, + "Action::Window::Fullscreen {{ id: {:?} }}", + id + ), + Action::UnsetFullscreen { id } => write!( + f, + "Action::Window::UnsetFullscreen {{ id: {:?} }}", + id + ), + Action::InteractiveMove { id } => write!( + f, + "Action::Window::InteractiveMove {{ id: {:?} }}", + id + ), + Action::ShowWindowMenu { id, x, y } => write!( + f, + "Action::Window::ShowWindowMenu {{ id: {:?}, x: {x}, y: {y} }}", + id + ), + Action::InteractiveResize { id, edge } => write!( + f, + "Action::Window::InteractiveResize {{ id: {:?}, edge: {:?} }}", + id, edge + ), + Action::Destroy(id) => write!( + f, + "Action::Window::Destroy {{ id: {:?} }}", + id + ), + Action::Mode(id, m) => write!( + f, + "Action::Window::Mode {{ id: {:?}, mode: {:?} }}", + id, m + ), + Action::ToggleMaximized { id } => write!( + f, + "Action::Window::Maximized {{ id: {:?} }}", + id + ), + Action::ToggleFullscreen { id } => write!( + f, + "Action::Window::ToggleFullscreen {{ id: {:?} }}", + id + ), + Action::AppId { id, app_id } => write!( + f, + "Action::Window::Mode {{ id: {:?}, app_id: {:?} }}", + id, app_id + ), + } + } +} diff --git a/runtime/src/program/state.rs b/runtime/src/program/state.rs index dad260c9b3..5ccc9090a1 100644 --- a/runtime/src/program/state.rs +++ b/runtime/src/program/state.rs @@ -29,12 +29,14 @@ where /// Creates a new [`State`] with the provided [`Program`], initializing its /// primitive with the given logical bounds and renderer. pub fn new( + id: crate::window::Id, mut program: P, bounds: Size, renderer: &mut P::Renderer, debug: &mut Debug, ) -> Self { let user_interface = build_user_interface( + id, &mut program, user_interface::Cache::default(), renderer, @@ -90,6 +92,7 @@ where /// after updating it, only if an update was necessary. pub fn update( &mut self, + id: crate::window::Id, bounds: Size, cursor: mouse::Cursor, renderer: &mut P::Renderer, @@ -99,6 +102,7 @@ where debug: &mut Debug, ) -> (Vec, Option>) { let mut user_interface = build_user_interface( + id, &mut self.program, self.cache.take().unwrap(), renderer, @@ -157,6 +161,7 @@ where })); let mut user_interface = build_user_interface( + id, &mut self.program, temp_cache, renderer, @@ -180,6 +185,7 @@ where /// Applies [`Operation`]s to the [`State`] pub fn operate( &mut self, + id: crate::window::Id, renderer: &mut P::Renderer, operations: impl Iterator< Item = Box>>, @@ -188,6 +194,7 @@ where debug: &mut Debug, ) { let mut user_interface = build_user_interface( + id, &mut self.program, self.cache.take().unwrap(), renderer, @@ -221,6 +228,7 @@ where } fn build_user_interface<'a, P: Program>( + id: crate::window::Id, program: &'a mut P, cache: user_interface::Cache, renderer: &mut P::Renderer, diff --git a/runtime/src/window.rs b/runtime/src/window.rs index 2136d64dcf..e20253c03d 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -3,14 +3,13 @@ mod action; pub mod screenshot; +pub use crate::core::window::Id; pub use action::Action; pub use screenshot::Screenshot; use crate::command::{self, Command}; use crate::core::time::Instant; -use crate::core::window::{ - Event, Icon, Id, Level, Mode, Settings, UserAttention, -}; +use crate::core::window::{Event, Icon, Level, Mode, Settings, UserAttention}; use crate::core::{Point, Size}; use crate::futures::event; use crate::futures::Subscription; @@ -25,7 +24,28 @@ use crate::futures::Subscription; /// animations without missing any frames. pub fn frames() -> Subscription { event::listen_raw(|event, _status| match event { - crate::core::Event::Window(_, Event::RedrawRequested(at)) => Some(at), + iced_core::Event::Window(_, Event::RedrawRequested(at)) => Some(at), + _ => None, + }) +} + +#[cfg(feature = "wayland")] +/// Subscribes to the frames of the window of the running application. +/// +/// The resulting [`Subscription`] will produce items at a rate equal to the +/// refresh rate of the window. Note that this rate may be variable, as it is +/// normally managed by the graphics driver and/or the OS. +/// +/// In any case, this [`Subscription`] is useful to smoothly draw application-driven +/// animations without missing any frames. +pub fn wayland_frames() -> Subscription<(Id, Instant)> { + event::listen_raw(|event, _status| match event { + iced_core::Event::Window(id, Event::RedrawRequested(at)) + | iced_core::Event::PlatformSpecific( + iced_core::event::PlatformSpecific::Wayland( + iced_core::event::wayland::Event::Frame(at, _, id), + ), + ) => Some((id, at)), _ => None, }) } diff --git a/sctk/Cargo.toml b/sctk/Cargo.toml new file mode 100644 index 0000000000..9592eb4566 --- /dev/null +++ b/sctk/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "iced_sctk" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +debug = ["iced_runtime/debug"] +system = ["sysinfo"] +application = [] +multi_window = [] +a11y = ["iced_accessibility", "iced_runtime/a11y"] + +[dependencies] +tracing = "0.1" +thiserror = "1.0" +sctk.workspace = true +wayland-protocols.workspace = true +# sctk = { package = "smithay-client-toolkit", path = "../../fork/client-toolkit/" } +raw-window-handle = "0.6" +enum-repr = "0.2" +futures = "0.3" +wayland-backend = {version = "0.3.1", features = ["client_system"]} +float-cmp = "0.9" +smithay-clipboard = "0.6" +xkbcommon-dl = "0.4.1" + +itertools = "0.12" +xkeysym = "0.2.0" +lazy_static = "1.4.0" + +[dependencies.iced_runtime] +path = "../runtime" +features = ["wayland", "multi-window"] + +[dependencies.iced_style] +path = "../style" + +[dependencies.iced_graphics] +path = "../graphics" + + +[dependencies.iced_futures] +path = "../futures" + +[dependencies.sysinfo] +version = "0.28" +optional = true + +[dependencies.iced_accessibility] +path = "../accessibility" +optional = true +features = ["accesskit_unix"] diff --git a/sctk/LICENSE.md b/sctk/LICENSE.md new file mode 100644 index 0000000000..8dc5b15d9a --- /dev/null +++ b/sctk/LICENSE.md @@ -0,0 +1,359 @@ +Mozilla Public License Version 2.0 +================================== + +## 1. Definitions + +### 1.1. "Contributor" +means each individual or legal entity that creates, contributes to +the creation of, or owns Covered Software. + +### 1.2. "Contributor Version" +means the combination of the Contributions of others (if any) used +by a Contributor and that particular Contributor's Contribution. + +### 1.3. "Contribution" +means Covered Software of a particular Contributor. + +### 1.4. "Covered Software" +means Source Code Form to which the initial Contributor has attached +the notice in Exhibit A, the Executable Form of such Source Code +Form, and Modifications of such Source Code Form, in each case +including portions thereof. + +### 1.5. "Incompatible With Secondary Licenses" +means + ++ (a) that the initial Contributor has attached the notice described +in Exhibit B to the Covered Software; or + ++ (b) that the Covered Software was made available under the terms of +version 1.1 or earlier of the License, but not also under the +terms of a Secondary License. + +### 1.6. "Executable Form" +means any form of the work other than Source Code Form. + +### 1.7. "Larger Work" +means a work that combines Covered Software with other material, in +a separate file or files, that is not Covered Software. + +### 1.8. "License" +means this document. + +### 1.9. "Licensable" +means having the right to grant, to the maximum extent possible, +whether at the time of the initial grant or subsequently, any and +all of the rights conveyed by this License. + +### 1.10. "Modifications" +means any of the following: + ++ (a) any file in Source Code Form that results from an addition to, +deletion from, or modification of the contents of Covered +Software; or + ++ (b) any new file in Source Code Form that contains any Covered +Software. + +### 1.11. "Patent Claims" of a Contributor +means any patent claim(s), including without limitation, method, +process, and apparatus claims, in any patent Licensable by such +Contributor that would be infringed, but for the grant of the +License, by the making, using, selling, offering for sale, having +made, import, or transfer of either its Contributions or its +Contributor Version. + +### 1.12. "Secondary License" +means either the GNU General Public License, Version 2.0, the GNU +Lesser General Public License, Version 2.1, the GNU Affero General +Public License, Version 3.0, or any later versions of those +licenses. + +### 1.13. "Source Code Form" +means the form of the work preferred for making modifications. + +### 1.14. "You" (or "Your") +means an individual or a legal entity exercising rights under this +License. For legal entities, "You" includes any entity that +controls, is controlled by, or is under common control with You. For +purposes of this definition, "control" means (a) the power, direct +or indirect, to cause the direction or management of such entity, +whether by contract or otherwise, or (b) ownership of more than +fifty percent (50%) of the outstanding shares or beneficial +ownership of such entity. + +## 2. License Grants and Conditions + +### 2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + ++ (a) under intellectual property rights (other than patent or trademark) +Licensable by such Contributor to use, reproduce, make available, +modify, display, perform, distribute, and otherwise exploit its +Contributions, either on an unmodified basis, with Modifications, or +as part of a Larger Work; and + ++ (b) under Patent Claims of such Contributor to make, use, sell, offer +for sale, have made, import, and otherwise transfer either its +Contributions or its Contributor Version. + +### 2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +### 2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + ++ (a) for any code that a Contributor has removed from Covered Software; +or + ++ (b) for infringements caused by: (i) Your and any other third party's +modifications of Covered Software, or (ii) the combination of its +Contributions with other software (except as part of its Contributor +Version); or + ++ (c) under Patent Claims infringed by Covered Software in the absence of +its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +### 2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +### 2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +### 2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +### 2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +## 3. Responsibilities + +### 3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +### 3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + ++ (a) such Covered Software must also be made available in Source Code +Form, as described in Section 3.1, and You must inform recipients of +the Executable Form how they can obtain a copy of such Source Code +Form by reasonable means in a timely manner, at a charge no more +than the cost of distribution to the recipient; and + ++ (b) You may distribute such Executable Form under the terms of this +License, or sublicense it under different terms, provided that the +license for the Executable Form does not attempt to limit or alter +the recipients' rights in the Source Code Form under this License. + +### 3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +### 3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +### 3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +## 4. Inability to Comply Due to Statute or Regulation + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +## 5. Termination + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + + +## 6. Disclaimer of Warranty + +**Covered Software is provided under this License on an "as is" +basis, without warranty of any kind, either expressed, implied, or +statutory, including, without limitation, warranties that the +Covered Software is free of defects, merchantable, fit for a +particular purpose or non-infringing. The entire risk as to the +quality and performance of the Covered Software is with You. +Should any Covered Software prove defective in any respect, You +(not any Contributor) assume the cost of any necessary servicing, +repair, or correction. This disclaimer of warranty constitutes an +essential part of this License. No use of any Covered Software is +authorized under this License except under this disclaimer.** + + +#7. Limitation of Liability + +**Under no circumstances and under no legal theory, whether tort +(including negligence), contract, or otherwise, shall any +Contributor, or anyone who distributes Covered Software as +permitted above, be liable to You for any direct, indirect, +special, incidental, or consequential damages of any character +including, without limitation, damages for lost profits, loss of +goodwill, work stoppage, computer failure or malfunction, or any +and all other commercial damages or losses, even if such party +shall have been informed of the possibility of such damages. This +limitation of liability shall not apply to liability for death or +personal injury resulting from such party's negligence to the +extent applicable law prohibits such limitation. Some +jurisdictions do not allow the exclusion or limitation of +incidental or consequential damages, so this exclusion and +limitation may not apply to You.** + + +## 8. Litigation + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +## 9. Miscellaneous + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +## 10. Versions of the License + +### 10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +### 10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +### 10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +### 10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +## Exhibit A - Source Code Form License Notice + + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +## Exhibit B - "Incompatible With Secondary Licenses" Notice + + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. + diff --git a/sctk/src/adaptor.rs b/sctk/src/adaptor.rs new file mode 100644 index 0000000000..c26b8e5cf4 --- /dev/null +++ b/sctk/src/adaptor.rs @@ -0,0 +1,42 @@ +use accesskit::{kurbo::Rect, ActionHandler, TreeUpdate}; +use accesskit_unix::Adapter as UnixAdapter; +use winit::window::Window; + +pub struct Adapter { + adapter: Option, +} + +impl Adapter { + pub fn new( + _: &Window, + source: impl 'static + FnOnce() -> TreeUpdate, + action_handler: Box, + ) -> Self { + let adapter = UnixAdapter::new( + String::new(), + String::new(), + String::new(), + source, + action_handler, + ); + Self { adapter } + } + + pub fn set_root_window_bounds(&self, outer: Rect, inner: Rect) { + if let Some(adapter) = &self.adapter { + adapter.set_root_window_bounds(outer, inner); + } + } + + pub fn update(&self, update: TreeUpdate) { + if let Some(adapter) = &self.adapter { + adapter.update(update); + } + } + + pub fn update_if_active(&self, updater: impl FnOnce() -> TreeUpdate) { + if let Some(adapter) = &self.adapter { + adapter.update(updater()); + } + } +} diff --git a/sctk/src/application.rs b/sctk/src/application.rs new file mode 100644 index 0000000000..3181ed4017 --- /dev/null +++ b/sctk/src/application.rs @@ -0,0 +1,2196 @@ +#[cfg(feature = "a11y")] +use crate::sctk_event::ActionRequestEvent; +use crate::{ + clipboard::Clipboard, + commands::{layer_surface::get_layer_surface, window::get_window}, + dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}, + error::{self, Error}, + event_loop::{ + control_flow::ControlFlow, proxy, state::SctkState, SctkEventLoop, + }, + sctk_event::{ + DataSourceEvent, IcedSctkEvent, KeyboardEventVariant, + LayerSurfaceEventVariant, PopupEventVariant, SctkEvent, StartCause, + }, + settings, +}; +use float_cmp::{approx_eq, F32Margin, F64Margin}; +use futures::{channel::mpsc, task, Future, FutureExt, StreamExt}; +#[cfg(feature = "a11y")] +use iced_accessibility::{ + accesskit::{NodeBuilder, NodeId}, + A11yId, A11yNode, +}; +use iced_futures::{ + core::{ + event::{Event as CoreEvent, Status}, + layout::Limits, + mouse, + renderer::Style, + time::Instant, + widget::{ + operation::{self, focusable::focus, OperationWrapper}, + tree, Operation, Tree, + }, + Widget, + }, + Executor, Runtime, Subscription, +}; +use tracing::error; + +use sctk::{ + reexports::client::{protocol::wl_surface::WlSurface, Proxy, QueueHandle}, + seat::{keyboard::Modifiers, pointer::PointerEventKind}, +}; +use std::{ + borrow::BorrowMut, + collections::HashMap, + hash::Hash, + marker::PhantomData, + os::raw::c_void, + ptr::NonNull, + sync::{Arc, Mutex}, + time::Duration, +}; +use wayland_backend::client::ObjectId; +use wayland_protocols::wp::viewporter::client::wp_viewport::WpViewport; + +use iced_graphics::{compositor, renderer, Compositor, Viewport}; +use iced_runtime::{ + clipboard, + command::{ + self, + platform_specific::{ + self, + wayland::{data_device::DndIcon, popup}, + }, + }, + core::{mouse::Interaction, Color, Point, Renderer, Size}, + multi_window::Program, + system, user_interface, + window::Id as SurfaceId, + Command, Debug, UserInterface, +}; +use iced_style::application::{self, StyleSheet}; +use itertools::Itertools; +use raw_window_handle::{ + DisplayHandle, HandleError, HasDisplayHandle, HasRawDisplayHandle, + HasRawWindowHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle, + WaylandDisplayHandle, WaylandWindowHandle, WindowHandle, +}; +use std::mem::ManuallyDrop; + +pub enum Event { + /// A normal sctk event + SctkEvent(IcedSctkEvent), + /// TODO + // (maybe we should also allow users to listen/react to those internal messages?) + + /// layer surface requests from the client + LayerSurface(platform_specific::wayland::layer_surface::Action), + /// window requests from the client + Window(platform_specific::wayland::window::Action), + /// popup requests from the client + Popup(platform_specific::wayland::popup::Action), + /// data device requests from the client + DataDevice(platform_specific::wayland::data_device::Action), + /// xdg-activation request from the client + Activation(platform_specific::wayland::activation::Action), + /// data session lock requests from the client + SessionLock(platform_specific::wayland::session_lock::Action), + /// request sctk to set the cursor of the active pointer + SetCursor(Interaction), + /// Application Message + Message(Message), +} + +pub struct IcedSctkState; + +#[derive(Debug, Clone)] +pub struct SurfaceDisplayWrapper { + backend: wayland_backend::client::Backend, + wl_surface: WlSurface, +} + +impl HasDisplayHandle for SurfaceDisplayWrapper { + fn display_handle(&self) -> Result { + let mut ptr = self.backend.display_ptr() as *mut c_void; + let Some(ptr) = NonNull::new(ptr) else { + return Err(HandleError::Unavailable); + }; + let mut display_handle = WaylandDisplayHandle::new(ptr); + Ok(unsafe { + DisplayHandle::borrow_raw(RawDisplayHandle::Wayland(display_handle)) + }) + } +} + +impl HasWindowHandle for SurfaceDisplayWrapper { + fn window_handle(&self) -> Result { + let ptr = self.wl_surface.id().as_ptr() as *mut c_void; + let Some(ptr) = NonNull::new(ptr) else { + return Err(HandleError::Unavailable); + }; + let window_handle = WaylandWindowHandle::new(ptr); + Ok(unsafe { + WindowHandle::borrow_raw(RawWindowHandle::Wayland(window_handle)) + }) + } +} + +/// An interactive, native, cross-platform, multi-windowed application. +/// +/// This trait is the main entrypoint of multi-window Iced. Once implemented, you can run +/// your GUI application by simply calling [`run`]. It will run in +/// its own window. +/// +/// An [`Application`] can execute asynchronous actions by returning a +/// [`Command`] in some of its methods. +/// +/// When using an [`Application`] with the `debug` feature enabled, a debug view +/// can be toggled by pressing `F12`. +pub trait Application: Program +where + Self::Theme: StyleSheet, +{ + /// The data needed to initialize your [`Application`]. + type Flags; + + /// Initializes the [`Application`] with the flags provided to + /// [`run`] as part of the [`Settings`]. + /// + /// Here is where you should return the initial state of your app. + /// + /// Additionally, you can return a [`Command`] if you need to perform some + /// async action in the background on startup. This is useful if you want to + /// load state from a file, perform an initial HTTP request, etc. + fn new(flags: Self::Flags) -> (Self, Command); + + /// Returns the current title of the [`Application`]. + /// + /// This title can be dynamic! The runtime will automatically update the + /// title of your application when necessary. + fn title(&self, window: SurfaceId) -> String; + + /// Returns the current `Theme` of the [`Application`]. + fn theme(&self, window: SurfaceId) -> Self::Theme; + + /// Returns the `Style` variation of the `Theme`. + fn style(&self) -> ::Style { + Default::default() + } + + /// Returns the event `Subscription` for the current state of the + /// application. + /// + /// The messages produced by the `Subscription` will be handled by + /// [`update`](#tymethod.update). + /// + /// A `Subscription` will be kept alive as long as you keep returning it! + /// + /// By default, it returns an empty subscription. + fn subscription(&self) -> Subscription { + Subscription::none() + } + + /// Returns the scale factor of the window of the [`Application`]. + /// + /// It can be used to dynamically control the size of the UI at runtime + /// (i.e. zooming). + /// + /// For instance, a scale factor of `2.0` will make widgets twice as big, + /// while a scale factor of `0.5` will shrink them to half their size. + /// + /// By default, it returns `1.0`. + #[allow(unused_variables)] + fn scale_factor(&self, window: SurfaceId) -> f64 { + 1.0 + } +} + +/// Runs an [`Application`] with an executor, compositor, and the provided +/// settings. +pub fn run( + settings: settings::Settings, + compositor_settings: C::Settings, +) -> Result<(), error::Error> +where + A: Application + 'static, + E: Executor + 'static, + C: Compositor + 'static, + ::Theme: StyleSheet, +{ + let mut debug = Debug::new(); + debug.startup_started(); + + let exit_on_close_request = settings.exit_on_close_request; + + let mut event_loop = SctkEventLoop::::new(&settings) + .expect("Failed to initialize the event loop"); + + let (runtime, ev_proxy) = { + let ev_proxy = event_loop.proxy(); + let executor = E::new().map_err(Error::ExecutorCreationFailed)?; + + (Runtime::new(executor, ev_proxy.clone()), ev_proxy) + }; + + let (application, init_command) = { + let flags = settings.flags; + + runtime.enter(|| A::new(flags)) + }; + + let init_command = match settings.surface { + settings::InitialSurface::LayerSurface(b) => { + Command::batch(vec![init_command, get_layer_surface(b)]) + } + settings::InitialSurface::XdgWindow(b) => { + Command::batch(vec![init_command, get_window(b)]) + } + settings::InitialSurface::None => init_command, + }; + let wl_surface = event_loop + .state + .compositor_state + .create_surface(&event_loop.state.queue_handle); + + // let (display, context, config, surface) = init_egl(&wl_surface, 100, 100); + let backend = event_loop + .wayland_dispatcher + .as_source_ref() + .connection() + .backend(); + let qh = event_loop.state.queue_handle.clone(); + let wrapper = SurfaceDisplayWrapper { + backend: backend.clone(), + wl_surface, + }; + + #[allow(unsafe_code)] + let compositor = C::new(compositor_settings, wrapper.clone()).unwrap(); + let renderer = compositor.create_renderer(); + + let auto_size_surfaces = HashMap::new(); + + let surface_ids = Default::default(); + + let (mut sender, receiver) = mpsc::unbounded::>(); + let (control_sender, mut control_receiver) = mpsc::unbounded(); + + let mut instance = Box::pin(run_instance::( + application, + compositor, + renderer, + runtime, + ev_proxy, + debug, + receiver, + control_sender, + surface_ids, + auto_size_surfaces, + // display, + // context, + // config, + backend, + init_command, + exit_on_close_request, + qh, + )); + + let mut context = task::Context::from_waker(task::noop_waker_ref()); + + let _ = event_loop.run_return(move |event, _, control_flow| { + if let ControlFlow::ExitWithCode(_) = control_flow { + return; + } + + sender.start_send(event).expect("Failed to send event"); + + let poll = instance.as_mut().poll(&mut context); + + match poll { + task::Poll::Pending => { + if let Ok(Some(flow)) = control_receiver.try_next() { + *control_flow = flow + } + } + task::Poll::Ready(_) => { + *control_flow = ControlFlow::ExitWithCode(1) + } + }; + }); + + Ok(()) +} + +fn subscription_map(e: A::Message) -> Event +where + A: Application + 'static, + E: Executor + 'static, + C: Compositor + 'static, + ::Theme: StyleSheet, +{ + Event::SctkEvent(IcedSctkEvent::UserEvent(e)) +} + +// XXX Ashley careful, A, E, C must be exact same as in update, or the subscription map type will have a different hash +async fn run_instance( + mut application: A, + mut compositor: C, + mut renderer: A::Renderer, + mut runtime: Runtime>, Event>, + mut ev_proxy: proxy::Proxy>, + mut debug: Debug, + mut receiver: mpsc::UnboundedReceiver>, + mut control_sender: mpsc::UnboundedSender, + mut surface_ids: HashMap, + mut auto_size_surfaces: HashMap, + backend: wayland_backend::client::Backend, + init_command: Command, + exit_on_close_request: bool, + queue_handle: QueueHandle::Message>>, +) -> Result<(), Error> +where + A: Application + 'static, + E: Executor + 'static, + C: Compositor + 'static, + ::Theme: StyleSheet, +{ + let mut cache = user_interface::Cache::default(); + + let mut states: HashMap> = HashMap::new(); + let mut interfaces = ManuallyDrop::new(HashMap::new()); + let mut simple_clipboard = Clipboard::unconnected(); + + { + run_command( + &application, + &mut cache, + None::<&State>, + &mut renderer, + init_command, + &mut runtime, + &mut ev_proxy, + &mut debug, + || compositor.fetch_information(), + &mut auto_size_surfaces, + &mut Vec::new(), + &mut simple_clipboard, + ); + } + runtime.track( + application + .subscription() + .map(subscription_map::) + .into_recipes(), + ); + + let mut mouse_interaction = Interaction::default(); + let mut sctk_events: Vec = Vec::new(); + #[cfg(feature = "a11y")] + let mut a11y_events: Vec = + Vec::new(); + #[cfg(feature = "a11y")] + let mut a11y_enabled = false; + #[cfg(feature = "a11y")] + let mut adapters: HashMap< + SurfaceId, + crate::event_loop::adapter::IcedSctkAdapter, + > = HashMap::new(); + + let mut messages: Vec = Vec::new(); + #[cfg(feature = "a11y")] + let mut commands: Vec> = Vec::new(); + let mut redraw_pending = false; + + debug.startup_finished(); + + // let mut current_context_window = init_id_inner; + + let mut kbd_surface_id: Option = None; + let mut mods: Modifiers = Modifiers::default(); + let mut destroyed_surface_ids: HashMap = + Default::default(); + + 'main: while let Some(event) = receiver.next().await { + match event { + IcedSctkEvent::NewEvents(start_cause) => { + redraw_pending = matches!( + start_cause, + StartCause::Init + | StartCause::Poll + | StartCause::ResumeTimeReached { .. } + ); + } + IcedSctkEvent::UserEvent(message) => { + messages.push(message); + } + IcedSctkEvent::SctkEvent(event) => { + sctk_events.push(event.clone()); + match event { + SctkEvent::SeatEvent { .. } => {} // TODO Ashley: handle later possibly if multiseat support is wanted + SctkEvent::PointerEvent { + variant, + .. + } => { + let (state, _native_id) = match surface_ids + .get(&variant.surface.id()) + .and_then(|id| states.get_mut(&id.inner()).map(|state| (state, id))) + { + Some(s) => s, + None => continue, + }; + match variant.kind { + PointerEventKind::Enter { .. } => { + state.set_cursor_position(Some(LogicalPosition { x: variant.position.0, y: variant.position.1 })); + } + PointerEventKind::Leave { .. } => { + state.set_cursor_position(None); + } + PointerEventKind::Motion { .. } => { + state.set_cursor_position(Some(LogicalPosition { x: variant.position.0, y: variant.position.1 })); + } + PointerEventKind::Press { .. } + | PointerEventKind::Release { .. } + | PointerEventKind::Axis { .. } => {} + } + } + SctkEvent::KeyboardEvent { variant, .. } => match variant { + KeyboardEventVariant::Leave(_) => { + kbd_surface_id.take(); + } + KeyboardEventVariant::Enter(object_id) => { + kbd_surface_id.replace(object_id.id()); + } + KeyboardEventVariant::Press(_) + | KeyboardEventVariant::Release(_) + | KeyboardEventVariant::Repeat(_) => {} + KeyboardEventVariant::Modifiers(mods) => { + if let Some(state) = kbd_surface_id + .as_ref() + .and_then(|id| surface_ids.get(id)) + .and_then(|id| states.get_mut(&id.inner())) + { + state.modifiers = mods; + } + } + }, + SctkEvent::WindowEvent { variant, id: wl_surface } => match variant { + crate::sctk_event::WindowEventVariant::Created(id, native_id) => { + surface_ids.insert(id, SurfaceIdWrapper::Window(native_id)); + states.insert(native_id, State::new(&application, SurfaceIdWrapper::Window(native_id), SurfaceDisplayWrapper { + backend: backend.clone(), + wl_surface + })); + } + crate::sctk_event::WindowEventVariant::Close => { + if let Some(surface_id) = surface_ids.remove(&wl_surface.id()) { + // drop(compositor_surfaces.remove(&surface_id.inner())); + auto_size_surfaces.remove(&surface_id); + interfaces.remove(&surface_id.inner()); + states.remove(&surface_id.inner()); + destroyed_surface_ids.insert(wl_surface.id(), surface_id); + if exit_on_close_request && states.is_empty() { + break 'main; + } + } + } + crate::sctk_event::WindowEventVariant::WmCapabilities(_) + | crate::sctk_event::WindowEventVariant::ConfigureBounds { .. } => {} + crate::sctk_event::WindowEventVariant::Configure( + configure, + wl_surface, + first, + ) => { + if let Some(id) = surface_ids.get(&wl_surface.id()) { + let Some(state) = states.get_mut(&id.inner()) else { + continue; + }; + if state.surface.is_none() { + let wrapper = SurfaceDisplayWrapper { + backend: backend.clone(), + wl_surface + }; + if matches!(simple_clipboard.state, crate::clipboard::State::Unavailable) { + if let Ok(h) = wrapper.display_handle() { + if let RawDisplayHandle::Wayland(mut h) = h.as_raw() { + simple_clipboard = unsafe { Clipboard::connect(h.display.as_mut()) }; + } + } + } + let mut c_surface = compositor.create_surface(wrapper.clone(), configure.new_size.0.unwrap().get(), configure.new_size.1.unwrap().get()); + compositor.configure_surface(&mut c_surface, configure.new_size.0.unwrap().get(), configure.new_size.1.unwrap().get()); + state.surface = Some(c_surface); + } + if first { + let user_interface = build_user_interface( + &application, + user_interface::Cache::default(), + &mut renderer, + state.logical_size(), + &state.title, + &mut debug, + *id, + &mut auto_size_surfaces, + &mut ev_proxy + ); + interfaces.insert(id.inner(), user_interface); + } + if let Some((w, h, _, dirty)) = auto_size_surfaces.get_mut(id) { + if *w == configure.new_size.0.unwrap().get() && *h == configure.new_size.1.unwrap().get() { + *dirty = false; + } else { + continue; + } + } + state.set_logical_size(configure.new_size.0.unwrap().get() as f64 , configure.new_size.1.unwrap().get() as f64); + } + } + crate::sctk_event::WindowEventVariant::ScaleFactorChanged(sf, viewport) => { + if let Some(state) = surface_ids + .get(&wl_surface.id()) + .and_then(|id| states.get_mut(&id.inner())) + { + state.wp_viewport = viewport; + state.set_scale_factor(sf); + } + }, + // handled by the application + crate::sctk_event::WindowEventVariant::StateChanged(_) => {}, + }, + SctkEvent::LayerSurfaceEvent { variant, id: wl_surface } => match variant { + LayerSurfaceEventVariant::Created(id, native_id) => { + surface_ids.insert(id, SurfaceIdWrapper::LayerSurface(native_id)); + states.insert(native_id, State::new(&application, SurfaceIdWrapper::LayerSurface(native_id), SurfaceDisplayWrapper { + backend: backend.clone(), + wl_surface: wl_surface.clone() + })); + + } + LayerSurfaceEventVariant::Done => { + if let Some(surface_id) = surface_ids.remove(&wl_surface.id()) { + if kbd_surface_id == Some(wl_surface.id()) { + kbd_surface_id = None; + } + auto_size_surfaces.remove(&surface_id); + interfaces.remove(&surface_id.inner()); + states.remove(&surface_id.inner()); + destroyed_surface_ids.insert(wl_surface.id(), surface_id); + if exit_on_close_request && states.is_empty() { + break 'main; + } + } + } + LayerSurfaceEventVariant::Configure(configure, wl_surface, first) => { + if let Some(id) = surface_ids.get(&wl_surface.id()) { + let Some(state) = states.get_mut(&id.inner()) else { + continue; + }; + if state.surface.is_none() { + let wrapper = SurfaceDisplayWrapper { + backend: backend.clone(), + wl_surface + }; + if matches!(simple_clipboard.state, crate::clipboard::State::Unavailable) { + if let Ok(h) = wrapper.display_handle() { + if let RawDisplayHandle::Wayland(mut h) = h.as_raw() { + simple_clipboard = unsafe { Clipboard::connect(h.display.as_mut()) }; + } + } + } + let mut c_surface = compositor.create_surface(wrapper.clone(), configure.new_size.0, configure.new_size.1); + compositor.configure_surface(&mut c_surface, configure.new_size.0, configure.new_size.1); + state.surface = Some(c_surface); + }; + if first { + let user_interface = build_user_interface( + &application, + user_interface::Cache::default(), + &mut renderer, + state.logical_size(), + &state.title, + &mut debug, + *id, + &mut auto_size_surfaces, + &mut ev_proxy + ); + interfaces.insert(id.inner(), user_interface); + } + if let Some((w, h, _, dirty)) = auto_size_surfaces.get_mut(id) { + if *w == configure.new_size.0 && *h == configure.new_size.1 { + *dirty = false; + } else { + continue; + } + } + if let Some(state) = states.get_mut(&id.inner()) { + state.set_logical_size( + configure.new_size.0 as f64, + configure.new_size.1 as f64, + ); + } + } + } + LayerSurfaceEventVariant::ScaleFactorChanged(sf, viewport) => { + if let Some(state) = surface_ids + .get(&wl_surface.id()) + .and_then(|id| states.get_mut(&id.inner())) + { + state.wp_viewport = viewport; + state.set_scale_factor(sf); + } + }, + }, + SctkEvent::PopupEvent { + variant, + toplevel_id: _, + parent_id: _, + id: wl_surface, + } => match variant { + PopupEventVariant::Created(id, native_id) => { + surface_ids.insert(id, SurfaceIdWrapper::Popup(native_id)); + states.insert(native_id, State::new(&application, SurfaceIdWrapper::Popup(native_id),SurfaceDisplayWrapper { + backend: backend.clone(), + wl_surface + })); + + + } + PopupEventVariant::Done => { + if let Some(surface_id) = surface_ids.remove(&wl_surface.id()) { + auto_size_surfaces.remove(&surface_id); + interfaces.remove(&surface_id.inner()); + states.remove(&surface_id.inner()); + destroyed_surface_ids.insert(wl_surface.id(), surface_id); + } + } + PopupEventVariant::Configure(configure, wl_surface, first) => { + if let Some(id) = surface_ids.get(&wl_surface.id()) { + let Some(state) = states.get_mut(&id.inner()) else { + continue; + }; + if state.surface.is_none() { + let wrapper = SurfaceDisplayWrapper { + backend: backend.clone(), + wl_surface + }; + let c_surface = compositor.create_surface(wrapper.clone(), configure.width as u32, configure.height as u32); + + state.surface = Some(c_surface); + } + if first { + let user_interface = build_user_interface( + &application, + user_interface::Cache::default(), + &mut renderer, + state.logical_size(), + &state.title, + &mut debug, + *id, + &mut auto_size_surfaces, + &mut ev_proxy + ); + interfaces.insert(id.inner(), user_interface); + } + if let Some((w, h, _, dirty)) = auto_size_surfaces.get_mut(id) { + if *w == configure.width as u32 && *h == configure.height as u32 { + *dirty = false; + } else { + continue; + } + } + state.set_logical_size( + configure.width as f64, + configure.height as f64, + ); + } + } + PopupEventVariant::RepositionionedPopup { .. } => {} + PopupEventVariant::Size(width, height) => { + if let Some(id) = surface_ids.get(&wl_surface.id()) { + if let Some(state) = states.get_mut(&id.inner()) { + state.set_logical_size( + width as f64, + height as f64, + ); + } + if let Some((w, h, _, dirty)) = auto_size_surfaces.get_mut(id) { + if *w == width && *h == height { + *dirty = false; + } else { + continue; + } + } + } + }, + PopupEventVariant::ScaleFactorChanged(sf, viewport) => { + if let Some(id) = surface_ids.get(&wl_surface.id()) { + if let Some(state) = states.get_mut(&id.inner()) { + state.wp_viewport = viewport; + state.set_scale_factor(sf); + } + } + }, + }, + // TODO forward these events to an application which requests them? + SctkEvent::NewOutput { .. } => { + } + SctkEvent::UpdateOutput { .. } => { + } + SctkEvent::RemovedOutput( ..) => { + } + SctkEvent::ScaleFactorChanged { .. } => {} + SctkEvent::DataSource(DataSourceEvent::DndFinished) | SctkEvent::DataSource(DataSourceEvent::DndCancelled)=> { + surface_ids.retain(|id, surface_id| { + match surface_id { + SurfaceIdWrapper::Dnd(inner) => { + interfaces.remove(inner); + states.remove(inner); + destroyed_surface_ids.insert(id.clone(), *surface_id); + false + }, + _ => true, + } + }) + } + SctkEvent::SessionLockSurfaceCreated { surface, native_id } => { + surface_ids.insert(surface.id(), SurfaceIdWrapper::SessionLock(native_id)); + states.insert(native_id, State::new(&application, SurfaceIdWrapper::SessionLock(native_id), SurfaceDisplayWrapper { + backend: backend.clone(), + wl_surface: surface.clone() + } + )); + } + SctkEvent::SessionLockSurfaceConfigure { surface, configure, first } => { + if let Some(id) = surface_ids.get(&surface.id()) { + let Some(state) = states.get_mut(&id.inner()) else { + continue; + }; + if state.surface.is_none() { + let c_surface = compositor.create_surface(state.wrapper.clone(), configure.new_size.0, configure.new_size.1); + + state.surface = Some(c_surface); + } + if first { + let user_interface = build_user_interface( + &application, + user_interface::Cache::default(), + &mut renderer, + state.logical_size(), + &state.title, + &mut debug, + *id, + &mut auto_size_surfaces, + &mut ev_proxy + ); + interfaces.insert(id.inner(), user_interface); + } + + state.set_logical_size(configure.new_size.0 as f64 , configure.new_size.1 as f64); + } + + } + _ => {} + } + } + IcedSctkEvent::DndSurfaceCreated( + wl_surface, + dnd_icon, + origin_id, + ) => { + // if the surface is meant to be drawn as a custom widget by the + // application, we should treat it like any other surfaces + // + // TODO if the surface is meant to be drawn by a widget that implements + // draw_dnd_icon, we should mark it and not pass it to the view method + // of the Application + // + // Dnd Surfaces are only drawn once + + let id = wl_surface.id(); + let (native_id, e, node) = match dnd_icon { + DndIcon::Custom(id) => { + let mut e = application.view(id); + let state = e.as_widget().state(); + let tag = e.as_widget().tag(); + let mut tree = Tree { + id: e.as_widget().id(), + tag, + state, + children: e.as_widget().children(), + }; + e.as_widget_mut().diff(&mut tree); + let node = Widget::layout( + e.as_widget(), + &mut tree, + &renderer, + &Limits::NONE, + ); + (id, e, node) + } + DndIcon::Widget(id, widget_state) => { + let mut e = application.view(id); + let mut tree = Tree { + id: e.as_widget().id(), + tag: e.as_widget().tag(), + state: tree::State::Some(widget_state), + children: e.as_widget().children(), + }; + e.as_widget_mut().diff(&mut tree); + let node = Widget::layout( + e.as_widget(), + &mut tree, + &renderer, + &Limits::NONE, + ); + (id, e, node) + } + }; + + let bounds = node.bounds(); + let (w, h) = ( + (bounds.width.round()) as u32, + (bounds.height.round()) as u32, + ); + if w == 0 || h == 0 { + error!("Dnd surface has zero size, ignoring"); + continue; + } + let parent_size = states + .get(&origin_id) + .map(|s| s.logical_size()) + .unwrap_or_else(|| Size::new(1024.0, 1024.0)); + if w > parent_size.width as u32 || h > parent_size.height as u32 + { + error!("Dnd surface is too large, ignoring"); + continue; + } + let wrapper = SurfaceDisplayWrapper { + backend: backend.clone(), + wl_surface, + }; + let mut c_surface = + compositor.create_surface(wrapper.clone(), w, h); + compositor.configure_surface(&mut c_surface, w, h); + let mut state = State::new( + &application, + SurfaceIdWrapper::Dnd(native_id), + wrapper, + ); + state.surface = Some(c_surface); + state.set_logical_size(w as f64, h as f64); + let mut user_interface = build_user_interface( + &application, + user_interface::Cache::default(), + &mut renderer, + state.logical_size(), + &state.title, + &mut debug, + SurfaceIdWrapper::Dnd(native_id), + &mut auto_size_surfaces, + &mut ev_proxy, + ); + state.synchronize(&application); + + // just draw here immediately and never again for dnd icons + // TODO handle scale factor? + let _new_mouse_interaction = user_interface.draw( + &mut renderer, + state.theme(), + &Style { + text_color: state.text_color(), + }, + state.cursor(), + ); + + let _ = compositor.present( + &mut renderer, + state.surface.as_mut().unwrap(), + &state.viewport, + Color::TRANSPARENT, + &debug.overlay(), + ); + + surface_ids.insert(id, SurfaceIdWrapper::Dnd(native_id)); + + states.insert(native_id, state); + interfaces.insert(native_id, user_interface); + } + IcedSctkEvent::MainEventsCleared => { + if !redraw_pending + && sctk_events.is_empty() + && messages.is_empty() + { + continue; + } + + let mut i = 0; + while i < sctk_events.len() { + let remove = matches!( + sctk_events[i], + SctkEvent::NewOutput { .. } + | SctkEvent::UpdateOutput { .. } + | SctkEvent::RemovedOutput(_) + | SctkEvent::SessionLocked + | SctkEvent::SessionLockFinished + | SctkEvent::SessionUnlocked + ); + if remove { + let event = sctk_events.remove(i); + for native_event in event.to_native( + &mut mods, + &surface_ids, + &destroyed_surface_ids, + ) { + runtime.broadcast(native_event, Status::Ignored); + } + } else { + i += 1; + } + } + + if surface_ids.is_empty() && !messages.is_empty() { + // Update application + let pure_states: HashMap<_, _> = + ManuallyDrop::into_inner(interfaces) + .drain() + .map(|(id, interface)| (id, interface.into_cache())) + .collect(); + + // Update application + update::( + &mut application, + &mut cache, + None, + &mut renderer, + &mut runtime, + &mut ev_proxy, + &mut debug, + &mut messages, + &mut Vec::new(), + || compositor.fetch_information(), + &mut auto_size_surfaces, + &mut simple_clipboard, + ); + + interfaces = ManuallyDrop::new(build_user_interfaces( + &application, + &mut renderer, + &mut debug, + &states, + pure_states, + &mut auto_size_surfaces, + &mut ev_proxy, + )); + + let _ = control_sender.start_send(ControlFlow::Wait); + } else { + let mut actions = Vec::new(); + let mut needs_update = false; + + for (object_id, surface_id) in &surface_ids { + if matches!(surface_id, SurfaceIdWrapper::Dnd(_)) { + continue; + } + let mut filtered_sctk = + Vec::with_capacity(sctk_events.len()); + let Some(state) = states.get_mut(&surface_id.inner()) + else { + continue; + }; + let mut i = 0; + + while i < sctk_events.len() { + let has_kbd_focus = + kbd_surface_id.as_ref() == Some(object_id); + if event_is_for_surface( + &sctk_events[i], + object_id, + has_kbd_focus, + ) { + filtered_sctk.push(sctk_events.remove(i)); + } else { + i += 1; + } + } + let has_events = !sctk_events.is_empty(); + debug.event_processing_started(); + #[allow(unused_mut)] + let mut native_events: Vec<_> = filtered_sctk + .into_iter() + .flat_map(|e| { + e.to_native( + &mut mods, + &surface_ids, + &destroyed_surface_ids, + ) + }) + .collect(); + + #[cfg(feature = "a11y")] + { + let mut filtered_a11y = + Vec::with_capacity(a11y_events.len()); + while i < a11y_events.len() { + if a11y_events[i].surface_id == *object_id { + filtered_a11y.push(a11y_events.remove(i)); + } else { + i += 1; + } + } + native_events.extend( + filtered_a11y.into_iter().map(|e| { + iced_runtime::core::event::Event::A11y( + iced_runtime::core::id::Id::from( + u128::from(e.request.target.0) + as u64, + ), + e.request, + ) + }), + ); + } + let has_events = + has_events || !native_events.is_empty(); + + let (interface_state, statuses) = { + let Some(user_interface) = + interfaces.get_mut(&surface_id.inner()) + else { + continue; + }; + user_interface.update( + native_events.as_slice(), + state.cursor(), + &mut renderer, + &mut simple_clipboard, + &mut messages, + ) + }; + state.interface_state = interface_state; + debug.event_processing_finished(); + for (event, status) in + native_events.into_iter().zip(statuses.into_iter()) + { + runtime.broadcast(event, status); + } + + needs_update = !messages.is_empty() + || matches!( + interface_state, + user_interface::State::Outdated + ) + || state.first() + || has_events + || state.viewport_changed; + if redraw_pending || needs_update { + state.set_needs_redraw( + state.frame.is_some() || needs_update, + ); + state.set_first(false); + } + } + if needs_update { + let mut pure_states: HashMap<_, _> = + ManuallyDrop::into_inner(interfaces) + .drain() + .map(|(id, interface)| { + (id, interface.into_cache()) + }) + .collect(); + + for surface_id in surface_ids.values() { + let state = + match states.get_mut(&surface_id.inner()) { + Some(s) => { + if !s.needs_redraw() { + continue; + } else { + s + } + } + None => continue, + }; + let mut cache = + match pure_states.remove(&surface_id.inner()) { + Some(cache) => cache, + None => user_interface::Cache::default(), + }; + + // Update application + update::( + &mut application, + &mut cache, + Some(state), + &mut renderer, + &mut runtime, + &mut ev_proxy, + &mut debug, + &mut messages, + &mut actions, + || compositor.fetch_information(), + &mut auto_size_surfaces, + &mut simple_clipboard, + ); + + pure_states.insert(surface_id.inner(), cache); + + // Update state + state.synchronize(&application); + } + interfaces = ManuallyDrop::new(build_user_interfaces( + &application, + &mut renderer, + &mut debug, + &states, + pure_states, + &mut auto_size_surfaces, + &mut ev_proxy, + )); + } + let mut sent_control_flow = false; + for (object_id, surface_id) in &surface_ids { + let state = match states.get_mut(&surface_id.inner()) { + Some(s) => { + if !s.needs_redraw() { + continue; + } else if auto_size_surfaces + .get(surface_id) + .map(|(w, h, _, dirty)| { + // don't redraw yet if the autosize state is dirty + *dirty || { + let Size { width, height } = + s.logical_size(); + width.round() as u32 != *w + || height.round() as u32 != *h + } + }) + .unwrap_or_default() + { + continue; + } else { + s.set_needs_redraw(false); + + s + } + } + None => continue, + }; + + let redraw_event = CoreEvent::Window( + surface_id.inner(), + crate::core::window::Event::RedrawRequested( + Instant::now(), + ), + ); + let Some(user_interface) = + interfaces.get_mut(&surface_id.inner()) + else { + continue; + }; + let (interface_state, _) = user_interface.update( + &[redraw_event.clone()], + state.cursor(), + &mut renderer, + &mut simple_clipboard, + &mut messages, + ); + + runtime.broadcast(redraw_event, Status::Ignored); + + ev_proxy.send_event(Event::SctkEvent( + IcedSctkEvent::RedrawRequested(object_id.clone()), + )); + sent_control_flow = true; + let _ = + control_sender + .start_send(match interface_state { + user_interface::State::Updated { + redraw_request: Some(redraw_request), + } => { + match redraw_request { + crate::core::window::RedrawRequest::NextFrame => { + ControlFlow::Poll + } + crate::core::window::RedrawRequest::At(at) => { + ControlFlow::WaitUntil(at) + } + }}, + _ => if needs_update { + ControlFlow::Poll + } else { + ControlFlow::Wait + }, + }); + } + if !sent_control_flow { + let mut wait_500_ms = Instant::now(); + wait_500_ms = wait_500_ms + Duration::from_millis(250); + _ = control_sender + .start_send(ControlFlow::WaitUntil(wait_500_ms)); + } + redraw_pending = false; + } + + sctk_events.clear(); + // clear the destroyed surfaces after they have been handled + destroyed_surface_ids.clear(); + } + IcedSctkEvent::RedrawRequested(object_id) => { + if let Some(( + native_id, + Some(mut user_interface), + Some(state), + )) = surface_ids.get(&object_id).and_then(|id| { + if matches!(id, SurfaceIdWrapper::Dnd(_)) { + return None; + } + let interface = interfaces.remove(&id.inner()); + let state = states.get_mut(&id.inner()); + Some((*id, interface, state)) + }) { + // request a new frame + // NOTE Ashley: this is done here only after a redraw for now instead of the event handler. + // Otherwise cpu goes up in the running application as well as in cosmic-comp + if let Some(surface) = state.frame.take() { + surface.frame(&queue_handle, surface.clone()); + surface.commit(); + } + + let Some(mut comp_surface) = state.surface.take() else { + error!("missing surface!"); + continue; + }; + + debug.render_started(); + #[cfg(feature = "a11y")] + if let Some(Some(adapter)) = a11y_enabled + .then(|| adapters.get_mut(&native_id.inner())) + { + use iced_accessibility::{ + accesskit::{Role, Tree, TreeUpdate}, + 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.to_string()); + let window_tree = A11yTree::node_with_child_tree( + A11yNode::new(root, adapter.id), + child_tree, + ); + let tree = Tree::new(NodeId(adapter.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))); + } + } + } + tracing::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::>() + ); + let focus = focus + .filter(|f_id| window_tree.contains(f_id)) + .map(|id| id.into()); + adapter.adapter.update(TreeUpdate { + nodes: window_tree.into(), + tree: Some(tree), + focus, + }); + } + + if state.viewport_changed() { + let physical_size = state.physical_size(); + let logical_size = state.logical_size(); + compositor.configure_surface( + &mut comp_surface, + physical_size.width, + physical_size.height, + ); + + debug.layout_started(); + user_interface = user_interface + .relayout(logical_size, &mut renderer); + debug.layout_finished(); + + state.viewport_changed = false; + } + debug.draw_started(); + let new_mouse_interaction = user_interface.draw( + &mut renderer, + state.theme(), + &Style { + text_color: state.text_color(), + }, + state.cursor(), + ); + debug.draw_finished(); + ev_proxy + .send_event(Event::SetCursor(new_mouse_interaction)); + interfaces.insert(native_id.inner(), user_interface); + + let _ = compositor.present( + &mut renderer, + &mut comp_surface, + state.viewport(), + state.background_color(), + &debug.overlay(), + ); + state.surface = Some(comp_surface); + debug.render_finished(); + } + } + IcedSctkEvent::RedrawEventsCleared => { + // TODO + } + IcedSctkEvent::LoopDestroyed => { + panic!("Loop destroyed"); + } + #[cfg(feature = "a11y")] + IcedSctkEvent::A11yEvent(ActionRequestEvent { + surface_id, + request, + }) => { + use iced_accessibility::accesskit::Action; + match request.action { + Action::Default => { + // TODO default operation? + // messages.push(focus(request.target.into())); + a11y_events.push(ActionRequestEvent { + surface_id, + request, + }); + } + Action::Focus => { + commands.push(Command::widget(focus( + iced_runtime::core::id::Id::from(u128::from( + request.target.0, + ) + as u64), + ))); + } + Action::Blur => todo!(), + Action::Collapse => todo!(), + Action::Expand => todo!(), + Action::CustomAction => todo!(), + Action::Decrement => todo!(), + Action::Increment => todo!(), + Action::HideTooltip => todo!(), + Action::ShowTooltip => todo!(), + Action::InvalidateTree => todo!(), + Action::LoadInlineTextBoxes => todo!(), + Action::ReplaceSelectedText => todo!(), + Action::ScrollBackward => todo!(), + Action::ScrollDown => todo!(), + Action::ScrollForward => todo!(), + Action::ScrollLeft => todo!(), + Action::ScrollRight => todo!(), + Action::ScrollUp => todo!(), + Action::ScrollIntoView => todo!(), + Action::ScrollToPoint => todo!(), + Action::SetScrollOffset => todo!(), + Action::SetTextSelection => todo!(), + Action::SetSequentialFocusNavigationStartingPoint => { + todo!() + } + Action::SetValue => todo!(), + Action::ShowContextMenu => todo!(), + } + } + #[cfg(feature = "a11y")] + IcedSctkEvent::A11yEnabled => { + a11y_enabled = true; + } + #[cfg(feature = "a11y")] + IcedSctkEvent::A11ySurfaceCreated(surface_id, adapter) => { + adapters.insert(surface_id.inner(), adapter); + } + IcedSctkEvent::Frame(surface) => { + if let Some(id) = surface_ids.get(&surface.id()) { + if let Some(state) = states.get_mut(&id.inner()) { + // TODO set this to the callback? + state.set_frame(Some(surface)); + } + } + } + } + } + + Ok(()) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum SurfaceIdWrapper { + LayerSurface(SurfaceId), + Window(SurfaceId), + Popup(SurfaceId), + Dnd(SurfaceId), + SessionLock(SurfaceId), +} + +impl SurfaceIdWrapper { + pub fn inner(&self) -> SurfaceId { + match self { + SurfaceIdWrapper::LayerSurface(id) => *id, + SurfaceIdWrapper::Window(id) => *id, + SurfaceIdWrapper::Popup(id) => *id, + SurfaceIdWrapper::Dnd(id) => *id, + SurfaceIdWrapper::SessionLock(id) => *id, + } + } +} + +/// Builds a [`UserInterface`] for the provided [`Application`], logging +/// [`struct@Debug`] information accordingly. +pub fn build_user_interface<'a, A: Application>( + application: &'a A, + cache: user_interface::Cache, + renderer: &mut A::Renderer, + size: Size, + _title: &str, + debug: &mut Debug, + id: SurfaceIdWrapper, + auto_size_surfaces: &mut HashMap< + SurfaceIdWrapper, + (u32, u32, Limits, bool), + >, + ev_proxy: &mut proxy::Proxy>, +) -> UserInterface<'a, A::Message, A::Theme, A::Renderer> +where + ::Theme: StyleSheet, +{ + debug.view_started(); + let mut view = application.view(id.inner()); + debug.view_finished(); + + let size = if let Some((auto_size_w, auto_size_h, limits, dirty)) = + auto_size_surfaces.remove(&id) + { + // TODO would it be ok to diff against the current cache? + let mut tree = Tree::new(view.as_widget_mut()); + let bounds = view + .as_widget() + .layout(&mut tree, renderer, &limits) + .bounds() + .size(); + // XXX add a small number to make sure it doesn't get truncated... + let (w, h) = ( + (bounds.width.round()) as u32, + (bounds.height.round()) as u32, + ); + let dirty = dirty + || w != size.width.round() as u32 + || h != size.height.round() as u32 + || w != auto_size_w + || h != auto_size_h; + + auto_size_surfaces.insert(id, (w, h, limits, dirty)); + if dirty { + match id { + SurfaceIdWrapper::LayerSurface(inner) => { + ev_proxy.send_event( + Event::LayerSurface( + command::platform_specific::wayland::layer_surface::Action::Size { id: inner, width: Some(w), height: Some(h) }, + ) + ); + } + SurfaceIdWrapper::Window(inner) => { + ev_proxy.send_event( + Event::Window( + command::platform_specific::wayland::window::Action::Size { id: inner, width: w, height: h }, + ) + ); + } + SurfaceIdWrapper::Popup(inner) => { + ev_proxy.send_event( + Event::Popup( + command::platform_specific::wayland::popup::Action::Size { id: inner, width: w, height: h }, + ) + ); + } + SurfaceIdWrapper::Dnd(_) => {} + SurfaceIdWrapper::SessionLock(_) => {} + }; + } + + Size::new(w as f32, h as f32) + } else { + size + }; + + debug.layout_started(); + let user_interface = UserInterface::build(view, size, cache, renderer); + debug.layout_finished(); + + user_interface +} + +/// The state of a surface created by the application [`Application`]. +#[allow(missing_debug_implementations)] +pub struct State +where + ::Theme: application::StyleSheet, +{ + pub(crate) id: SurfaceIdWrapper, + title: String, + application_scale_factor: f64, + surface_scale_factor: f64, + viewport: Viewport, + viewport_changed: bool, + cursor_position: Option>, + modifiers: Modifiers, + theme: ::Theme, + appearance: application::Appearance, + application: PhantomData, + frame: Option, + needs_redraw: bool, + first: bool, + wp_viewport: Option, + interface_state: user_interface::State, + surface: Option, + wrapper: SurfaceDisplayWrapper, +} + +impl State +where + ::Theme: application::StyleSheet, +{ + /// Creates a new [`State`] for the provided [`Application`] + pub fn new( + application: &A, + id: SurfaceIdWrapper, + wrapper: SurfaceDisplayWrapper, + ) -> Self { + let title = application.title(id.inner()); + let scale_factor = application.scale_factor(id.inner()); + let theme = application.theme(id.inner()); + let appearance = theme.appearance(&application.style()); + let viewport = Viewport::with_physical_size(Size::new(1, 1), 1.0); + + Self { + id, + title, + application_scale_factor: scale_factor, + surface_scale_factor: 1.0, // assumed to be 1.0 at first + viewport, + viewport_changed: true, + // TODO: Encode cursor availability in the type-system + cursor_position: None, + modifiers: Modifiers::default(), + theme, + appearance, + application: PhantomData, + frame: None, + needs_redraw: false, + first: true, + wp_viewport: None, + interface_state: user_interface::State::Outdated, + surface: None, + wrapper, + } + } + + pub(crate) fn set_needs_redraw(&mut self, needs_redraw: bool) { + self.needs_redraw = needs_redraw; + } + + pub(crate) fn needs_redraw(&self) -> bool { + self.needs_redraw + } + + pub(crate) fn set_frame(&mut self, frame: Option) { + self.frame = frame; + } + + pub(crate) fn frame(&self) -> Option<&WlSurface> { + self.frame.as_ref() + } + + pub(crate) fn first(&self) -> bool { + self.first + } + + pub(crate) fn set_first(&mut self, first: bool) { + self.first = first; + } + + /// Returns the current [`Viewport`] of the [`State`]. + pub fn viewport(&self) -> &Viewport { + &self.viewport + } + + /// Returns the current title of the [`State`]. + pub fn title(&self) -> &str { + &self.title + } + + /// TODO + pub fn viewport_changed(&self) -> bool { + self.viewport_changed + } + + /// Returns the physical [`Size`] of the [`Viewport`] of the [`State`]. + pub fn physical_size(&self) -> Size { + self.viewport.physical_size() + } + + /// Returns the logical [`Size`] of the [`Viewport`] of the [`State`]. + pub fn logical_size(&self) -> Size { + self.viewport.logical_size() + } + + /// Sets the logical [`Size`] of the [`Viewport`] of the [`State`]. + pub fn set_logical_size(&mut self, w: f64, h: f64) { + let old_size = self.viewport.logical_size(); + if !approx_eq!(f32, w as f32, old_size.width, F32Margin::default()) + || !approx_eq!(f32, h as f32, old_size.height, F32Margin::default()) + { + let logical_size = LogicalSize::::new(w, h); + let physical_size: PhysicalSize = + logical_size.to_physical(self.scale_factor()); + self.viewport_changed = true; + self.viewport = Viewport::with_physical_size( + Size { + width: physical_size.width, + height: physical_size.height, + }, + self.scale_factor(), + ); + if let Some(wp_viewport) = self.wp_viewport.as_ref() { + wp_viewport.set_destination( + logical_size.width.round() as i32, + logical_size.height.round() as i32, + ); + } + } + } + + /// Returns the current scale factor of the [`Viewport`] of the [`State`]. + pub fn scale_factor(&self) -> f64 { + self.viewport.scale_factor() + } + + pub fn set_scale_factor(&mut self, scale_factor: f64) { + if !approx_eq!( + f64, + scale_factor, + self.surface_scale_factor, + F64Margin::default() + ) { + self.viewport_changed = true; + let logical_size = self.viewport.logical_size(); + let logical_size = LogicalSize::::new( + logical_size.width as f64, + logical_size.height as f64, + ); + self.surface_scale_factor = scale_factor; + let physical_size: PhysicalSize = logical_size.to_physical( + self.application_scale_factor * self.surface_scale_factor, + ); + self.viewport = Viewport::with_physical_size( + Size { + width: physical_size.width, + height: physical_size.height, + }, + self.application_scale_factor * self.surface_scale_factor, + ); + if let Some(wp_viewport) = self.wp_viewport.as_ref() { + wp_viewport.set_destination( + logical_size.width.round() as i32, + logical_size.height.round() as i32, + ); + } + } + } + + // TODO use a type to encode cursor availability + /// Returns the current cursor position of the [`State`]. + pub fn cursor(&self) -> mouse::Cursor { + self.cursor_position + .map(|cursor_position| { + let scale_factor = self.application_scale_factor; + assert!( + scale_factor.is_sign_positive() && scale_factor.is_normal() + ); + let logical: LogicalPosition = + cursor_position.to_logical(scale_factor); + + Point { + x: logical.x as f32, + y: logical.y as f32, + } + }) + .map(mouse::Cursor::Available) + .unwrap_or(mouse::Cursor::Unavailable) + } + + /// Returns the current keyboard modifiers of the [`State`]. + pub fn modifiers(&self) -> Modifiers { + self.modifiers + } + + /// Returns the current theme of the [`State`]. + pub fn theme(&self) -> &::Theme { + &self.theme + } + + /// Returns the current background [`Color`] of the [`State`]. + pub fn background_color(&self) -> Color { + self.appearance.background_color + } + + /// Returns the current text [`Color`] of the [`State`]. + pub fn text_color(&self) -> Color { + self.appearance.text_color + } + + pub fn set_cursor_position(&mut self, p: Option>) { + self.cursor_position = + p.map(|p| p.to_physical(self.application_scale_factor)); + } + + fn synchronize(&mut self, application: &A) { + // Update theme and appearance + self.theme = application.theme(self.id.inner()); + self.appearance = self.theme.appearance(&application.style()); + } +} + +// XXX Ashley careful, A, E, C must be exact same as in run_instance, or the subscription map type will have a different hash +/// Updates an [`Application`] by feeding it the provided messages, spawning any +/// resulting [`Command`], and tracking its [`Subscription`] +pub(crate) fn update( + application: &mut A, + cache: &mut user_interface::Cache, + state: Option<&State>, + renderer: &mut A::Renderer, + runtime: MyRuntime, + proxy: &mut proxy::Proxy>, + debug: &mut Debug, + messages: &mut Vec, + actions: &mut Vec>, + graphics_info: impl FnOnce() -> compositor::Information + Copy, + auto_size_surfaces: &mut HashMap< + SurfaceIdWrapper, + (u32, u32, Limits, bool), + >, + clipboard: &mut Clipboard, +) where + A: Application + 'static, + E: Executor + 'static, + C: iced_graphics::Compositor + 'static, + ::Theme: StyleSheet, +{ + let actions_ = std::mem::take(actions); + for a in actions_ { + if let Some(a) = handle_actions( + application, + cache, + state, + renderer, + a, + runtime, + proxy, + debug, + graphics_info, + auto_size_surfaces, + clipboard, + ) { + actions.push(a); + } + } + for message in messages.drain(..) { + debug.log_message(&message); + + debug.update_started(); + let command = runtime.enter(|| application.update(message)); + debug.update_finished(); + + run_command( + application, + cache, + state, + renderer, + command, + runtime, + proxy, + debug, + graphics_info, + auto_size_surfaces, + actions, + clipboard, + ) + } + + runtime.track( + application + .subscription() + .map(subscription_map::) + .into_recipes(), + ); +} + +type MyRuntime<'a, E, M> = &'a mut Runtime>, Event>; + +/// Runs the actions of a [`Command`]. +fn run_command( + application: &A, + cache: &mut user_interface::Cache, + state: Option<&State>, + renderer: &mut A::Renderer, + command: Command, + runtime: MyRuntime, + proxy: &mut proxy::Proxy>, + debug: &mut Debug, + graphics_info: impl FnOnce() -> compositor::Information + Copy, + auto_size_surfaces: &mut HashMap< + SurfaceIdWrapper, + (u32, u32, Limits, bool), + >, + actions: &mut Vec>, + clipboard: &mut Clipboard, +) where + A: Application, + E: Executor, + ::Theme: StyleSheet, + C: Compositor, +{ + for action in command.actions() { + if let Some(a) = handle_actions( + application, + cache, + state, + renderer, + action, + runtime, + proxy, + debug, + graphics_info, + auto_size_surfaces, + clipboard, + ) { + actions.push(a); + } + } +} + +fn handle_actions( + application: &A, + cache: &mut user_interface::Cache, + state: Option<&State>, + renderer: &mut A::Renderer, + action: command::Action, + runtime: MyRuntime, + proxy: &mut proxy::Proxy>, + debug: &mut Debug, + _graphics_info: impl FnOnce() -> compositor::Information + Copy, + auto_size_surfaces: &mut HashMap< + SurfaceIdWrapper, + (u32, u32, Limits, bool), + >, + clipboard: &mut Clipboard, +) -> Option> +where + A: Application, + E: Executor, + ::Theme: StyleSheet, + C: Compositor, +{ + match action { + command::Action::Future(future) => { + runtime + .spawn(Box::pin(future.map(|e| { + Event::SctkEvent(IcedSctkEvent::UserEvent(e)) + }))); + } + command::Action::Clipboard(action) => match action { + clipboard::Action::Read(s_to_msg) => { + if matches!(clipboard.state, crate::clipboard::State::Connected(_)) { + let contents = clipboard.read(); + let message = s_to_msg(contents); + proxy.send_event(Event::Message(message)); + } + } + clipboard::Action::Write(contents) => { + if matches!(clipboard.state, crate::clipboard::State::Connected(_)) { + clipboard.write(contents) + } + } + }, + command::Action::Window(..) => { + unimplemented!("Use platform specific events instead") + } + command::Action::System(action) => match action { + system::Action::QueryInformation(_tag) => { + #[cfg(feature = "system")] + { + let graphics_info = _graphics_info(); + let proxy = proxy.clone(); + + let _ = std::thread::spawn(move || { + let information = + crate::system::information(graphics_info); + + let message = _tag(information); + + proxy + .send_event(Event::Message(message)); + }); + } + } + }, + command::Action::Widget(action) => { + let state = match state { + Some(s) => s, + None => return None, + }; + let id = &state.id; + let mut current_cache = std::mem::take(cache); + let mut current_operation = Some(Box::new(OperationWrapper::Message(action))); + + + let mut user_interface = build_user_interface( + application, + current_cache, + renderer, + state.logical_size(), + &state.title, + debug, + id.clone(), // TODO: run the operation on every widget tree ? + auto_size_surfaces, + proxy + ); + let mut ret = None; + + while let Some(mut operation) = current_operation.take() { + user_interface.operate(renderer, operation.as_mut()); + + match operation.as_ref().finish() { + operation::Outcome::None => { + ret = Some(operation); + } + operation::Outcome::Some(message) => { + match message { + operation::OperationOutputWrapper::Message(m) => { + proxy.send_event(Event::SctkEvent( + IcedSctkEvent::UserEvent(m), + )); + ret = Some(operation) + }, + operation::OperationOutputWrapper::Id(_) => { + // should not happen + }, + } + } + operation::Outcome::Chain(next) => { + current_operation = Some(Box::new(OperationWrapper::Wrapper(next))); + } + } + } + + current_cache = user_interface.into_cache(); + *cache = current_cache; + return ret.and_then(|o| match *o { + OperationWrapper::Message(o) => Some(command::Action::Widget(o)), + _ => None + }); + } + command::Action::PlatformSpecific( + platform_specific::Action::Wayland( + platform_specific::wayland::Action::LayerSurface( + layer_surface_action, + ), + ), + ) => { + if let platform_specific::wayland::layer_surface::Action::LayerSurface{ mut builder, _phantom } = layer_surface_action { + if builder.size.is_none() { + let e = application.view(builder.id); + let mut tree = Tree::new(e.as_widget()); + let node = Widget::layout(e.as_widget(), &mut tree, renderer, &builder.size_limits); + let bounds = node.bounds(); + let (w, h) = ((bounds.width.round()) as u32, (bounds.height.round()) as u32); + auto_size_surfaces.insert(SurfaceIdWrapper::LayerSurface(builder.id), (w, h, builder.size_limits, false)); + builder.size = Some((Some(bounds.width as u32), Some(bounds.height as u32))); + } + proxy.send_event(Event::LayerSurface(platform_specific::wayland::layer_surface::Action::LayerSurface {builder, _phantom})); + } else { + proxy.send_event(Event::LayerSurface(layer_surface_action)); + } + } + command::Action::PlatformSpecific( + platform_specific::Action::Wayland( + platform_specific::wayland::Action::Window(window_action), + ), + ) => { + if let platform_specific::wayland::window::Action::Window{ mut builder, _phantom } = window_action { + if builder.autosize { + let e = application.view(builder.window_id); + let mut tree = Tree::new(e.as_widget()); + let node = Widget::layout(e.as_widget(), &mut tree, renderer, &builder.size_limits); + let bounds = node.bounds(); + let (w, h) = ((bounds.width.round()) as u32, (bounds.height.round()) as u32); + auto_size_surfaces.insert(SurfaceIdWrapper::Window(builder.window_id), (w, h, builder.size_limits, false)); + builder.size = (bounds.width as u32, bounds.height as u32); + } + proxy.send_event(Event::Window(platform_specific::wayland::window::Action::Window{builder, _phantom})); + } else { + proxy.send_event(Event::Window(window_action)); + } + } + command::Action::PlatformSpecific( + platform_specific::Action::Wayland( + platform_specific::wayland::Action::Popup(popup_action), + ), + ) => { + if let popup::Action::Popup { mut popup, _phantom } = popup_action { + if popup.positioner.size.is_none() { + let e = application.view(popup.id); + let mut tree = Tree::new(e.as_widget()); + let node = Widget::layout( e.as_widget(), &mut tree, renderer, &popup.positioner.size_limits); + let bounds = node.bounds(); + let (w, h) = ((bounds.width.round()) as u32, (bounds.height.round()) as u32); + auto_size_surfaces.insert(SurfaceIdWrapper::Popup(popup.id), (w, h, popup.positioner.size_limits, false)); + popup.positioner.size = Some((w, h)); + } + proxy.send_event(Event::Popup(popup::Action::Popup{popup, _phantom})); + } else { + proxy.send_event(Event::Popup(popup_action)); + } + } + command::Action::PlatformSpecific(platform_specific::Action::Wayland(platform_specific::wayland::Action::DataDevice(data_device_action))) => { + proxy.send_event(Event::DataDevice(data_device_action)); + } + command::Action::PlatformSpecific( + platform_specific::Action::Wayland( + platform_specific::wayland::Action::Activation(activation_action) + ) + ) => { + proxy.send_event(Event::Activation(activation_action)); + } + command::Action::PlatformSpecific(platform_specific::Action::Wayland(platform_specific::wayland::Action::SessionLock(session_lock_action))) => { + proxy.send_event(Event::SessionLock(session_lock_action)); + } + _ => {} + }; + None +} +pub fn build_user_interfaces<'a, A, C>( + application: &'a A, + renderer: &mut A::Renderer, + debug: &mut Debug, + states: &HashMap>, + mut pure_states: HashMap, + auto_size_surfaces: &mut HashMap< + SurfaceIdWrapper, + (u32, u32, Limits, bool), + >, + ev_proxy: &mut proxy::Proxy>, +) -> HashMap< + SurfaceId, + UserInterface< + 'a, + ::Message, + ::Theme, + ::Renderer, + >, +> +where + A: Application + 'static, + ::Theme: StyleSheet, + C: Compositor, +{ + let mut interfaces = HashMap::new(); + + // TODO ASHLEY make sure Ids are iterated in the same order every time for a11y + for (id, pure_state) in pure_states.drain().sorted_by(|a, b| a.0.cmp(&b.0)) + { + let state = &states.get(&id).unwrap(); + + let user_interface = build_user_interface( + application, + pure_state, + renderer, + state.logical_size(), + &state.title, + debug, + state.id, + auto_size_surfaces, + ev_proxy, + ); + + let _ = interfaces.insert(id, user_interface); + } + + interfaces +} + +// Determine if `SctkEvent` is for surface with given object id. +fn event_is_for_surface( + evt: &SctkEvent, + object_id: &ObjectId, + has_kbd_focus: bool, +) -> bool { + match evt { + SctkEvent::SeatEvent { id, .. } => &id.id() == object_id, + SctkEvent::PointerEvent { variant, .. } => { + &variant.surface.id() == object_id + } + SctkEvent::KeyboardEvent { variant, .. } => match variant { + KeyboardEventVariant::Leave(id) => &id.id() == object_id, + _ => has_kbd_focus, + }, + SctkEvent::WindowEvent { id, .. } => &id.id() == object_id, + SctkEvent::LayerSurfaceEvent { id, .. } => &id.id() == object_id, + SctkEvent::PopupEvent { id, .. } => &id.id() == object_id, + SctkEvent::NewOutput { .. } + | SctkEvent::UpdateOutput { .. } + | SctkEvent::RemovedOutput(_) => false, + SctkEvent::ScaleFactorChanged { id, .. } => &id.id() == object_id, + SctkEvent::DndOffer { surface, .. } => &surface.id() == object_id, + SctkEvent::DataSource(_) => true, + SctkEvent::SessionLocked => false, + SctkEvent::SessionLockFinished => false, + SctkEvent::SessionLockSurfaceCreated { surface, .. } => { + &surface.id() == object_id + } + SctkEvent::SessionLockSurfaceConfigure { surface, .. } => { + &surface.id() == object_id + } + SctkEvent::SessionUnlocked => false, + } +} diff --git a/sctk/src/clipboard.rs b/sctk/src/clipboard.rs new file mode 100644 index 0000000000..74ab0c6c94 --- /dev/null +++ b/sctk/src/clipboard.rs @@ -0,0 +1,81 @@ +//! Access the clipboard. +pub use iced_runtime::clipboard::Action; + +use iced_runtime::command::{self, Command}; +use std::ffi::c_void; +use std::sync::{Arc, Mutex}; + +/// A buffer for short-term storage and transfer within and between +/// applications. +#[allow(missing_debug_implementations)] +pub struct Clipboard { + pub(crate) state: State, +} + +pub(crate) enum State { + Connected(Arc>), + Unavailable, +} + +impl Clipboard { + pub unsafe fn connect(display: *mut c_void) -> Clipboard { + let context = Arc::new(Mutex::new(smithay_clipboard::Clipboard::new( + display as *mut _, + ))); + + Clipboard { + state: State::Connected(context), + } + } + + /// Creates a new [`Clipboard`] that isn't associated with a window. + /// This clipboard will never contain a copied value. + pub fn unconnected() -> Clipboard { + Clipboard { + state: State::Unavailable, + } + } + + /// Reads the current content of the [`Clipboard`] as text. + pub fn read(&self) -> Option { + match &self.state { + State::Connected(clipboard) => { + let clipboard = clipboard.lock().unwrap(); + clipboard.load().ok() + } + State::Unavailable => None, + } + } + + /// Writes the given text contents to the [`Clipboard`]. + pub fn write(&mut self, contents: String) { + match &mut self.state { + State::Connected(clipboard) => { + clipboard.lock().unwrap().store(contents) + } + State::Unavailable => {} + } + } +} + +impl iced_runtime::core::clipboard::Clipboard for Clipboard { + fn read(&self) -> Option { + self.read() + } + + fn write(&mut self, contents: String) { + self.write(contents) + } +} + +/// Read the current contents of the clipboard. +pub fn read( + f: impl Fn(Option) -> Message + 'static, +) -> Command { + Command::single(command::Action::Clipboard(Action::Read(Box::new(f)))) +} + +/// Write the given contents to the clipboard. +pub fn write(contents: String) -> Command { + Command::single(command::Action::Clipboard(Action::Write(contents))) +} diff --git a/sctk/src/commands/activation.rs b/sctk/src/commands/activation.rs new file mode 100644 index 0000000000..0efe1236cf --- /dev/null +++ b/sctk/src/commands/activation.rs @@ -0,0 +1,30 @@ +use iced_runtime::command::Command; +use iced_runtime::command::{ + self, + platform_specific::{self, wayland}, +}; +use iced_runtime::window::Id as SurfaceId; + +pub fn request_token( + app_id: Option, + window: Option, + to_message: impl FnOnce(Option) -> Message + Send + Sync + 'static, +) -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::Activation( + wayland::activation::Action::RequestToken { + app_id, + window, + message: Box::new(to_message), + }, + )), + )) +} + +pub fn activate(window: SurfaceId, token: String) -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::Activation( + wayland::activation::Action::Activate { window, token }, + )), + )) +} diff --git a/sctk/src/commands/data_device.rs b/sctk/src/commands/data_device.rs new file mode 100644 index 0000000000..b009dca473 --- /dev/null +++ b/sctk/src/commands/data_device.rs @@ -0,0 +1,119 @@ +//! Interact with the data device objects of your application. + +use iced_runtime::{ + command::{ + self, + platform_specific::{ + self, + wayland::{ + self, + data_device::{ActionInner, DataFromMimeType, DndIcon}, + }, + }, + }, + window, Command, +}; +use sctk::reexports::client::protocol::wl_data_device_manager::DndAction; + +/// start an internal drag and drop operation. Events will only be delivered to the same client. +/// The client is responsible for data transfer. +pub fn start_internal_drag( + origin_id: window::Id, + icon_id: Option, +) -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::DataDevice( + wayland::data_device::ActionInner::StartInternalDnd { + origin_id, + icon_id, + } + .into(), + )), + )) +} + +/// Start a drag and drop operation. When a client asks for the selection, an event will be delivered +/// to the client with the fd to write the data to. +pub fn start_drag( + mime_types: Vec, + actions: DndAction, + origin_id: window::Id, + icon_id: Option, + data: Box, +) -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::DataDevice( + wayland::data_device::ActionInner::StartDnd { + mime_types, + actions, + origin_id, + icon_id, + data, + } + .into(), + )), + )) +} + +/// Set accepted and preferred drag and drop actions. +pub fn set_actions( + preferred: DndAction, + accepted: DndAction, +) -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::DataDevice( + wayland::data_device::ActionInner::SetActions { + preferred, + accepted, + } + .into(), + )), + )) +} + +/// Accept a mime type or None to reject the drag and drop operation. +pub fn accept_mime_type( + mime_type: Option, +) -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::DataDevice( + wayland::data_device::ActionInner::Accept(mime_type).into(), + )), + )) +} + +/// Read drag and drop data. This will trigger an event with the data. +pub fn request_dnd_data(mime_type: String) -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::DataDevice( + wayland::data_device::ActionInner::RequestDndData(mime_type).into(), + )), + )) +} + +/// Finished the drag and drop operation. +pub fn finish_dnd() -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::DataDevice( + wayland::data_device::ActionInner::DndFinished.into(), + )), + )) +} + +/// Cancel the drag and drop operation. +pub fn cancel_dnd() -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::DataDevice( + wayland::data_device::ActionInner::DndCancelled.into(), + )), + )) +} + +/// Run a generic drag action +pub fn action(action: ActionInner) -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::DataDevice( + action.into(), + )), + )) +} diff --git a/sctk/src/commands/layer_surface.rs b/sctk/src/commands/layer_surface.rs new file mode 100644 index 0000000000..b8846adeae --- /dev/null +++ b/sctk/src/commands/layer_surface.rs @@ -0,0 +1,123 @@ +//! Interact with the window of your application. +use std::marker::PhantomData; + +use iced_runtime::command::{ + self, + platform_specific::{ + self, + wayland::{ + self, + layer_surface::{IcedMargin, SctkLayerSurfaceSettings}, + }, + }, + Command, +}; +use iced_runtime::window::Id as SurfaceId; + +pub use sctk::shell::wlr_layer::{Anchor, KeyboardInteractivity, Layer}; + +// TODO ASHLEY: maybe implement as builder that outputs a batched commands +/// +pub fn get_layer_surface( + builder: SctkLayerSurfaceSettings, +) -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::LayerSurface( + wayland::layer_surface::Action::LayerSurface { + builder, + _phantom: PhantomData::default(), + }, + )), + )) +} + +/// +pub fn destroy_layer_surface(id: SurfaceId) -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::LayerSurface( + wayland::layer_surface::Action::Destroy(id), + )), + )) +} + +/// +pub fn set_size( + id: SurfaceId, + width: Option, + height: Option, +) -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::LayerSurface( + wayland::layer_surface::Action::Size { id, width, height }, + )), + )) +} +/// +pub fn set_anchor(id: SurfaceId, anchor: Anchor) -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::LayerSurface( + wayland::layer_surface::Action::Anchor { id, anchor }, + )), + )) +} +/// +pub fn set_exclusive_zone( + id: SurfaceId, + zone: i32, +) -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::LayerSurface( + wayland::layer_surface::Action::ExclusiveZone { + id, + exclusive_zone: zone, + }, + )), + )) +} + +/// +pub fn set_margin( + id: SurfaceId, + top: i32, + right: i32, + bottom: i32, + left: i32, +) -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::LayerSurface( + wayland::layer_surface::Action::Margin { + id, + margin: IcedMargin { + top, + right, + bottom, + left, + }, + }, + )), + )) +} + +/// +pub fn set_keyboard_interactivity( + id: SurfaceId, + keyboard_interactivity: KeyboardInteractivity, +) -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::LayerSurface( + wayland::layer_surface::Action::KeyboardInteractivity { + id, + keyboard_interactivity, + }, + )), + )) +} + +/// +pub fn set_layer(id: SurfaceId, layer: Layer) -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::LayerSurface( + wayland::layer_surface::Action::Layer { id, layer }, + )), + )) +} diff --git a/sctk/src/commands/mod.rs b/sctk/src/commands/mod.rs new file mode 100644 index 0000000000..c7866914db --- /dev/null +++ b/sctk/src/commands/mod.rs @@ -0,0 +1,8 @@ +//! Interact with the wayland objects of your application. + +pub mod activation; +pub mod data_device; +pub mod layer_surface; +pub mod popup; +pub mod session_lock; +pub mod window; diff --git a/sctk/src/commands/popup.rs b/sctk/src/commands/popup.rs new file mode 100644 index 0000000000..fc74ba1a2d --- /dev/null +++ b/sctk/src/commands/popup.rs @@ -0,0 +1,54 @@ +//! Interact with the popups of your application. +use iced_runtime::command::Command; +use iced_runtime::command::{ + self, + platform_specific::{ + self, + wayland::{self, popup::SctkPopupSettings}, + }, +}; +use iced_runtime::window::Id as SurfaceId; + +/// +/// +pub fn get_popup(popup: SctkPopupSettings) -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::Popup( + wayland::popup::Action::Popup { + popup, + _phantom: Default::default(), + }, + )), + )) +} + +/// +pub fn set_size( + id: SurfaceId, + width: u32, + height: u32, +) -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::Popup( + wayland::popup::Action::Size { id, width, height }, + )), + )) +} + +// https://wayland.app/protocols/xdg-shell#xdg_popup:request:grab +pub fn grab_popup(id: SurfaceId) -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::Popup( + wayland::popup::Action::Grab { id }, + )), + )) +} + +/// +pub fn destroy_popup(id: SurfaceId) -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::Popup( + wayland::popup::Action::Destroy { id }, + )), + )) +} diff --git a/sctk/src/commands/session_lock.rs b/sctk/src/commands/session_lock.rs new file mode 100644 index 0000000000..007379efd6 --- /dev/null +++ b/sctk/src/commands/session_lock.rs @@ -0,0 +1,48 @@ +use iced_runtime::command::Command; +use iced_runtime::command::{ + self, + platform_specific::{self, wayland}, +}; +use iced_runtime::window::Id as SurfaceId; +use sctk::reexports::client::protocol::wl_output::WlOutput; + +use std::marker::PhantomData; + +pub fn lock() -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::SessionLock( + wayland::session_lock::Action::Lock, + )), + )) +} + +pub fn unlock() -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::SessionLock( + wayland::session_lock::Action::Unlock, + )), + )) +} + +pub fn get_lock_surface( + id: SurfaceId, + output: WlOutput, +) -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::SessionLock( + wayland::session_lock::Action::LockSurface { + id, + output, + _phantom: PhantomData, + }, + )), + )) +} + +pub fn destroy_lock_surface(id: SurfaceId) -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::SessionLock( + wayland::session_lock::Action::DestroyLockSurface { id }, + )), + )) +} diff --git a/sctk/src/commands/window.rs b/sctk/src/commands/window.rs new file mode 100644 index 0000000000..bf9923b4fe --- /dev/null +++ b/sctk/src/commands/window.rs @@ -0,0 +1,87 @@ +//! Interact with the window of your application. +use std::marker::PhantomData; + +use iced_runtime::{ + command::{ + self, + platform_specific::{ + self, + wayland::{self, window::SctkWindowSettings}, + }, + }, + core::window::Mode, + window, Command, +}; + +pub fn get_window(builder: SctkWindowSettings) -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::Window( + wayland::window::Action::Window { + builder, + _phantom: PhantomData::default(), + }, + )), + )) +} + +// TODO Ashley refactor to use regular window events maybe... +/// close the window +pub fn close_window(id: window::Id) -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::Window( + wayland::window::Action::Destroy(id), + )), + )) +} + +/// Resizes the window to the given logical dimensions. +pub fn resize_window( + id: window::Id, + width: u32, + height: u32, +) -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::Window( + wayland::window::Action::Size { id, width, height }, + )), + )) +} + +pub fn start_drag_window(id: window::Id) -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::Window( + wayland::window::Action::InteractiveMove { id }, + )), + )) +} + +pub fn toggle_maximize(id: window::Id) -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::Window( + wayland::window::Action::ToggleMaximized { id }, + )), + )) +} + +pub fn set_app_id_window( + id: window::Id, + app_id: String, +) -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::Window( + wayland::window::Action::AppId { id, app_id }, + )), + )) +} + +/// Sets the [`Mode`] of the window. +pub fn set_mode_window( + id: window::Id, + mode: Mode, +) -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::Window( + wayland::window::Action::Mode(id, mode), + )), + )) +} diff --git a/sctk/src/conversion.rs b/sctk/src/conversion.rs new file mode 100644 index 0000000000..10564e7fed --- /dev/null +++ b/sctk/src/conversion.rs @@ -0,0 +1,89 @@ +use iced_futures::core::mouse::Interaction; +use iced_runtime::core::{ + keyboard, + mouse::{self, ScrollDelta}, +}; +use sctk::{ + reexports::client::protocol::wl_pointer::AxisSource, + seat::{ + keyboard::Modifiers, + pointer::{AxisScroll, CursorIcon, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT}, + }, +}; + +/// An error that occurred while running an application. +#[derive(Debug, thiserror::Error)] +#[error("the futures executor could not be created")] +pub struct KeyCodeError(u32); + +pub fn pointer_button_to_native(button: u32) -> Option { + if button == BTN_LEFT { + Some(mouse::Button::Left) + } else if button == BTN_RIGHT { + Some(mouse::Button::Right) + } else if button == BTN_MIDDLE { + Some(mouse::Button::Middle) + } else { + button.try_into().ok().map(mouse::Button::Other) + } +} + +pub fn pointer_axis_to_native( + source: Option, + horizontal: AxisScroll, + vertical: AxisScroll, +) -> Option { + source.map(|source| match source { + AxisSource::Wheel | AxisSource::WheelTilt => ScrollDelta::Lines { + x: -1. * horizontal.discrete as f32, + y: -1. * vertical.discrete as f32, + }, + _ => ScrollDelta::Pixels { + x: -1. * horizontal.absolute as f32, + y: -1. * vertical.absolute as f32, + }, + }) +} + +pub fn modifiers_to_native(mods: Modifiers) -> keyboard::Modifiers { + let mut native_mods = keyboard::Modifiers::empty(); + if mods.alt { + native_mods = native_mods.union(keyboard::Modifiers::ALT); + } + if mods.ctrl { + native_mods = native_mods.union(keyboard::Modifiers::CTRL); + } + if mods.logo { + native_mods = native_mods.union(keyboard::Modifiers::LOGO); + } + if mods.shift { + native_mods = native_mods.union(keyboard::Modifiers::SHIFT); + } + // TODO Ashley: missing modifiers as platform specific additions? + // if mods.caps_lock { + // native_mods = native_mods.union(keyboard::Modifier); + // } + // if mods.num_lock { + // native_mods = native_mods.union(keyboard::Modifiers::); + // } + native_mods +} + +// pub fn keysym_to_vkey(keysym: RawKeysym) -> Option { +// key_conversion.get(&keysym).cloned() +// } + +pub(crate) fn cursor_icon(cursor: Interaction) -> CursorIcon { + match cursor { + Interaction::Idle => CursorIcon::Default, + Interaction::Pointer => CursorIcon::Pointer, + Interaction::Grab => CursorIcon::Grab, + Interaction::Text => CursorIcon::Text, + Interaction::Crosshair => CursorIcon::Crosshair, + Interaction::Working => CursorIcon::Progress, + Interaction::Grabbing => CursorIcon::Grabbing, + Interaction::ResizingHorizontally => CursorIcon::EwResize, + Interaction::ResizingVertically => CursorIcon::NsResize, + Interaction::NotAllowed => CursorIcon::NotAllowed, + } +} diff --git a/sctk/src/dpi.rs b/sctk/src/dpi.rs new file mode 100644 index 0000000000..afef5a3b0a --- /dev/null +++ b/sctk/src/dpi.rs @@ -0,0 +1,613 @@ +//! UI scaling is important, so read the docs for this module if you don't want to be confused. +//! +//! ## Why should I care about UI scaling? +//! +//! Modern computer screens don't have a consistent relationship between resolution and size. +//! 1920x1080 is a common resolution for both desktop and mobile screens, despite mobile screens +//! normally being less than a quarter the size of their desktop counterparts. What's more, neither +//! desktop nor mobile screens are consistent resolutions within their own size classes - common +//! mobile screens range from below 720p to above 1440p, and desktop screens range from 720p to 5K +//! and beyond. +//! +//! Given that, it's a mistake to assume that 2D content will only be displayed on screens with +//! a consistent pixel density. If you were to render a 96-pixel-square image on a 1080p screen, +//! then render the same image on a similarly-sized 4K screen, the 4K rendition would only take up +//! about a quarter of the physical space as it did on the 1080p screen. That issue is especially +//! problematic with text rendering, where quarter-sized text becomes a significant legibility +//! problem. +//! +//! Failure to account for the scale factor can create a significantly degraded user experience. +//! Most notably, it can make users feel like they have bad eyesight, which will potentially cause +//! them to think about growing elderly, resulting in them having an existential crisis. Once users +//! enter that state, they will no longer be focused on your application. +//! +//! ## How should I handle it? +//! +//! The solution to this problem is to account for the device's *scale factor*. The scale factor is +//! the factor UI elements should be scaled by to be consistent with the rest of the user's system - +//! for example, a button that's normally 50 pixels across would be 100 pixels across on a device +//! with a scale factor of `2.0`, or 75 pixels across with a scale factor of `1.5`. +//! +//! Many UI systems, such as CSS, expose DPI-dependent units like [points] or [picas]. That's +//! usually a mistake, since there's no consistent mapping between the scale factor and the screen's +//! actual DPI. Unless you're printing to a physical medium, you should work in scaled pixels rather +//! than any DPI-dependent units. +//! +//! ### Position and Size types +//! +//! Winit's [`PhysicalPosition`] / [`PhysicalSize`] types correspond with the actual pixels on the +//! device, and the [`LogicalPosition`] / [`LogicalSize`] types correspond to the physical pixels +//! divided by the scale factor. +//! All of Winit's functions return physical types, but can take either logical or physical +//! coordinates as input, allowing you to use the most convenient coordinate system for your +//! particular application. +//! +//! Winit's position and size types types are generic over their exact pixel type, `P`, to allow the +//! API to have integer precision where appropriate (e.g. most window manipulation functions) and +//! floating precision when necessary (e.g. logical sizes for fractional scale factors and touch +//! input). If `P` is a floating-point type, please do not cast the values with `as {int}`. Doing so +//! will truncate the fractional part of the float, rather than properly round to the nearest +//! integer. Use the provided `cast` function or [`From`]/[`Into`] conversions, which handle the +//! rounding properly. Note that precision loss will still occur when rounding from a float to an +//! int, although rounding lessens the problem. +//! +//! ### Events +//! +//! Winit will dispatch a [`ScaleFactorChanged`] event whenever a window's scale factor has changed. +//! This can happen if the user drags their window from a standard-resolution monitor to a high-DPI +//! monitor, or if the user changes their DPI settings. This gives you a chance to rescale your +//! application's UI elements and adjust how the platform changes the window's size to reflect the new +//! scale factor. If a window hasn't received a [`ScaleFactorChanged`] event, then its scale factor +//! can be found by calling [`window.scale_factor()`]. +//! +//! ## How is the scale factor calculated? +//! +//! Scale factor is calculated differently on different platforms: +//! +//! - **Windows:** On Windows 8 and 10, per-monitor scaling is readily configured by users from the +//! display settings. While users are free to select any option they want, they're only given a +//! selection of "nice" scale factors, i.e. 1.0, 1.25, 1.5... on Windows 7, the scale factor is +//! global and changing it requires logging out. See [this article][windows_1] for technical +//! details. +//! - **macOS:** Recent versions of macOS allow the user to change the scaling factor for certain +//! displays. When this is available, the user may pick a per-monitor scaling factor from a set +//! of pre-defined settings. All "retina displays" have a scaling factor above 1.0 by default but +//! the specific value varies across devices. +//! - **X11:** Many man-hours have been spent trying to figure out how to handle DPI in X11. Winit +//! currently uses a three-pronged approach: +//! + Use the value in the `WINIT_X11_SCALE_FACTOR` environment variable, if present. +//! + If not present, use the value set in `Xft.dpi` in Xresources. +//! + Otherwise, calculate the scale factor based on the millimeter monitor dimensions provided by XRandR. +//! +//! If `WINIT_X11_SCALE_FACTOR` is set to `randr`, it'll ignore the `Xft.dpi` field and use the +//! XRandR scaling method. Generally speaking, you should try to configure the standard system +//! variables to do what you want before resorting to `WINIT_X11_SCALE_FACTOR`. +//! - **Wayland:** On Wayland, scale factors are set per-screen by the server, and are always +//! integers (most often 1 or 2). +//! - **iOS:** Scale factors are set by Apple to the value that best suits the device, and range +//! from `1.0` to `3.0`. See [this article][apple_1] and [this article][apple_2] for more +//! information. +//! - **Android:** Scale factors are set by the manufacturer to the value that best suits the +//! device, and range from `1.0` to `4.0`. See [this article][android_1] for more information. +//! - **Web:** The scale factor is the ratio between CSS pixels and the physical device pixels. +//! In other words, it is the value of [`window.devicePixelRatio`][web_1]. It is affected by +//! both the screen scaling and the browser zoom level and can go below `1.0`. +//! +//! +//! [points]: https://en.wikipedia.org/wiki/Point_(typography) +//! [picas]: https://en.wikipedia.org/wiki/Pica_(typography) +//! [`ScaleFactorChanged`]: crate::event::WindowEvent::ScaleFactorChanged +//! [`window.scale_factor()`]: crate::window::Window::scale_factor +//! [windows_1]: https://docs.microsoft.com/en-us/windows/win32/hidpi/high-dpi-desktop-application-development-on-windows +//! [apple_1]: https://developer.apple.com/library/archive/documentation/DeviceInformation/Reference/iOSDeviceCompatibility/Displays/Displays.html +//! [apple_2]: https://developer.apple.com/design/human-interface-guidelines/macos/icons-and-images/image-size-and-resolution/ +//! [android_1]: https://developer.android.com/training/multiscreen/screendensities +//! [web_1]: https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio + +pub trait Pixel: Copy + Into { + fn from_f64(f: f64) -> Self; + fn cast(self) -> P { + P::from_f64(self.into()) + } +} + +impl Pixel for u8 { + fn from_f64(f: f64) -> Self { + f.round() as u8 + } +} +impl Pixel for u16 { + fn from_f64(f: f64) -> Self { + f.round() as u16 + } +} +impl Pixel for u32 { + fn from_f64(f: f64) -> Self { + f.round() as u32 + } +} +impl Pixel for i8 { + fn from_f64(f: f64) -> Self { + f.round() as i8 + } +} +impl Pixel for i16 { + fn from_f64(f: f64) -> Self { + f.round() as i16 + } +} +impl Pixel for i32 { + fn from_f64(f: f64) -> Self { + f.round() as i32 + } +} +impl Pixel for f32 { + fn from_f64(f: f64) -> Self { + f as f32 + } +} +impl Pixel for f64 { + fn from_f64(f: f64) -> Self { + f + } +} + +/// Checks that the scale factor is a normal positive `f64`. +/// +/// All functions that take a scale factor assert that this will return `true`. If you're sourcing scale factors from +/// anywhere other than winit, it's recommended to validate them using this function before passing them to winit; +/// otherwise, you risk panics. +#[inline] +pub fn validate_scale_factor(scale_factor: f64) -> bool { + scale_factor.is_sign_positive() && scale_factor.is_normal() +} + +/// A position represented in logical pixels. +/// +/// The position is stored as floats, so please be careful. Casting floats to integers truncates the +/// fractional part, which can cause noticable issues. To help with that, an `Into<(i32, i32)>` +/// implementation is provided which does the rounding for you. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct LogicalPosition

{ + pub x: P, + pub y: P, +} + +impl

LogicalPosition

{ + #[inline] + pub const fn new(x: P, y: P) -> Self { + LogicalPosition { x, y } + } +} + +impl LogicalPosition

{ + #[inline] + pub fn from_physical>, X: Pixel>( + physical: T, + scale_factor: f64, + ) -> Self { + physical.into().to_logical(scale_factor) + } + + #[inline] + pub fn to_physical( + &self, + scale_factor: f64, + ) -> PhysicalPosition { + assert!(validate_scale_factor(scale_factor)); + let x = self.x.into() * scale_factor; + let y = self.y.into() * scale_factor; + PhysicalPosition::new(x, y).cast() + } + + #[inline] + pub fn cast(&self) -> LogicalPosition { + LogicalPosition { + x: self.x.cast(), + y: self.y.cast(), + } + } +} + +impl From<(X, X)> for LogicalPosition

{ + fn from((x, y): (X, X)) -> LogicalPosition

{ + LogicalPosition::new(x.cast(), y.cast()) + } +} + +impl From> for (X, X) { + fn from(p: LogicalPosition

) -> (X, X) { + (p.x.cast(), p.y.cast()) + } +} + +impl From<[X; 2]> for LogicalPosition

{ + fn from([x, y]: [X; 2]) -> LogicalPosition

{ + LogicalPosition::new(x.cast(), y.cast()) + } +} + +impl From> for [X; 2] { + fn from(p: LogicalPosition

) -> [X; 2] { + [p.x.cast(), p.y.cast()] + } +} + +#[cfg(feature = "mint")] +impl From> for LogicalPosition

{ + fn from(p: mint::Point2

) -> Self { + Self::new(p.x, p.y) + } +} + +#[cfg(feature = "mint")] +impl From> for mint::Point2

{ + fn from(p: LogicalPosition

) -> Self { + mint::Point2 { x: p.x, y: p.y } + } +} + +/// A position represented in physical pixels. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct PhysicalPosition

{ + pub x: P, + pub y: P, +} + +impl

PhysicalPosition

{ + #[inline] + pub const fn new(x: P, y: P) -> Self { + PhysicalPosition { x, y } + } +} + +impl PhysicalPosition

{ + #[inline] + pub fn from_logical>, X: Pixel>( + logical: T, + scale_factor: f64, + ) -> Self { + logical.into().to_physical(scale_factor) + } + + #[inline] + pub fn to_logical( + &self, + scale_factor: f64, + ) -> LogicalPosition { + assert!(validate_scale_factor(scale_factor)); + let x = self.x.into() / scale_factor; + let y = self.y.into() / scale_factor; + LogicalPosition::new(x, y).cast() + } + + #[inline] + pub fn cast(&self) -> PhysicalPosition { + PhysicalPosition { + x: self.x.cast(), + y: self.y.cast(), + } + } +} + +impl From<(X, X)> for PhysicalPosition

{ + fn from((x, y): (X, X)) -> PhysicalPosition

{ + PhysicalPosition::new(x.cast(), y.cast()) + } +} + +impl From> for (X, X) { + fn from(p: PhysicalPosition

) -> (X, X) { + (p.x.cast(), p.y.cast()) + } +} + +impl From<[X; 2]> for PhysicalPosition

{ + fn from([x, y]: [X; 2]) -> PhysicalPosition

{ + PhysicalPosition::new(x.cast(), y.cast()) + } +} + +impl From> for [X; 2] { + fn from(p: PhysicalPosition

) -> [X; 2] { + [p.x.cast(), p.y.cast()] + } +} + +#[cfg(feature = "mint")] +impl From> for PhysicalPosition

{ + fn from(p: mint::Point2

) -> Self { + Self::new(p.x, p.y) + } +} + +#[cfg(feature = "mint")] +impl From> for mint::Point2

{ + fn from(p: PhysicalPosition

) -> Self { + mint::Point2 { x: p.x, y: p.y } + } +} + +/// A size represented in logical pixels. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct LogicalSize

{ + pub width: P, + pub height: P, +} + +impl

LogicalSize

{ + #[inline] + pub const fn new(width: P, height: P) -> Self { + LogicalSize { width, height } + } +} + +impl LogicalSize

{ + #[inline] + pub fn from_physical>, X: Pixel>( + physical: T, + scale_factor: f64, + ) -> Self { + physical.into().to_logical(scale_factor) + } + + #[inline] + pub fn to_physical(&self, scale_factor: f64) -> PhysicalSize { + assert!(validate_scale_factor(scale_factor)); + let width = self.width.into() * scale_factor; + let height = self.height.into() * scale_factor; + PhysicalSize::new(width, height).cast() + } + + #[inline] + pub fn cast(&self) -> LogicalSize { + LogicalSize { + width: self.width.cast(), + height: self.height.cast(), + } + } +} + +impl From<(X, X)> for LogicalSize

{ + fn from((x, y): (X, X)) -> LogicalSize

{ + LogicalSize::new(x.cast(), y.cast()) + } +} + +impl From> for (X, X) { + fn from(s: LogicalSize

) -> (X, X) { + (s.width.cast(), s.height.cast()) + } +} + +impl From<[X; 2]> for LogicalSize

{ + fn from([x, y]: [X; 2]) -> LogicalSize

{ + LogicalSize::new(x.cast(), y.cast()) + } +} + +impl From> for [X; 2] { + fn from(s: LogicalSize

) -> [X; 2] { + [s.width.cast(), s.height.cast()] + } +} + +#[cfg(feature = "mint")] +impl From> for LogicalSize

{ + fn from(v: mint::Vector2

) -> Self { + Self::new(v.x, v.y) + } +} + +#[cfg(feature = "mint")] +impl From> for mint::Vector2

{ + fn from(s: LogicalSize

) -> Self { + mint::Vector2 { + x: s.width, + y: s.height, + } + } +} + +/// A size represented in physical pixels. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct PhysicalSize

{ + pub width: P, + pub height: P, +} + +impl

PhysicalSize

{ + #[inline] + pub const fn new(width: P, height: P) -> Self { + PhysicalSize { width, height } + } +} + +impl PhysicalSize

{ + #[inline] + pub fn from_logical>, X: Pixel>( + logical: T, + scale_factor: f64, + ) -> Self { + logical.into().to_physical(scale_factor) + } + + #[inline] + pub fn to_logical(&self, scale_factor: f64) -> LogicalSize { + assert!(validate_scale_factor(scale_factor)); + let width = self.width.into() / scale_factor; + let height = self.height.into() / scale_factor; + LogicalSize::new(width, height).cast() + } + + #[inline] + pub fn cast(&self) -> PhysicalSize { + PhysicalSize { + width: self.width.cast(), + height: self.height.cast(), + } + } +} + +impl From<(X, X)> for PhysicalSize

{ + fn from((x, y): (X, X)) -> PhysicalSize

{ + PhysicalSize::new(x.cast(), y.cast()) + } +} + +impl From> for (X, X) { + fn from(s: PhysicalSize

) -> (X, X) { + (s.width.cast(), s.height.cast()) + } +} + +impl From<[X; 2]> for PhysicalSize

{ + fn from([x, y]: [X; 2]) -> PhysicalSize

{ + PhysicalSize::new(x.cast(), y.cast()) + } +} + +impl From> for [X; 2] { + fn from(s: PhysicalSize

) -> [X; 2] { + [s.width.cast(), s.height.cast()] + } +} + +#[cfg(feature = "mint")] +impl From> for PhysicalSize

{ + fn from(v: mint::Vector2

) -> Self { + Self::new(v.x, v.y) + } +} + +#[cfg(feature = "mint")] +impl From> for mint::Vector2

{ + fn from(s: PhysicalSize

) -> Self { + mint::Vector2 { + x: s.width, + y: s.height, + } + } +} + +/// A size that's either physical or logical. +#[derive(Debug, Copy, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum Size { + Physical(PhysicalSize), + Logical(LogicalSize), +} + +impl Size { + pub fn new>(size: S) -> Size { + size.into() + } + + pub fn to_logical(&self, scale_factor: f64) -> LogicalSize

{ + match *self { + Size::Physical(size) => size.to_logical(scale_factor), + Size::Logical(size) => size.cast(), + } + } + + pub fn to_physical(&self, scale_factor: f64) -> PhysicalSize

{ + match *self { + Size::Physical(size) => size.cast(), + Size::Logical(size) => size.to_physical(scale_factor), + } + } + + pub fn clamp>( + input: S, + min: S, + max: S, + scale_factor: f64, + ) -> Size { + let (input, min, max) = ( + input.into().to_physical::(scale_factor), + min.into().to_physical::(scale_factor), + max.into().to_physical::(scale_factor), + ); + + let clamp = |input: f64, min: f64, max: f64| { + if input < min { + min + } else if input > max { + max + } else { + input + } + }; + + let width = clamp(input.width, min.width, max.width); + let height = clamp(input.height, min.height, max.height); + + PhysicalSize::new(width, height).into() + } +} + +impl From> for Size { + #[inline] + fn from(size: PhysicalSize

) -> Size { + Size::Physical(size.cast()) + } +} + +impl From> for Size { + #[inline] + fn from(size: LogicalSize

) -> Size { + Size::Logical(size.cast()) + } +} + +/// A position that's either physical or logical. +#[derive(Debug, Copy, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum Position { + Physical(PhysicalPosition), + Logical(LogicalPosition), +} + +impl Position { + pub fn new>(position: S) -> Position { + position.into() + } + + pub fn to_logical( + &self, + scale_factor: f64, + ) -> LogicalPosition

{ + match *self { + Position::Physical(position) => position.to_logical(scale_factor), + Position::Logical(position) => position.cast(), + } + } + + pub fn to_physical( + &self, + scale_factor: f64, + ) -> PhysicalPosition

{ + match *self { + Position::Physical(position) => position.cast(), + Position::Logical(position) => position.to_physical(scale_factor), + } + } +} + +impl From> for Position { + #[inline] + fn from(position: PhysicalPosition

) -> Position { + Position::Physical(position.cast()) + } +} + +impl From> for Position { + #[inline] + fn from(position: LogicalPosition

) -> Position { + Position::Logical(position.cast()) + } +} diff --git a/sctk/src/error.rs b/sctk/src/error.rs new file mode 100644 index 0000000000..807a8f84f6 --- /dev/null +++ b/sctk/src/error.rs @@ -0,0 +1,23 @@ +use iced_futures::futures; + +/// An error that occurred while running an application. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// The futures executor could not be created. + #[error("the futures executor could not be created")] + ExecutorCreationFailed(futures::io::Error), + + /// The application window could not be created. + #[error("the application window could not be created")] + WindowCreationFailed(Box), + + /// The application graphics context could not be created. + #[error("the application graphics context could not be created")] + GraphicsCreationFailed(iced_graphics::Error), +} + +impl From for Error { + fn from(error: iced_graphics::Error) -> Error { + Error::GraphicsCreationFailed(error) + } +} diff --git a/sctk/src/event_loop/adapter.rs b/sctk/src/event_loop/adapter.rs new file mode 100644 index 0000000000..a185ad9172 --- /dev/null +++ b/sctk/src/event_loop/adapter.rs @@ -0,0 +1,34 @@ +use crate::sctk_event::ActionRequestEvent; +use iced_accessibility::{accesskit, accesskit_unix}; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::Proxy; +use std::{ + num::NonZeroU128, + sync::{Arc, Mutex}, +}; + +pub enum A11yWrapper { + Enabled, + Event(ActionRequestEvent), +} + +pub struct IcedSctkAdapter { + pub(crate) id: NonZeroU128, + pub(crate) adapter: accesskit_unix::Adapter, +} + +pub struct IcedSctkActionHandler { + pub(crate) wl_surface: WlSurface, + pub(crate) event_list: Arc>>, +} +impl accesskit::ActionHandler for IcedSctkActionHandler { + fn do_action(&self, request: accesskit::ActionRequest) { + let mut event_list = self.event_list.lock().unwrap(); + event_list.push(A11yWrapper::Event( + crate::sctk_event::ActionRequestEvent { + request, + surface_id: self.wl_surface.id(), + }, + )); + } +} diff --git a/sctk/src/event_loop/control_flow.rs b/sctk/src/event_loop/control_flow.rs new file mode 100644 index 0000000000..bc920ed478 --- /dev/null +++ b/sctk/src/event_loop/control_flow.rs @@ -0,0 +1,56 @@ +/// Set by the user callback given to the [`EventLoop::run`] method. +/// +/// Indicates the desired behavior of the event loop after [`Event::RedrawEventsCleared`] is emitted. +/// +/// Defaults to [`Poll`]. +/// +/// ## Persistency +/// +/// Almost every change is persistent between multiple calls to the event loop closure within a +/// given run loop. The only exception to this is [`ExitWithCode`] which, once set, cannot be unset. +/// Changes are **not** persistent between multiple calls to `run_return` - issuing a new call will +/// reset the control flow to [`Poll`]. +/// +/// [`ExitWithCode`]: Self::ExitWithCode +/// [`Poll`]: Self::Poll +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum ControlFlow { + /// When the current loop iteration finishes, immediately begin a new iteration regardless of + /// whether or not new events are available to process. + /// + /// ## Platform-specific + /// + /// - **Web:** Events are queued and usually sent when `requestAnimationFrame` fires but sometimes + /// the events in the queue may be sent before the next `requestAnimationFrame` callback, for + /// example when the scaling of the page has changed. This should be treated as an implementation + /// detail which should not be relied on. + Poll, + /// When the current loop iteration finishes, suspend the thread until another event arrives. + Wait, + /// When the current loop iteration finishes, suspend the thread until either another event + /// arrives or the given time is reached. + /// + /// Useful for implementing efficient timers. Applications which want to render at the display's + /// native refresh rate should instead use [`Poll`] and the VSync functionality of a graphics API + /// to reduce odds of missed frames. + /// + /// [`Poll`]: Self::Poll + WaitUntil(std::time::Instant), + /// Send a [`LoopDestroyed`] event and stop the event loop. This variant is *sticky* - once set, + /// `control_flow` cannot be changed from `ExitWithCode`, and any future attempts to do so will + /// result in the `control_flow` parameter being reset to `ExitWithCode`. + /// + /// The contained number will be used as exit code. The [`Exit`] constant is a shortcut for this + /// with exit code 0. + /// + /// ## Platform-specific + /// + /// - **Android / iOS / WASM:** The supplied exit code is unused. + /// - **Unix:** On most Unix-like platforms, only the 8 least significant bits will be used, + /// which can cause surprises with negative exit values (`-42` would end up as `214`). See + /// [`std::process::exit`]. + /// + /// [`LoopDestroyed`]: Event::LoopDestroyed + /// [`Exit`]: ControlFlow::Exit + ExitWithCode(i32), +} diff --git a/sctk/src/event_loop/mod.rs b/sctk/src/event_loop/mod.rs new file mode 100644 index 0000000000..46f33cd702 --- /dev/null +++ b/sctk/src/event_loop/mod.rs @@ -0,0 +1,1373 @@ +#[cfg(feature = "a11y")] +pub mod adapter; +pub mod control_flow; +pub mod proxy; +pub mod state; + +#[cfg(feature = "a11y")] +use crate::application::SurfaceIdWrapper; +use crate::{ + application::Event, + conversion, + dpi::LogicalSize, + handlers::{ + activation::IcedRequestData, + wp_fractional_scaling::FractionalScalingManager, + wp_viewporter::ViewporterState, + }, + sctk_event::{ + DndOfferEvent, IcedSctkEvent, LayerSurfaceEventVariant, + PopupEventVariant, SctkEvent, StartCause, WindowEventVariant, + }, + settings, +}; +use iced_futures::core::window::Mode; +use iced_runtime::command::platform_specific::{ + self, + wayland::{ + data_device::DndIcon, layer_surface::SctkLayerSurfaceSettings, + window::SctkWindowSettings, + }, +}; +use sctk::{ + activation::{ActivationState, RequestData}, + compositor::CompositorState, + data_device_manager::DataDeviceManagerState, + output::OutputState, + reexports::{ + calloop::{self, EventLoop, PostAction}, + client::{ + globals::registry_queue_init, protocol::wl_surface::WlSurface, + ConnectError, Connection, DispatchError, Proxy, + }, + }, + registry::RegistryState, + seat::SeatState, + session_lock::SessionLockState, + shell::{wlr_layer::LayerShell, xdg::XdgShell, WaylandSurface}, + shm::Shm, +}; +use sctk::{ + data_device_manager::data_source::DragSource, + reexports::calloop_wayland_source::WaylandSource, +}; +#[cfg(feature = "a11y")] +use std::sync::{Arc, Mutex}; +use std::{ + collections::HashMap, + fmt::Debug, + io::{BufRead, BufReader}, + num::NonZeroU32, + time::{Duration, Instant}, +}; +use tracing::error; +use wayland_backend::client::WaylandError; + +use self::{ + control_flow::ControlFlow, + state::{Dnd, LayerSurfaceCreationError, SctkState}, +}; + +#[derive(Debug, Default, Clone, Copy)] +pub struct Features { + // TODO +} + +pub struct SctkEventLoop { + // TODO after merged + // pub data_device_manager_state: DataDeviceManagerState, + pub(crate) event_loop: EventLoop<'static, SctkState>, + pub(crate) wayland_dispatcher: + calloop::Dispatcher<'static, WaylandSource>, SctkState>, + pub(crate) _features: Features, + /// A proxy to wake up event loop. + pub event_loop_awakener: calloop::ping::Ping, + /// A sender for submitting user events in the event loop + pub user_events_sender: calloop::channel::Sender>, + pub(crate) state: SctkState, + + #[cfg(feature = "a11y")] + pub(crate) a11y_events: Arc>>, +} + +impl SctkEventLoop +where + T: 'static + Debug, +{ + pub(crate) fn new( + _settings: &settings::Settings, + ) -> Result { + let connection = Connection::connect_to_env()?; + let _display = connection.display(); + let (globals, event_queue) = registry_queue_init(&connection).unwrap(); + let event_loop = calloop::EventLoop::>::try_new().unwrap(); + let loop_handle = event_loop.handle(); + + let qh = event_queue.handle(); + let registry_state = RegistryState::new(&globals); + + let (ping, ping_source) = calloop::ping::make_ping().unwrap(); + // TODO + loop_handle + .insert_source(ping_source, |_, _, _state| { + // Drain events here as well to account for application doing batch event processing + // on RedrawEventsCleared. + // shim::handle_window_requests(state); + }) + .unwrap(); + let (user_events_sender, user_events_channel) = + calloop::channel::channel(); + + loop_handle + .insert_source(user_events_channel, |event, _, state| match event { + calloop::channel::Event::Msg(e) => { + state.pending_user_events.push(e); + } + calloop::channel::Event::Closed => {} + }) + .unwrap(); + let wayland_source = + WaylandSource::new(connection.clone(), event_queue); + + let wayland_dispatcher = calloop::Dispatcher::new( + wayland_source, + |_, queue, winit_state| queue.dispatch_pending(winit_state), + ); + + let _wayland_source_dispatcher = event_loop + .handle() + .register_dispatcher(wayland_dispatcher.clone()) + .unwrap(); + + let (viewporter_state, fractional_scaling_manager) = + match FractionalScalingManager::new(&globals, &qh) { + Ok(m) => { + let viewporter_state = + match ViewporterState::new(&globals, &qh) { + Ok(s) => Some(s), + Err(e) => { + error!( + "Failed to initialize viewporter: {}", + e + ); + None + } + }; + (viewporter_state, Some(m)) + } + Err(e) => { + error!( + "Failed to initialize fractional scaling manager: {}", + e + ); + (None, None) + } + }; + + Ok(Self { + event_loop, + wayland_dispatcher, + state: SctkState { + connection, + registry_state, + seat_state: SeatState::new(&globals, &qh), + output_state: OutputState::new(&globals, &qh), + compositor_state: CompositorState::bind(&globals, &qh) + .expect("wl_compositor is not available"), + shm_state: Shm::bind(&globals, &qh) + .expect("wl_shm is not available"), + xdg_shell_state: XdgShell::bind(&globals, &qh) + .expect("xdg shell is not available"), + layer_shell: LayerShell::bind(&globals, &qh).ok(), + data_device_manager_state: DataDeviceManagerState::bind( + &globals, &qh, + ) + .expect("data device manager is not available"), + activation_state: ActivationState::bind(&globals, &qh).ok(), + session_lock_state: SessionLockState::new(&globals, &qh), + session_lock: None, + + queue_handle: qh, + loop_handle, + + _cursor_surface: None, + _multipool: None, + outputs: Vec::new(), + seats: Vec::new(), + windows: Vec::new(), + layer_surfaces: Vec::new(), + popups: Vec::new(), + lock_surfaces: Vec::new(), + dnd_source: None, + _kbd_focus: None, + sctk_events: Vec::new(), + frame_events: Vec::new(), + pending_user_events: Vec::new(), + token_ctr: 0, + _accept_counter: 0, + dnd_offer: None, + fractional_scaling_manager, + viewporter_state, + compositor_updates: Default::default(), + }, + _features: Default::default(), + event_loop_awakener: ping, + user_events_sender, + #[cfg(feature = "a11y")] + a11y_events: Arc::new(Mutex::new(Vec::new())), + }) + } + + pub fn proxy(&self) -> proxy::Proxy> { + proxy::Proxy::new(self.user_events_sender.clone()) + } + + pub fn get_layer_surface( + &mut self, + layer_surface: SctkLayerSurfaceSettings, + ) -> Result<(iced_runtime::window::Id, WlSurface), LayerSurfaceCreationError> + { + self.state.get_layer_surface(layer_surface) + } + + pub fn get_window( + &mut self, + settings: SctkWindowSettings, + ) -> (iced_runtime::window::Id, WlSurface) { + self.state.get_window(settings) + } + + // TODO Ashley provide users a reasonable method of setting the role for the surface + #[cfg(feature = "a11y")] + pub fn init_a11y_adapter( + &mut self, + surface: &WlSurface, + app_id: Option, + surface_title: Option, + role: iced_accessibility::accesskit::Role, + ) -> adapter::IcedSctkAdapter { + use iced_accessibility::{ + accesskit::{ + Node, NodeBuilder, NodeClassSet, NodeId, Role, Tree, TreeUpdate, + }, + accesskit_unix::Adapter, + window_node_id, + }; + let node_id = window_node_id(); + let event_list = self.a11y_events.clone(); + adapter::IcedSctkAdapter { + adapter: Adapter::new( + app_id.unwrap_or_else(|| String::from("None")), + "Iced".to_string(), + env!("CARGO_PKG_VERSION").to_string(), + move || { + event_list + .lock() + .unwrap() + .push(adapter::A11yWrapper::Enabled); + let mut node = NodeBuilder::new(Role::Window); + if let Some(name) = surface_title { + node.set_name(name); + } + let node = node.build(&mut NodeClassSet::lock_global()); + TreeUpdate { + nodes: vec![(NodeId(node_id), node)], + tree: Some(Tree::new(NodeId(node_id))), + focus: None, + } + }, + Box::new(adapter::IcedSctkActionHandler { + wl_surface: surface.clone(), + event_list: self.a11y_events.clone(), + }), + ) + .unwrap(), + id: node_id, + } + } + + pub fn run_return(&mut self, mut callback: F) -> i32 + where + F: FnMut(IcedSctkEvent, &SctkState, &mut ControlFlow), + { + let mut control_flow = ControlFlow::Poll; + + callback( + IcedSctkEvent::NewEvents(StartCause::Init), + &self.state, + &mut control_flow, + ); + + let mut sctk_event_sink_back_buffer = Vec::new(); + let mut compositor_event_back_buffer = Vec::new(); + let mut frame_event_back_buffer = Vec::new(); + + // NOTE We break on errors from dispatches, since if we've got protocol error + // libwayland-client/wayland-rs will inform us anyway, but crashing downstream is not + // really an option. Instead we inform that the event loop got destroyed. We may + // communicate an error that something was terminated, but winit doesn't provide us + // with an API to do that via some event. + // Still, we set the exit code to the error's OS error code, or to 1 if not possible. + let exit_code = loop { + // Send pending events to the server. + match self.wayland_dispatcher.as_source_ref().connection().flush() { + Ok(_) => {} + Err(error) => { + break match error { + WaylandError::Io(err) => err.raw_os_error(), + WaylandError::Protocol(_) => None, + } + .unwrap_or(1) + } + } + + // During the run of the user callback, some other code monitoring and reading the + // Wayland socket may have been run (mesa for example does this with vsync), if that + // is the case, some events may have been enqueued in our event queue. + // + // If some messages are there, the event loop needs to behave as if it was instantly + // woken up by messages arriving from the Wayland socket, to avoid delaying the + // dispatch of these events until we're woken up again. + let instant_wakeup = { + let mut wayland_source = + self.wayland_dispatcher.as_source_mut(); + let queue = wayland_source.queue(); + match queue.dispatch_pending(&mut self.state) { + Ok(dispatched) => dispatched > 0, + // TODO better error handling + Err(error) => { + break match error { + DispatchError::BadMessage { .. } => None, + DispatchError::Backend(err) => match err { + WaylandError::Io(err) => err.raw_os_error(), + WaylandError::Protocol(_) => None, + }, + } + .unwrap_or(1) + } + } + }; + + match control_flow { + ControlFlow::ExitWithCode(code) => break code, + ControlFlow::Poll => { + // Non-blocking dispatch. + let timeout = Duration::from_millis(0); + if let Err(error) = + self.event_loop.dispatch(Some(timeout), &mut self.state) + { + break raw_os_err(error); + } + + callback( + IcedSctkEvent::NewEvents(StartCause::Poll), + &self.state, + &mut control_flow, + ); + } + ControlFlow::Wait => { + let timeout = if instant_wakeup { + Some(Duration::from_millis(0)) + } else { + None + }; + + if let Err(error) = + self.event_loop.dispatch(timeout, &mut self.state) + { + break raw_os_err(error); + } + + callback( + IcedSctkEvent::NewEvents(StartCause::WaitCancelled { + start: Instant::now(), + requested_resume: None, + }), + &self.state, + &mut control_flow, + ); + } + ControlFlow::WaitUntil(deadline) => { + let start = Instant::now(); + + // Compute the amount of time we'll block for. + let duration = if deadline > start && !instant_wakeup { + deadline - start + } else { + Duration::from_millis(0) + }; + + if let Err(error) = self + .event_loop + .dispatch(Some(duration), &mut self.state) + { + break raw_os_err(error); + } + + let now = Instant::now(); + + if now < deadline { + callback( + IcedSctkEvent::NewEvents( + StartCause::WaitCancelled { + start, + requested_resume: Some(deadline), + }, + ), + &self.state, + &mut control_flow, + ) + } else { + callback( + IcedSctkEvent::NewEvents( + StartCause::ResumeTimeReached { + start, + requested_resume: deadline, + }, + ), + &self.state, + &mut control_flow, + ) + } + } + } + + // handle compositor events + std::mem::swap( + &mut compositor_event_back_buffer, + &mut self.state.compositor_updates, + ); + + for event in compositor_event_back_buffer.drain(..) { + let forward_event = match &event { + SctkEvent::LayerSurfaceEvent { + variant: + LayerSurfaceEventVariant::ScaleFactorChanged(..), + .. + } + | SctkEvent::PopupEvent { + variant: PopupEventVariant::ScaleFactorChanged(..), + .. + } + | SctkEvent::WindowEvent { + variant: WindowEventVariant::ScaleFactorChanged(..), + .. + } => true, + // ignore other events that shouldn't be in this buffer + event => { + tracing::warn!( + "Unhandled compositor event: {:?}", + event + ); + false + } + }; + if forward_event { + sticky_exit_callback( + IcedSctkEvent::SctkEvent(event), + &self.state, + &mut control_flow, + &mut callback, + ); + } + } + + std::mem::swap( + &mut frame_event_back_buffer, + &mut self.state.frame_events, + ); + + for event in frame_event_back_buffer.drain(..) { + sticky_exit_callback( + IcedSctkEvent::Frame(event), + &self.state, + &mut control_flow, + &mut callback, + ); + } + + // The purpose of the back buffer and that swap is to not hold borrow_mut when + // we're doing callback to the user, since we can double borrow if the user decides + // to create a window in one of those callbacks. + std::mem::swap( + &mut sctk_event_sink_back_buffer, + &mut self.state.sctk_events, + ); + + // handle a11y events + #[cfg(feature = "a11y")] + if let Ok(mut events) = self.a11y_events.lock() { + for event in events.drain(..) { + match event { + adapter::A11yWrapper::Enabled => sticky_exit_callback( + IcedSctkEvent::A11yEnabled, + &self.state, + &mut control_flow, + &mut callback, + ), + adapter::A11yWrapper::Event(event) => { + sticky_exit_callback( + IcedSctkEvent::A11yEvent(event), + &self.state, + &mut control_flow, + &mut callback, + ) + } + } + } + } + // Handle pending sctk events. + for event in sctk_event_sink_back_buffer.drain(..) { + match event { + SctkEvent::PopupEvent { + variant: PopupEventVariant::Done, + toplevel_id, + parent_id, + id, + } => { + match self + .state + .popups + .iter() + .position(|s| s.popup.wl_surface().id() == id.id()) + { + Some(p) => { + let _p = self.state.popups.remove(p); + sticky_exit_callback( + IcedSctkEvent::SctkEvent( + SctkEvent::PopupEvent { + variant: PopupEventVariant::Done, + toplevel_id, + parent_id, + id, + }, + ), + &self.state, + &mut control_flow, + &mut callback, + ); + } + None => continue, + }; + } + SctkEvent::LayerSurfaceEvent { + variant: LayerSurfaceEventVariant::Done, + id, + } => { + if let Some(i) = + self.state.layer_surfaces.iter().position(|l| { + l.surface.wl_surface().id() == id.id() + }) + { + let _l = self.state.layer_surfaces.remove(i); + sticky_exit_callback( + IcedSctkEvent::SctkEvent( + SctkEvent::LayerSurfaceEvent { + variant: LayerSurfaceEventVariant::Done, + id, + }, + ), + &self.state, + &mut control_flow, + &mut callback, + ); + } + } + SctkEvent::WindowEvent { + variant: WindowEventVariant::Close, + id, + } => { + if let Some(i) = + self.state.windows.iter().position(|l| { + l.window.wl_surface().id() == id.id() + }) + { + let w = self.state.windows.remove(i); + w.window.xdg_toplevel().destroy(); + sticky_exit_callback( + IcedSctkEvent::SctkEvent( + SctkEvent::WindowEvent { + variant: WindowEventVariant::Close, + id, + }, + ), + &self.state, + &mut control_flow, + &mut callback, + ); + } + } + _ => sticky_exit_callback( + IcedSctkEvent::SctkEvent(event), + &self.state, + &mut control_flow, + &mut callback, + ), + } + } + + // handle events indirectly via callback to the user. + let (sctk_events, user_events): (Vec<_>, Vec<_>) = self + .state + .pending_user_events + .drain(..) + .partition(|e| matches!(e, Event::SctkEvent(_))); + let mut to_commit = HashMap::new(); + let mut pending_redraws = Vec::new(); + for event in sctk_events.into_iter().chain(user_events.into_iter()) + { + match event { + Event::Message(m) => { + sticky_exit_callback( + IcedSctkEvent::UserEvent(m), + &self.state, + &mut control_flow, + &mut callback, + ); + } + Event::SctkEvent(event) => { + match event { + IcedSctkEvent::RedrawRequested(id) => { + pending_redraws.push(id); + }, + e => sticky_exit_callback( + e, + &self.state, + &mut control_flow, + &mut callback, + ), + } + } + Event::LayerSurface(action) => match action { + platform_specific::wayland::layer_surface::Action::LayerSurface { + builder, + _phantom, + } => { + // TODO ASHLEY: error handling + if let Ok((id, wl_surface)) = self.state.get_layer_surface(builder) { + let object_id = wl_surface.id(); + // TODO Ashley: all surfaces should probably have an optional title for a11y if nothing else + sticky_exit_callback( + IcedSctkEvent::SctkEvent(SctkEvent::LayerSurfaceEvent { + variant: LayerSurfaceEventVariant::Created(object_id.clone(), id), + id: wl_surface.clone(), + }), + &self.state, + &mut control_flow, + &mut callback, + ); + #[cfg(feature = "a11y")] + { + let adapter = self.init_a11y_adapter(&wl_surface, None, None, iced_accessibility::accesskit::Role::Window); + + sticky_exit_callback( + IcedSctkEvent::A11ySurfaceCreated(SurfaceIdWrapper::LayerSurface(id), adapter), + &self.state, + &mut control_flow, + &mut callback, + ); + } + } + } + platform_specific::wayland::layer_surface::Action::Size { + id, + width, + height, + } => { + if let Some(layer_surface) = self.state.layer_surfaces.iter_mut().find(|l| l.id == id) { + layer_surface.set_size(width, height); + pending_redraws.push(layer_surface.surface.wl_surface().id()); + let wl_surface = layer_surface.surface.wl_surface(); + + if let Some(mut prev_configure) = layer_surface.last_configure.clone() { + prev_configure.new_size = (width.unwrap_or(prev_configure.new_size.0), width.unwrap_or(prev_configure.new_size.1)); + sticky_exit_callback( + IcedSctkEvent::SctkEvent(SctkEvent::LayerSurfaceEvent { variant: LayerSurfaceEventVariant::Configure(prev_configure, wl_surface.clone(), false), id: wl_surface.clone()}), + &self.state, + &mut control_flow, + &mut callback, + ); + } + } + }, + platform_specific::wayland::layer_surface::Action::Destroy(id) => { + if let Some(i) = self.state.layer_surfaces.iter().position(|l| l.id == id) { + let l = self.state.layer_surfaces.remove(i); + sticky_exit_callback( + IcedSctkEvent::SctkEvent(SctkEvent::LayerSurfaceEvent { + variant: LayerSurfaceEventVariant::Done, + id: l.surface.wl_surface().clone(), + }), + &self.state, + &mut control_flow, + &mut callback, + ); + } + }, + platform_specific::wayland::layer_surface::Action::Anchor { id, anchor } => { + if let Some(layer_surface) = self.state.layer_surfaces.iter_mut().find(|l| l.id == id) { + layer_surface.anchor = anchor; + layer_surface.surface.set_anchor(anchor); + to_commit.insert(id, layer_surface.surface.wl_surface().clone()); + + } + } + platform_specific::wayland::layer_surface::Action::ExclusiveZone { + id, + exclusive_zone, + } => { + if let Some(layer_surface) = self.state.layer_surfaces.iter_mut().find(|l| l.id == id) { + layer_surface.exclusive_zone = exclusive_zone; + layer_surface.surface.set_exclusive_zone(exclusive_zone); + to_commit.insert(id, layer_surface.surface.wl_surface().clone()); + } + }, + platform_specific::wayland::layer_surface::Action::Margin { + id, + margin, + } => { + if let Some(layer_surface) = self.state.layer_surfaces.iter_mut().find(|l| l.id == id) { + layer_surface.margin = margin; + layer_surface.surface.set_margin(margin.top, margin.right, margin.bottom, margin.left); + to_commit.insert(id, layer_surface.surface.wl_surface().clone()); + } + }, + platform_specific::wayland::layer_surface::Action::KeyboardInteractivity { id, keyboard_interactivity } => { + if let Some(layer_surface) = self.state.layer_surfaces.iter_mut().find(|l| l.id == id) { + layer_surface.keyboard_interactivity = keyboard_interactivity; + layer_surface.surface.set_keyboard_interactivity(keyboard_interactivity); + to_commit.insert(id, layer_surface.surface.wl_surface().clone()); + + } + }, + platform_specific::wayland::layer_surface::Action::Layer { id, layer } => { + if let Some(layer_surface) = self.state.layer_surfaces.iter_mut().find(|l| l.id == id) { + layer_surface.layer = layer; + layer_surface.surface.set_layer(layer); + to_commit.insert(id, layer_surface.surface.wl_surface().clone()); + + } + }, + }, + Event::SetCursor(iced_icon) => { + if let Some(ptr) = self.state.seats.get(0).and_then(|s| s.ptr.as_ref()) { + let icon = conversion::cursor_icon(iced_icon); + let _ = ptr.set_cursor(self.wayland_dispatcher.as_source_ref().connection(), icon); + } + + } + Event::Window(action) => match action { + platform_specific::wayland::window::Action::Window { builder, _phantom } => { + #[cfg(feature = "a11y")] + let app_id = builder.app_id.clone(); + #[cfg(feature = "a11y")] + let title = builder.title.clone(); + let (id, wl_surface) = self.state.get_window(builder); + let object_id = wl_surface.id(); + sticky_exit_callback( + IcedSctkEvent::SctkEvent(SctkEvent::WindowEvent { + variant: WindowEventVariant::Created(object_id.clone(), id), + id: wl_surface.clone() }), + &self.state, + &mut control_flow, + &mut callback, + ); + + #[cfg(feature = "a11y")] + { + let adapter = self.init_a11y_adapter(&wl_surface, app_id, title, iced_accessibility::accesskit::Role::Window); + + sticky_exit_callback( + IcedSctkEvent::A11ySurfaceCreated(SurfaceIdWrapper::Window(id), adapter), + &self.state, + &mut control_flow, + &mut callback, + ); + } + }, + platform_specific::wayland::window::Action::Size { id, width, height } => { + if let Some(window) = self.state.windows.iter_mut().find(|w| w.id == id) { + window.set_size(LogicalSize::new(NonZeroU32::new(width).unwrap_or(NonZeroU32::new(1).unwrap()), NonZeroU32::new(1).unwrap())); + // TODO Ashley maybe don't force window size? + pending_redraws.push(window.window.wl_surface().id()); + + if let Some(mut prev_configure) = window.last_configure.clone() { + let (width, height) = ( + NonZeroU32::new(width).unwrap_or(NonZeroU32::new(1).unwrap()), + NonZeroU32::new(height).unwrap_or(NonZeroU32::new(1).unwrap()), + ); + prev_configure.new_size = (Some(width), Some(height)); + sticky_exit_callback( + IcedSctkEvent::SctkEvent(SctkEvent::WindowEvent { variant: WindowEventVariant::Configure(prev_configure, window.window.wl_surface().clone(), false), id: window.window.wl_surface().clone()}), + &self.state, + &mut control_flow, + &mut callback, + ); + } + } + }, + platform_specific::wayland::window::Action::MinSize { id, size } => { + if let Some(window) = self.state.windows.iter_mut().find(|w| w.id == id) { + window.window.set_min_size(size); + to_commit.insert(id, window.window.wl_surface().clone()); + } + }, + platform_specific::wayland::window::Action::MaxSize { id, size } => { + if let Some(window) = self.state.windows.iter_mut().find(|w| w.id == id) { + window.window.set_max_size(size); + to_commit.insert(id, window.window.wl_surface().clone()); + } + }, + platform_specific::wayland::window::Action::Title { id, title } => { + if let Some(window) = self.state.windows.iter_mut().find(|w| w.id == id) { + window.window.set_title(title); + to_commit.insert(id, window.window.wl_surface().clone()); + } + }, + platform_specific::wayland::window::Action::Minimize { id } => { + if let Some(window) = self.state.windows.iter_mut().find(|w| w.id == id) { + window.window.set_minimized(); + to_commit.insert(id, window.window.wl_surface().clone()); + } + }, + platform_specific::wayland::window::Action::Maximize { id } => { + if let Some(window) = self.state.windows.iter_mut().find(|w| w.id == id) { + window.window.set_maximized(); + to_commit.insert(id, window.window.wl_surface().clone()); + } + }, + platform_specific::wayland::window::Action::UnsetMaximize { id } => { + if let Some(window) = self.state.windows.iter_mut().find(|w| w.id == id) { + window.window.unset_maximized(); + to_commit.insert(id, window.window.wl_surface().clone()); + } + }, + platform_specific::wayland::window::Action::Fullscreen { id } => { + if let Some(window) = self.state.windows.iter_mut().find(|w| w.id == id) { + // TODO ASHLEY: allow specific output to be requested for fullscreen? + window.window.set_fullscreen(None); + to_commit.insert(id, window.window.wl_surface().clone()); + } + }, + platform_specific::wayland::window::Action::UnsetFullscreen { id } => { + if let Some(window) = self.state.windows.iter_mut().find(|w| w.id == id) { + window.window.unset_fullscreen(); + to_commit.insert(id, window.window.wl_surface().clone()); + } + }, + platform_specific::wayland::window::Action::InteractiveMove { id } => { + if let (Some(window), Some((seat, last_press))) = (self.state.windows.iter_mut().find(|w| w.id == id), self.state.seats.first().and_then(|seat| seat.last_ptr_press.map(|p| (&seat.seat, p.2)))) { + window.window.xdg_toplevel()._move(seat, last_press); + to_commit.insert(id, window.window.wl_surface().clone()); + } + }, + platform_specific::wayland::window::Action::InteractiveResize { id, edge } => { + if let (Some(window), Some((seat, last_press))) = (self.state.windows.iter_mut().find(|w| w.id == id), self.state.seats.first().and_then(|seat| seat.last_ptr_press.map(|p| (&seat.seat, p.2)))) { + window.window.xdg_toplevel().resize(seat, last_press, edge); + to_commit.insert(id, window.window.wl_surface().clone()); + } + }, + platform_specific::wayland::window::Action::ToggleMaximized { id } => { + if let Some(window) = self.state.windows.iter_mut().find(|w| w.id == id) { + if let Some(c) = &window.last_configure { + if c.is_maximized() { + window.window.unset_maximized(); + } else { + window.window.set_maximized(); + } + to_commit.insert(id, window.window.wl_surface().clone()); + } + } + }, + platform_specific::wayland::window::Action::ShowWindowMenu { id: _, x: _, y: _ } => todo!(), + platform_specific::wayland::window::Action::Destroy(id) => { + if let Some(i) = self.state.windows.iter().position(|l| l.id == id) { + let window = self.state.windows.remove(i); + window.window.xdg_toplevel().destroy(); + sticky_exit_callback( + IcedSctkEvent::SctkEvent(SctkEvent::WindowEvent { + variant: WindowEventVariant::Close, + id: window.window.wl_surface().clone(), + }), + &self.state, + &mut control_flow, + &mut callback, + ); + } + }, + platform_specific::wayland::window::Action::Mode(id, mode) => { + if let Some(window) = self.state.windows.iter_mut().find(|w| w.id == id) { + match mode { + Mode::Windowed => { + window.window.unset_fullscreen(); + }, + Mode::Fullscreen => { + window.window.set_fullscreen(None); + }, + Mode::Hidden => { + window.window.set_minimized(); + }, + } + to_commit.insert(id, window.window.wl_surface().clone()); + } + }, + platform_specific::wayland::window::Action::ToggleFullscreen { id } => { + if let Some(window) = self.state.windows.iter_mut().find(|w| w.id == id) { + if let Some(c) = &window.last_configure { + if c.is_fullscreen() { + window.window.unset_fullscreen(); + } else { + window.window.set_fullscreen(None); + } + to_commit.insert(id, window.window.wl_surface().clone()); + } + } + }, + platform_specific::wayland::window::Action::AppId { id, app_id } => { + if let Some(window) = self.state.windows.iter_mut().find(|w| w.id == id) { + window.window.set_app_id(app_id); + to_commit.insert(id, window.window.wl_surface().clone()); + } + }, + }, + Event::Popup(action) => match action { + platform_specific::wayland::popup::Action::Popup { popup, .. } => { + if let Ok((id, parent_id, toplevel_id, wl_surface)) = self.state.get_popup(popup) { + let object_id = wl_surface.id(); + sticky_exit_callback( + IcedSctkEvent::SctkEvent(SctkEvent::PopupEvent { + variant: crate::sctk_event::PopupEventVariant::Created(object_id.clone(), id), + toplevel_id, parent_id, id: wl_surface.clone() }), + &self.state, + &mut control_flow, + &mut callback, + ); + + #[cfg(feature = "a11y")] + { + let adapter = self.init_a11y_adapter(&wl_surface, None, None, iced_accessibility::accesskit::Role::Window); + + sticky_exit_callback( + IcedSctkEvent::A11ySurfaceCreated(SurfaceIdWrapper::LayerSurface(id), adapter), + &self.state, + &mut control_flow, + &mut callback, + ); + } + } + }, + // XXX popup destruction must be done carefully + // first destroy the uppermost popup, then work down to the requested popup + platform_specific::wayland::popup::Action::Destroy { id } => { + let sctk_popup = match self.state + .popups + .iter() + .position(|s| s.data.id == id) + { + Some(p) => self.state.popups.remove(p), + None => continue, + }; + let mut to_destroy = vec![sctk_popup]; + while let Some(popup_to_destroy) = to_destroy.last() { + match popup_to_destroy.data.parent.clone() { + state::SctkSurface::LayerSurface(_) | state::SctkSurface::Window(_) => { + break; + } + state::SctkSurface::Popup(popup_to_destroy_first) => { + let popup_to_destroy_first = self + .state + .popups + .iter() + .position(|p| p.popup.wl_surface() == &popup_to_destroy_first) + .unwrap(); + let popup_to_destroy_first = self.state.popups.remove(popup_to_destroy_first); + to_destroy.push(popup_to_destroy_first); + } + } + } + for popup in to_destroy.into_iter().rev() { + sticky_exit_callback(IcedSctkEvent::SctkEvent(SctkEvent::PopupEvent { + variant: PopupEventVariant::Done, + toplevel_id: popup.data.toplevel.clone(), + parent_id: popup.data.parent.wl_surface().clone(), + id: popup.popup.wl_surface().clone(), + }), + &self.state, + &mut control_flow, + &mut callback, + ); + } + }, + platform_specific::wayland::popup::Action::Size { id, width, height } => { + if let Some(sctk_popup) = self.state + .popups + .iter_mut() + .find(|s| s.data.id == id) + { + // update geometry + // update positioner + self.state.token_ctr += 1; + sctk_popup.set_size(width, height, self.state.token_ctr); + + pending_redraws.push(sctk_popup.popup.wl_surface().id()); + + sticky_exit_callback(IcedSctkEvent::SctkEvent(SctkEvent::PopupEvent { + variant: PopupEventVariant::Size(width, height), + toplevel_id: sctk_popup.data.toplevel.clone(), + parent_id: sctk_popup.data.parent.wl_surface().clone(), + id: sctk_popup.popup.wl_surface().clone(), + }), + &self.state, + &mut control_flow, + &mut callback, + ); + } + }, + // TODO probably remove this? + platform_specific::wayland::popup::Action::Grab { .. } => {}, + }, + Event::DataDevice(action) => { + match action.inner { + platform_specific::wayland::data_device::ActionInner::Accept(mime_type) => { + let drag_offer = match self.state.dnd_offer.as_mut() { + Some(d) => d, + None => continue, + }; + drag_offer.offer.accept_mime_type(drag_offer.offer.serial, mime_type); + } + platform_specific::wayland::data_device::ActionInner::StartInternalDnd { origin_id, icon_id } => { + let qh = &self.state.queue_handle.clone(); + let seat = match self.state.seats.get(0) { + Some(s) => s, + None => continue, + }; + let serial = match seat.last_ptr_press { + Some(s) => s.2, + None => continue, + }; + + let origin = match self + .state + .windows + .iter() + .find(|w| w.id == origin_id) + .map(|w| Some(w.window.wl_surface())) + .unwrap_or_else(|| self.state.layer_surfaces.iter() + .find(|l| l.id == origin_id).map(|l| Some(l.surface.wl_surface())) + .unwrap_or_else(|| self.state.popups.iter().find(|p| p.data.id == origin_id).map(|p| p.popup.wl_surface()))) { + Some(s) => s.clone(), + None => continue, + }; + let device = match self.state.seats.get(0) { + Some(s) => &s.data_device, + None => continue, + }; + let icon_surface = if let Some(icon_id) = icon_id{ + let wl_surface = self.state.compositor_state.create_surface(qh); + DragSource::start_internal_drag(device, &origin, Some(&wl_surface), serial); + Some((wl_surface, icon_id)) + } else { + DragSource::start_internal_drag(device, &origin, None, serial); + None + }; + self.state.dnd_source = Some(Dnd { + origin_id, + icon_surface, + origin, + source: None, + pending_requests: Vec::new(), + pipe: None, + cur_write: None, + }); + } + platform_specific::wayland::data_device::ActionInner::StartDnd { mime_types, actions, origin_id, icon_id, data } => { + if let Some(dnd_source) = self.state.dnd_source.as_ref() { + if dnd_source.cur_write.is_some() { + continue; + } + } + let qh = &self.state.queue_handle.clone(); + let seat = match self.state.seats.get(0) { + Some(s) => s, + None => continue, + }; + let serial = match seat.last_ptr_press { + Some(s) => s.2, + None => continue, + }; + + let origin = match self + .state + .windows + .iter() + .find(|w| w.id == origin_id) + .map(|w| Some(w.window.wl_surface())) + .unwrap_or_else(|| self.state.layer_surfaces.iter() + .find(|l| l.id == origin_id).map(|l| Some(l.surface.wl_surface())) + .unwrap_or_else(|| self.state.popups.iter().find(|p| p.data.id == origin_id).map(|p| p.popup.wl_surface()))) { + Some(s) => s.clone(), + None => continue, + }; + let device = match self.state.seats.get(0) { + Some(s) => &s.data_device, + None => continue, + }; + let source = self.state.data_device_manager_state.create_drag_and_drop_source(qh, mime_types.iter().map(|s| s.as_str()).collect::>(), actions); + let icon_surface = if let Some(icon_id) = icon_id{ + let icon_native_id = match &icon_id { + DndIcon::Custom(icon_id) => *icon_id, + DndIcon::Widget(icon_id, _) => *icon_id, + }; + let wl_surface = self.state.compositor_state.create_surface(qh); + source.start_drag(device, &origin, Some(&wl_surface), serial); + sticky_exit_callback( + IcedSctkEvent::DndSurfaceCreated( + wl_surface.clone(), + icon_id, + origin_id) + , + &self.state, + &mut control_flow, + &mut callback + ); + Some((wl_surface, icon_native_id)) + } else { + source.start_drag(device, &origin, None, serial); + None + }; + self.state.dnd_source = Some(Dnd { origin_id, origin, source: Some((source, data)), icon_surface, pending_requests: Vec::new(), pipe: None, cur_write: None }); + }, + platform_specific::wayland::data_device::ActionInner::DndFinished => { + if let Some(offer) = self.state.dnd_offer.take() { + if offer.dropped { + offer.offer.finish(); + } + else { + self.state.dnd_offer = Some(offer); + } + } + }, + platform_specific::wayland::data_device::ActionInner::DndCancelled => { + if let Some(source) = self.state.dnd_source.as_mut() { + source.source = None; + } + }, + platform_specific::wayland::data_device::ActionInner::RequestDndData (mime_type) => { + if let Some(dnd_offer) = self.state.dnd_offer.as_mut() { + let read_pipe = match dnd_offer.offer.receive(mime_type.clone()) { + Ok(p) => p, + Err(_) => continue, // TODO error handling + }; + let loop_handle = self.event_loop.handle(); + match self.event_loop.handle().insert_source(read_pipe, move |_, f, state| { + let mut dnd_offer = match state.dnd_offer.take() { + Some(s) => s, + None => return PostAction::Continue, + }; + let (mime_type, data, token) = match dnd_offer.cur_read.take() { + Some(s) => s, + None => return PostAction::Continue, + }; + let mut reader = BufReader::new(f.as_ref()); + let consumed = match reader.fill_buf() { + Ok(buf) => { + if buf.is_empty() { + loop_handle.remove(token); + state.sctk_events.push(SctkEvent::DndOffer { event: DndOfferEvent::Data { data, mime_type }, surface: dnd_offer.offer.surface.clone() }); + if dnd_offer.dropped { + dnd_offer.offer.finish(); + } else { + state.dnd_offer = Some(dnd_offer); + } + } else { + let mut data = data; + data.extend_from_slice(buf); + dnd_offer.cur_read = Some((mime_type, data, token)); + state.dnd_offer = Some(dnd_offer); + } + buf.len() + }, + Err(e) if matches!(e.kind(), std::io::ErrorKind::Interrupted) => { + dnd_offer.cur_read = Some((mime_type, data, token)); + state.dnd_offer = Some(dnd_offer); + return PostAction::Continue; + }, + Err(e) => { + error!("Error reading selection data: {}", e); + if !dnd_offer.dropped { + state.dnd_offer = Some(dnd_offer); + } + return PostAction::Remove; + }, + }; + reader.consume(consumed); + PostAction::Continue + }) { + Ok(token) => { + dnd_offer.cur_read = Some((mime_type.clone(), Vec::new(), token)); + }, + Err(_) => continue, + }; + } + } + platform_specific::wayland::data_device::ActionInner::SetActions { preferred, accepted } => { + if let Some(offer) = self.state.dnd_offer.as_ref() { + offer.offer.set_actions(accepted, preferred); + } + } + } + }, + Event::Activation(activation_event) => match activation_event { + platform_specific::wayland::activation::Action::RequestToken { app_id, window, message } => { + if let Some(activation_state) = self.state.activation_state.as_ref() { + let (seat_and_serial, surface) = if let Some(id) = window { + let surface = self.state.windows.iter().find(|w| w.id == id) + .map(|w| w.window.wl_surface().clone()) + .or_else(|| self.state.layer_surfaces.iter().find(|l| l.id == id) + .map(|l| l.surface.wl_surface().clone()) + ); + let seat_and_serial = surface.as_ref().and_then(|surface| { + self.state.seats.first().and_then(|seat| if seat.kbd_focus.as_ref().map(|focus| focus == surface).unwrap_or(false) { + seat.last_kbd_press.as_ref().map(|(_, serial)| (seat.seat.clone(), *serial)) + } else if seat.ptr_focus.as_ref().map(|focus| focus == surface).unwrap_or(false) { + seat.last_ptr_press.as_ref().map(|(_, _, serial)| (seat.seat.clone(), *serial)) + } else { + None + }) + }); + + (seat_and_serial, surface) + } else { + (None, None) + }; + + activation_state.request_token_with_data(&self.state.queue_handle, IcedRequestData::new( + RequestData { + app_id, + seat_and_serial, + surface, + }, + message, + )); + } else { + // if we don't have the global, we don't want to stall the app + sticky_exit_callback( + IcedSctkEvent::UserEvent(message(None)), + &self.state, + &mut control_flow, + &mut callback, + ) + } + }, + platform_specific::wayland::activation::Action::Activate { window, token } => { + if let Some(activation_state) = self.state.activation_state.as_ref() { + if let Some(surface) = self.state.windows.iter().find(|w| w.id == window).map(|w| w.window.wl_surface()) { + activation_state.activate::>(surface, token) + } + } + }, + }, + Event::SessionLock(action) => match action { + platform_specific::wayland::session_lock::Action::Lock => { + if self.state.session_lock.is_none() { + // TODO send message on error? When protocol doesn't exist. + self.state.session_lock = self.state.session_lock_state.lock(&self.state.queue_handle).ok(); + } + } + platform_specific::wayland::session_lock::Action::Unlock => { + self.state.session_lock.take(); + // Make sure server processes unlock before client exits + let _ = self.state.connection.roundtrip(); + sticky_exit_callback( + IcedSctkEvent::SctkEvent(SctkEvent::SessionUnlocked), + &self.state, + &mut control_flow, + &mut callback, + ); + } + platform_specific::wayland::session_lock::Action::LockSurface { id, output, _phantom } => { + // TODO how to handle this when there's no lock? + if let Some(surface) = self.state.get_lock_surface(id, &output) { + sticky_exit_callback( + IcedSctkEvent::SctkEvent(SctkEvent::SessionLockSurfaceCreated {surface, native_id: id}), + &self.state, + &mut control_flow, + &mut callback, + ); + } + } + platform_specific::wayland::session_lock::Action::DestroyLockSurface { id } => { + if let Some(i) = + self.state.lock_surfaces.iter().position(|s| { + s.id == id + }) + { + self.state.lock_surfaces.remove(i); + } + } + } + } + } + + // Send events cleared. + sticky_exit_callback( + IcedSctkEvent::MainEventsCleared, + &self.state, + &mut control_flow, + &mut callback, + ); + + // redraw + pending_redraws.dedup(); + for id in pending_redraws { + sticky_exit_callback( + IcedSctkEvent::RedrawRequested(id.clone()), + &self.state, + &mut control_flow, + &mut callback, + ); + } + + // commit changes made via actions + for s in to_commit { + s.1.commit(); + } + + // Send RedrawEventCleared. + sticky_exit_callback( + IcedSctkEvent::RedrawEventsCleared, + &self.state, + &mut control_flow, + &mut callback, + ); + }; + + callback(IcedSctkEvent::LoopDestroyed, &self.state, &mut control_flow); + exit_code + } +} + +fn sticky_exit_callback( + evt: IcedSctkEvent, + target: &SctkState, + control_flow: &mut ControlFlow, + callback: &mut F, +) where + F: FnMut(IcedSctkEvent, &SctkState, &mut ControlFlow), +{ + // make ControlFlow::ExitWithCode sticky by providing a dummy + // control flow reference if it is already ExitWithCode. + if let ControlFlow::ExitWithCode(code) = *control_flow { + callback(evt, target, &mut ControlFlow::ExitWithCode(code)) + } else { + callback(evt, target, control_flow) + } +} + +fn raw_os_err(err: calloop::Error) -> i32 { + match err { + calloop::Error::IoError(err) => err.raw_os_error(), + _ => None, + } + .unwrap_or(1) +} diff --git a/sctk/src/event_loop/proxy.rs b/sctk/src/event_loop/proxy.rs new file mode 100644 index 0000000000..7140cef708 --- /dev/null +++ b/sctk/src/event_loop/proxy.rs @@ -0,0 +1,66 @@ +use iced_futures::futures::{ + channel::mpsc, + task::{Context, Poll}, + Sink, +}; +use sctk::reexports::calloop; +use std::pin::Pin; + +/// An event loop proxy that implements `Sink`. +#[derive(Debug)] +pub struct Proxy { + raw: calloop::channel::Sender, +} + +impl Clone for Proxy { + fn clone(&self) -> Self { + Self { + raw: self.raw.clone(), + } + } +} + +impl Proxy { + /// Creates a new [`Proxy`] from an `EventLoopProxy`. + pub fn new(raw: calloop::channel::Sender) -> Self { + Self { raw } + } + /// send an event + pub fn send_event(&self, message: Message) { + let _ = self.raw.send(message); + } +} + +impl Sink for Proxy { + type Error = mpsc::SendError; + + fn poll_ready( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + + fn start_send( + self: Pin<&mut Self>, + message: Message, + ) -> Result<(), Self::Error> { + let _ = self.raw.send(message); + + Ok(()) + } + + fn poll_flush( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_close( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } +} diff --git a/sctk/src/event_loop/state.rs b/sctk/src/event_loop/state.rs new file mode 100644 index 0000000000..be3fb9e7eb --- /dev/null +++ b/sctk/src/event_loop/state.rs @@ -0,0 +1,851 @@ +use std::{ + fmt::{Debug, Formatter}, + num::NonZeroU32, +}; + +use crate::{ + application::Event, + dpi::LogicalSize, + handlers::{ + wp_fractional_scaling::FractionalScalingManager, + wp_viewporter::ViewporterState, + }, + sctk_event::{ + LayerSurfaceEventVariant, PopupEventVariant, SctkEvent, + WindowEventVariant, + }, +}; + +use iced_runtime::{ + command::platform_specific::{ + self, + wayland::{ + data_device::DataFromMimeType, + layer_surface::{IcedMargin, IcedOutput, SctkLayerSurfaceSettings}, + popup::SctkPopupSettings, + window::SctkWindowSettings, + }, + }, + keyboard::Modifiers, + window, +}; +use sctk::{ + activation::ActivationState, + compositor::CompositorState, + data_device_manager::{ + data_device::DataDevice, data_offer::DragOffer, + data_source::DragSource, DataDeviceManagerState, WritePipe, + }, + error::GlobalError, + output::OutputState, + reexports::{ + calloop::{LoopHandle, RegistrationToken}, + client::{ + protocol::{ + wl_keyboard::WlKeyboard, + wl_output::WlOutput, + wl_seat::WlSeat, + wl_surface::{self, WlSurface}, + wl_touch::WlTouch, + }, + Connection, QueueHandle, + }, + }, + registry::RegistryState, + seat::{ + keyboard::KeyEvent, + pointer::{CursorIcon, ThemedPointer}, + SeatState, + }, + session_lock::{ + SessionLock, SessionLockState, SessionLockSurface, + SessionLockSurfaceConfigure, + }, + shell::{ + wlr_layer::{ + Anchor, KeyboardInteractivity, Layer, LayerShell, LayerSurface, + LayerSurfaceConfigure, + }, + xdg::{ + popup::{Popup, PopupConfigure}, + window::{Window, WindowConfigure, WindowDecorations}, + XdgPositioner, XdgShell, XdgSurface, + }, + WaylandSurface, + }, + shm::{multi::MultiPool, Shm}, +}; +use wayland_protocols::wp::{ + fractional_scale::v1::client::wp_fractional_scale_v1::WpFractionalScaleV1, + viewporter::client::wp_viewport::WpViewport, +}; + +#[derive(Debug)] +pub(crate) struct SctkSeat { + pub(crate) seat: WlSeat, + pub(crate) kbd: Option, + pub(crate) kbd_focus: Option, + pub(crate) last_kbd_press: Option<(KeyEvent, u32)>, + pub(crate) ptr: Option, + pub(crate) ptr_focus: Option, + pub(crate) last_ptr_press: Option<(u32, u32, u32)>, // (time, button, serial) + pub(crate) _touch: Option, + pub(crate) _modifiers: Modifiers, + pub(crate) data_device: DataDevice, + pub(crate) icon: Option, +} + +#[derive(Debug, Clone)] +pub struct SctkWindow { + pub(crate) id: window::Id, + pub(crate) window: Window, + pub(crate) scale_factor: Option, + pub(crate) requested_size: Option<(u32, u32)>, + pub(crate) current_size: Option<(NonZeroU32, NonZeroU32)>, + pub(crate) last_configure: Option, + pub(crate) resizable: Option, + /// Requests that SCTK window should perform. + pub(crate) _pending_requests: + Vec>, + pub(crate) wp_fractional_scale: Option, + pub(crate) wp_viewport: Option, +} + +impl SctkWindow { + pub(crate) fn set_size(&mut self, logical_size: LogicalSize) { + self.requested_size = + Some((logical_size.width.get(), logical_size.height.get())); + self.update_size(logical_size) + } + + pub(crate) fn update_size( + &mut self, + LogicalSize { width, height }: LogicalSize, + ) { + self.window.set_window_geometry( + 0, + 0, + width.get() as u32, + height.get() as u32, + ); + self.current_size = Some((width, height)); + // Update the target viewport, this is used if and only if fractional scaling is in use. + if let Some(viewport) = self.wp_viewport.as_ref() { + // Set inner size without the borders. + viewport.set_destination(width.get() as _, height.get() as _); + } + } +} + +#[derive(Debug, Clone)] +pub struct SctkLayerSurface { + pub(crate) id: window::Id, + pub(crate) surface: LayerSurface, + pub(crate) requested_size: (Option, Option), + pub(crate) current_size: Option>, + pub(crate) layer: Layer, + pub(crate) anchor: Anchor, + pub(crate) keyboard_interactivity: KeyboardInteractivity, + pub(crate) margin: IcedMargin, + pub(crate) exclusive_zone: i32, + pub(crate) last_configure: Option, + pub(crate) _pending_requests: + Vec>, + pub(crate) scale_factor: Option, + pub(crate) wp_fractional_scale: Option, + pub(crate) wp_viewport: Option, +} + +impl SctkLayerSurface { + pub(crate) fn set_size(&mut self, w: Option, h: Option) { + self.requested_size = (w, h); + + let (w, h) = (w.unwrap_or_default(), h.unwrap_or_default()); + self.surface.set_size(w, h); + } + + pub(crate) fn update_viewport(&mut self, w: u32, h: u32) { + self.current_size = Some(LogicalSize::new(w, h)); + if let Some(viewport) = self.wp_viewport.as_ref() { + // Set inner size without the borders. + viewport.set_destination(w as i32, h as i32); + } + } +} + +#[derive(Debug, Clone)] +pub enum SctkSurface { + LayerSurface(WlSurface), + Window(WlSurface), + Popup(WlSurface), +} + +impl SctkSurface { + pub fn wl_surface(&self) -> &WlSurface { + match self { + SctkSurface::LayerSurface(s) + | SctkSurface::Window(s) + | SctkSurface::Popup(s) => s, + } + } +} + +#[derive(Debug)] +pub struct SctkPopup { + pub(crate) popup: Popup, + pub(crate) last_configure: Option, + // pub(crate) positioner: XdgPositioner, + pub(crate) _pending_requests: + Vec>, + pub(crate) data: SctkPopupData, + pub(crate) scale_factor: Option, + pub(crate) wp_fractional_scale: Option, + pub(crate) wp_viewport: Option, +} + +impl SctkPopup { + pub(crate) fn set_size(&mut self, w: u32, h: u32, token: u32) { + // update geometry + self.popup + .xdg_surface() + .set_window_geometry(0, 0, w as i32, h as i32); + // update positioner + self.data.positioner.set_size(w as i32, h as i32); + self.popup.reposition(&self.data.positioner, token); + } +} + +#[derive(Debug)] +pub struct SctkLockSurface { + pub(crate) id: window::Id, + pub(crate) session_lock_surface: SessionLockSurface, + pub(crate) last_configure: Option, +} + +pub struct Dnd { + pub(crate) origin_id: window::Id, + pub(crate) origin: WlSurface, + pub(crate) source: Option<(DragSource, Box)>, + pub(crate) icon_surface: Option<(WlSurface, window::Id)>, + pub(crate) pending_requests: + Vec>, + pub(crate) pipe: Option, + pub(crate) cur_write: Option<(Vec, usize, RegistrationToken)>, +} + +impl Debug for Dnd { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("Dnd") + .field(&self.origin_id) + .field(&self.origin) + .field(&self.icon_surface) + .field(&self.pending_requests) + .field(&self.pipe) + .field(&self.cur_write) + .finish() + } +} + +#[derive(Debug)] +pub struct SctkDragOffer { + pub(crate) dropped: bool, + pub(crate) offer: DragOffer, + pub(crate) cur_read: Option<(String, Vec, RegistrationToken)>, +} + +#[derive(Debug)] +pub struct SctkPopupData { + pub(crate) id: window::Id, + pub(crate) parent: SctkSurface, + pub(crate) toplevel: WlSurface, + pub(crate) positioner: XdgPositioner, +} + +/// Wrapper to carry sctk state. +pub struct SctkState { + pub(crate) connection: Connection, + + /// the cursor wl_surface + pub(crate) _cursor_surface: Option, + /// a memory pool + pub(crate) _multipool: Option>, + + // all present outputs + pub(crate) outputs: Vec, + // though (for now) only one seat will be active in an iced application at a time, all ought to be tracked + // Active seat is the first seat in the list + pub(crate) seats: Vec, + // Windows / Surfaces + /// Window list containing all SCTK windows. Since those windows aren't allowed + /// to be sent to other threads, they live on the event loop's thread + /// and requests from winit's windows are being forwarded to them either via + /// `WindowUpdate` or buffer on the associated with it `WindowHandle`. + pub(crate) windows: Vec>, + pub(crate) layer_surfaces: Vec>, + pub(crate) popups: Vec>, + pub(crate) lock_surfaces: Vec, + pub(crate) dnd_source: Option>, + pub(crate) _kbd_focus: Option, + + /// Window updates, which are coming from SCTK or the compositor, which require + /// calling back to the sctk's downstream. They are handled right in the event loop, + /// unlike the ones coming from buffers on the `WindowHandle`'s. + pub compositor_updates: Vec, + + /// data data_device + pub(crate) dnd_offer: Option, + pub(crate) _accept_counter: u32, + /// A sink for window and device events that is being filled during dispatching + /// event loop and forwarded downstream afterwards. + pub(crate) sctk_events: Vec, + pub(crate) frame_events: Vec, + + /// pending user events + pub pending_user_events: Vec>, + + // handles + pub(crate) queue_handle: QueueHandle, + pub(crate) loop_handle: LoopHandle<'static, Self>, + + // sctk state objects + /// Viewporter state on the given window. + pub viewporter_state: Option>, + pub(crate) fractional_scaling_manager: Option>, + pub(crate) registry_state: RegistryState, + pub(crate) seat_state: SeatState, + pub(crate) output_state: OutputState, + pub(crate) compositor_state: CompositorState, + pub(crate) shm_state: Shm, + pub(crate) xdg_shell_state: XdgShell, + pub(crate) layer_shell: Option, + pub(crate) data_device_manager_state: DataDeviceManagerState, + pub(crate) activation_state: Option, + pub(crate) session_lock_state: SessionLockState, + pub(crate) session_lock: Option, + pub(crate) token_ctr: u32, +} + +/// An error that occurred while running an application. +#[derive(Debug, thiserror::Error)] +pub enum PopupCreationError { + /// Positioner creation failed + #[error("Positioner creation failed")] + PositionerCreationFailed(GlobalError), + + /// The specified parent is missing + #[error("The specified parent is missing")] + ParentMissing, + + /// The specified size is missing + #[error("The specified size is missing")] + SizeMissing, + + /// Popup creation failed + #[error("Popup creation failed")] + PopupCreationFailed(GlobalError), +} + +/// An error that occurred while running an application. +#[derive(Debug, thiserror::Error)] +pub enum LayerSurfaceCreationError { + /// Layer shell is not supported by the compositor + #[error("Layer shell is not supported by the compositor")] + LayerShellNotSupported, + + /// WlSurface creation failed + #[error("WlSurface creation failed")] + WlSurfaceCreationFailed(GlobalError), + + /// LayerSurface creation failed + #[error("Layer Surface creation failed")] + LayerSurfaceCreationFailed(GlobalError), +} + +/// An error that occurred while starting a drag and drop operation. +#[derive(Debug, thiserror::Error)] +pub enum DndStartError {} + +impl SctkState { + pub fn scale_factor_changed( + &mut self, + surface: &WlSurface, + scale_factor: f64, + legacy: bool, + ) { + if let Some(window) = self + .windows + .iter_mut() + .find(|w| w.window.wl_surface() == surface) + { + if legacy && window.wp_fractional_scale.is_some() { + return; + } + window.scale_factor = Some(scale_factor); + if legacy { + let _ = window.window.set_buffer_scale(scale_factor as u32); + } + self.compositor_updates.push(SctkEvent::WindowEvent { + variant: WindowEventVariant::ScaleFactorChanged( + scale_factor, + window.wp_viewport.clone(), + ), + id: window.window.wl_surface().clone(), + }); + } + + if let Some(popup) = self + .popups + .iter_mut() + .find(|p| p.popup.wl_surface() == surface) + { + if legacy && popup.wp_fractional_scale.is_some() { + return; + } + popup.scale_factor = Some(scale_factor); + if legacy { + let _ = popup + .popup + .wl_surface() + .set_buffer_scale(scale_factor as _); + } + self.compositor_updates.push(SctkEvent::PopupEvent { + variant: PopupEventVariant::ScaleFactorChanged( + scale_factor, + popup.wp_viewport.clone(), + ), + id: popup.popup.wl_surface().clone(), + toplevel_id: popup.data.toplevel.clone(), + parent_id: popup.data.parent.wl_surface().clone(), + }); + } + + if let Some(layer_surface) = self + .layer_surfaces + .iter_mut() + .find(|l| l.surface.wl_surface() == surface) + { + if legacy && layer_surface.wp_fractional_scale.is_some() { + return; + } + layer_surface.scale_factor = Some(scale_factor); + if legacy { + let _ = + layer_surface.surface.set_buffer_scale(scale_factor as u32); + } + self.compositor_updates.push(SctkEvent::LayerSurfaceEvent { + variant: LayerSurfaceEventVariant::ScaleFactorChanged( + scale_factor, + layer_surface.wp_viewport.clone(), + ), + id: layer_surface.surface.wl_surface().clone(), + }); + } + + // TODO winit sets cursor size after handling the change for the window, so maybe that should be done as well. + } +} + +impl SctkState +where + T: 'static + Debug, +{ + pub fn get_popup( + &mut self, + settings: SctkPopupSettings, + ) -> Result<(window::Id, WlSurface, WlSurface, WlSurface), PopupCreationError> + { + let (parent, toplevel) = if let Some(parent) = + self.layer_surfaces.iter().find(|l| l.id == settings.parent) + { + ( + SctkSurface::LayerSurface(parent.surface.wl_surface().clone()), + parent.surface.wl_surface().clone(), + ) + } else if let Some(parent) = + self.windows.iter().find(|w| w.id == settings.parent) + { + ( + SctkSurface::Window(parent.window.wl_surface().clone()), + parent.window.wl_surface().clone(), + ) + } else if let Some(i) = self + .popups + .iter() + .position(|p| p.data.id == settings.parent) + { + let parent = &self.popups[i]; + ( + SctkSurface::Popup(parent.popup.wl_surface().clone()), + parent.data.toplevel.clone(), + ) + } else { + return Err(PopupCreationError::ParentMissing); + }; + + let size = if settings.positioner.size.is_none() { + return Err(PopupCreationError::SizeMissing); + } else { + settings.positioner.size.unwrap() + }; + + let positioner = XdgPositioner::new(&self.xdg_shell_state) + .map_err(PopupCreationError::PositionerCreationFailed)?; + positioner.set_anchor(settings.positioner.anchor); + positioner.set_anchor_rect( + settings.positioner.anchor_rect.x, + settings.positioner.anchor_rect.y, + settings.positioner.anchor_rect.width, + settings.positioner.anchor_rect.height, + ); + positioner.set_constraint_adjustment( + settings.positioner.constraint_adjustment, + ); + positioner.set_gravity(settings.positioner.gravity); + positioner.set_offset( + settings.positioner.offset.0, + settings.positioner.offset.1, + ); + if settings.positioner.reactive { + positioner.set_reactive(); + } + positioner.set_size(size.0 as i32, size.1 as i32); + + let grab = settings.grab; + + let wl_surface = + self.compositor_state.create_surface(&self.queue_handle); + + let (toplevel, popup) = match &parent { + SctkSurface::LayerSurface(parent) => { + let parent_layer_surface = self + .layer_surfaces + .iter() + .find(|w| w.surface.wl_surface() == parent) + .unwrap(); + let popup = Popup::from_surface( + None, + &positioner, + &self.queue_handle, + wl_surface.clone(), + &self.xdg_shell_state, + ) + .map_err(PopupCreationError::PopupCreationFailed)?; + parent_layer_surface.surface.get_popup(popup.xdg_popup()); + (parent_layer_surface.surface.wl_surface(), popup) + } + SctkSurface::Window(parent) => { + let parent_window = self + .windows + .iter() + .find(|w| w.window.wl_surface() == parent) + .unwrap(); + ( + parent_window.window.wl_surface(), + Popup::from_surface( + Some(parent_window.window.xdg_surface()), + &positioner, + &self.queue_handle, + wl_surface.clone(), + &self.xdg_shell_state, + ) + .map_err(PopupCreationError::PopupCreationFailed)?, + ) + } + SctkSurface::Popup(parent) => { + let parent_xdg = self + .windows + .iter() + .find_map(|w| { + if w.window.wl_surface() == parent { + Some(w.window.xdg_surface()) + } else { + None + } + }) + .unwrap(); + + ( + &toplevel, + Popup::from_surface( + Some(parent_xdg), + &positioner, + &self.queue_handle, + wl_surface.clone(), + &self.xdg_shell_state, + ) + .map_err(PopupCreationError::PopupCreationFailed)?, + ) + } + }; + if grab { + if let Some(s) = self.seats.first() { + popup.xdg_popup().grab( + &s.seat, + s.last_ptr_press.map(|p| p.2).unwrap_or_else(|| { + s.last_kbd_press + .as_ref() + .map(|p| p.1) + .unwrap_or_default() + }), + ) + } + } + wl_surface.commit(); + + let wp_viewport = self.viewporter_state.as_ref().map(|state| { + let viewport = + state.get_viewport(popup.wl_surface(), &self.queue_handle); + viewport.set_destination(size.0 as i32, size.1 as i32); + viewport + }); + let wp_fractional_scale = + self.fractional_scaling_manager.as_ref().map(|fsm| { + fsm.fractional_scaling(popup.wl_surface(), &self.queue_handle) + }); + + self.popups.push(SctkPopup { + popup: popup.clone(), + data: SctkPopupData { + id: settings.id, + parent: parent.clone(), + toplevel: toplevel.clone(), + positioner, + }, + last_configure: None, + _pending_requests: Default::default(), + wp_viewport, + wp_fractional_scale, + scale_factor: None, + }); + + Ok(( + settings.id, + parent.wl_surface().clone(), + toplevel.clone(), + popup.wl_surface().clone(), + )) + } + + pub fn get_window( + &mut self, + settings: SctkWindowSettings, + ) -> (window::Id, WlSurface) { + let SctkWindowSettings { + size, + client_decorations, + + window_id, + app_id, + title, + + size_limits, + resizable, + xdg_activation_token, + .. + } = settings; + // TODO Ashley: set window as opaque if transparency is false + // TODO Ashley: set icon + // TODO Ashley: save settings for window + // TODO Ashley: decorations + let wl_surface = + self.compositor_state.create_surface(&self.queue_handle); + let decorations: WindowDecorations = if client_decorations { + WindowDecorations::RequestClient + } else { + WindowDecorations::RequestServer + }; + let window = self.xdg_shell_state.create_window( + wl_surface.clone(), + decorations, + &self.queue_handle, + ); + if let Some(app_id) = app_id { + window.set_app_id(app_id); + } + // TODO better way of handling size limits + let min_size = size_limits.min(); + let min_size = if min_size.width as i32 <= 0 + || min_size.height as i32 <= 0 + || min_size.width > u16::MAX as f32 + || min_size.height > u16::MAX as f32 + { + None + } else { + Some((min_size.width as u32, min_size.height as u32)) + }; + let max_size: iced_futures::core::Size = size_limits.max(); + let max_size = if max_size.width as i32 <= 0 + || max_size.height as i32 <= 0 + || max_size.width > u16::MAX as f32 + || max_size.height > u16::MAX as f32 + { + None + } else { + Some((max_size.width as u32, max_size.height as u32)) + }; + if min_size.is_some() { + window.set_min_size(min_size); + } + if max_size.is_some() { + window.set_max_size(max_size); + } + + if let Some(title) = title { + window.set_title(title); + } + // if let Some(parent) = parent.and_then(|p| self.windows.iter().find(|w| w.window.wl_surface().id() == p)) { + // window.set_parent(Some(&parent.window)); + // } + window.xdg_surface().set_window_geometry( + 0, + 0, + size.0 as i32, + size.1 as i32, + ); + + window.commit(); + + if let (Some(token), Some(activation_state)) = + (xdg_activation_token, self.activation_state.as_ref()) + { + activation_state.activate::<()>(window.wl_surface(), token); + } + + let wp_viewport = self.viewporter_state.as_ref().map(|state| { + state.get_viewport(window.wl_surface(), &self.queue_handle) + }); + let wp_fractional_scale = + self.fractional_scaling_manager.as_ref().map(|fsm| { + fsm.fractional_scaling(window.wl_surface(), &self.queue_handle) + }); + + self.windows.push(SctkWindow { + id: window_id, + window, + scale_factor: None, + requested_size: Some(size), + current_size: Some(( + NonZeroU32::new(1).unwrap(), + NonZeroU32::new(1).unwrap(), + )), + last_configure: None, + _pending_requests: Vec::new(), + resizable, + wp_viewport, + wp_fractional_scale, + }); + (window_id, wl_surface) + } + + pub fn get_layer_surface( + &mut self, + SctkLayerSurfaceSettings { + id, + layer, + keyboard_interactivity, + pointer_interactivity, + anchor, + output, + namespace, + margin, + size, + exclusive_zone, + .. + }: SctkLayerSurfaceSettings, + ) -> Result<(window::Id, WlSurface), LayerSurfaceCreationError> { + let wl_output = match output { + IcedOutput::All => None, // TODO + IcedOutput::Active => None, + IcedOutput::Output(output) => Some(output), + }; + + let layer_shell = self + .layer_shell + .as_ref() + .ok_or(LayerSurfaceCreationError::LayerShellNotSupported)?; + let wl_surface = + self.compositor_state.create_surface(&self.queue_handle); + let mut size = size.unwrap(); + if anchor.contains(Anchor::BOTTOM.union(Anchor::TOP)) { + size.1 = None; + } + if anchor.contains(Anchor::LEFT.union(Anchor::RIGHT)) { + size.0 = None; + } + let layer_surface = layer_shell.create_layer_surface( + &self.queue_handle, + wl_surface.clone(), + layer, + Some(namespace), + wl_output.as_ref(), + ); + layer_surface.set_anchor(anchor); + layer_surface.set_keyboard_interactivity(keyboard_interactivity); + layer_surface.set_margin( + margin.top, + margin.right, + margin.bottom, + margin.left, + ); + layer_surface + .set_size(size.0.unwrap_or_default(), size.1.unwrap_or_default()); + layer_surface.set_exclusive_zone(exclusive_zone); + if !pointer_interactivity { + layer_surface.set_input_region(None); + } + layer_surface.commit(); + + let wp_viewport = self.viewporter_state.as_ref().map(|state| { + state.get_viewport(layer_surface.wl_surface(), &self.queue_handle) + }); + let wp_fractional_scale = + self.fractional_scaling_manager.as_ref().map(|fsm| { + fsm.fractional_scaling( + layer_surface.wl_surface(), + &self.queue_handle, + ) + }); + + self.layer_surfaces.push(SctkLayerSurface { + id, + surface: layer_surface, + requested_size: size, + current_size: None, + layer, + // builder needs to be refactored such that these fields are accessible + anchor, + keyboard_interactivity, + margin, + exclusive_zone, + last_configure: None, + _pending_requests: Vec::new(), + wp_viewport, + wp_fractional_scale, + scale_factor: None, + }); + Ok((id, wl_surface)) + } + pub fn get_lock_surface( + &mut self, + id: window::Id, + output: &WlOutput, + ) -> Option { + if let Some(lock) = self.session_lock.as_ref() { + let wl_surface = + self.compositor_state.create_surface(&self.queue_handle); + let session_lock_surface = lock.create_lock_surface( + wl_surface.clone(), + output, + &self.queue_handle, + ); + self.lock_surfaces.push(SctkLockSurface { + id, + session_lock_surface, + last_configure: None, + }); + Some(wl_surface) + } else { + None + } + } +} diff --git a/sctk/src/handlers/activation.rs b/sctk/src/handlers/activation.rs new file mode 100644 index 0000000000..e310d1a979 --- /dev/null +++ b/sctk/src/handlers/activation.rs @@ -0,0 +1,60 @@ +use std::sync::Mutex; + +use sctk::{ + activation::{ActivationHandler, RequestData, RequestDataExt}, + delegate_activation, + reexports::client::protocol::{wl_seat::WlSeat, wl_surface::WlSurface}, +}; + +use crate::event_loop::state::SctkState; + +pub struct IcedRequestData { + data: RequestData, + message: Mutex< + Option) -> T + Send + Sync + 'static>>, + >, +} + +impl IcedRequestData { + pub fn new( + data: RequestData, + message: Box) -> T + Send + Sync + 'static>, + ) -> IcedRequestData { + IcedRequestData { + data, + message: Mutex::new(Some(message)), + } + } +} + +impl RequestDataExt for IcedRequestData { + fn app_id(&self) -> Option<&str> { + self.data.app_id() + } + + fn seat_and_serial(&self) -> Option<(&WlSeat, u32)> { + self.data.seat_and_serial() + } + + fn surface(&self) -> Option<&WlSurface> { + self.data.surface() + } +} + +impl ActivationHandler for SctkState { + type RequestData = IcedRequestData; + + fn new_token(&mut self, token: String, data: &Self::RequestData) { + if let Some(message) = data.message.lock().unwrap().take() { + self.pending_user_events.push( + crate::application::Event::SctkEvent( + crate::sctk_event::IcedSctkEvent::UserEvent(message(Some( + token, + ))), + ), + ); + } // else the compositor send two tokens??? + } +} + +delegate_activation!(@ SctkState, IcedRequestData); diff --git a/sctk/src/handlers/compositor.rs b/sctk/src/handlers/compositor.rs new file mode 100644 index 0000000000..3e77b21dfc --- /dev/null +++ b/sctk/src/handlers/compositor.rs @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MPL-2.0-only +use sctk::{ + compositor::CompositorHandler, + delegate_compositor, + reexports::client::{protocol::wl_surface, Connection, Proxy, QueueHandle}, + shell::WaylandSurface, +}; +use std::fmt::Debug; + +use crate::{event_loop::state::SctkState, sctk_event::SctkEvent}; + +impl CompositorHandler for SctkState { + fn scale_factor_changed( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + surface: &wl_surface::WlSurface, + new_factor: i32, + ) { + self.scale_factor_changed(surface, new_factor as f64, true); + } + + fn frame( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + surface: &wl_surface::WlSurface, + _time: u32, + ) { + self.frame_events.push(surface.clone()); + } + + fn transform_changed( + &mut self, + conn: &Connection, + qh: &QueueHandle, + surface: &wl_surface::WlSurface, + new_transform: sctk::reexports::client::protocol::wl_output::Transform, + ) { + // TODO + // this is not required + } +} + +delegate_compositor!(@ SctkState); diff --git a/sctk/src/handlers/data_device/data_device.rs b/sctk/src/handlers/data_device/data_device.rs new file mode 100644 index 0000000000..0adf03c354 --- /dev/null +++ b/sctk/src/handlers/data_device/data_device.rs @@ -0,0 +1,140 @@ +use std::fmt::Debug; + +use sctk::{ + data_device_manager::{ + data_device::{DataDevice, DataDeviceHandler}, + data_offer::DragOffer, + }, + reexports::client::{protocol::wl_data_device, Connection, QueueHandle}, +}; + +use crate::{ + event_loop::state::{SctkDragOffer, SctkState}, + sctk_event::{DndOfferEvent, SctkEvent}, +}; + +impl DataDeviceHandler for SctkState { + fn enter( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + wl_data_device: &wl_data_device::WlDataDevice, + ) { + let data_device = if let Some(seat) = self + .seats + .iter() + .find(|s| s.data_device.inner() == wl_data_device) + { + &seat.data_device + } else { + return; + }; + + let drag_offer = data_device.data().drag_offer().unwrap(); + let mime_types = drag_offer.with_mime_types(|types| types.to_vec()); + self.dnd_offer = Some(SctkDragOffer { + dropped: false, + offer: drag_offer.clone(), + cur_read: None, + }); + self.sctk_events.push(SctkEvent::DndOffer { + event: DndOfferEvent::Enter { + mime_types, + x: drag_offer.x, + y: drag_offer.y, + }, + surface: drag_offer.surface.clone(), + }); + } + + fn leave( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _wl_data_device: &wl_data_device::WlDataDevice, + ) { + // ASHLEY TODO the dnd_offer should be removed when the leave event is received + // but for now it is not if the offer was previously dropped. + // It seems that leave events are received even for offers which have + // been accepted and need to be read. + if let Some(dnd_offer) = self.dnd_offer.take() { + if dnd_offer.dropped { + self.dnd_offer = Some(dnd_offer); + return; + } + + self.sctk_events.push(SctkEvent::DndOffer { + event: DndOfferEvent::Leave, + surface: dnd_offer.offer.surface.clone(), + }); + } + } + + fn motion( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + wl_data_device: &wl_data_device::WlDataDevice, + ) { + let data_device = if let Some(seat) = self + .seats + .iter() + .find(|s| s.data_device.inner() == wl_data_device) + { + &seat.data_device + } else { + return; + }; + + let offer = data_device.data().drag_offer(); + // if the offer is not the same as the current one, ignore the leave event + if offer.as_ref() != self.dnd_offer.as_ref().map(|o| &o.offer) { + return; + } + let DragOffer { x, y, surface, .. } = + data_device.data().drag_offer().unwrap(); + self.sctk_events.push(SctkEvent::DndOffer { + event: DndOfferEvent::Motion { x, y }, + surface: surface.clone(), + }); + } + + fn selection( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _wl_data_device: &wl_data_device::WlDataDevice, + ) { + // not handled here + } + + fn drop_performed( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + wl_data_device: &wl_data_device::WlDataDevice, + ) { + let data_device = if let Some(seat) = self + .seats + .iter() + .find(|s| s.data_device.inner() == wl_data_device) + { + &seat.data_device + } else { + return; + }; + + if let Some(offer) = data_device.data().drag_offer() { + if let Some(dnd_offer) = self.dnd_offer.as_mut() { + if offer != dnd_offer.offer { + return; + } + dnd_offer.dropped = true; + } + self.sctk_events.push(SctkEvent::DndOffer { + event: DndOfferEvent::DropPerformed, + surface: offer.surface.clone(), + }); + } + } +} diff --git a/sctk/src/handlers/data_device/data_offer.rs b/sctk/src/handlers/data_device/data_offer.rs new file mode 100644 index 0000000000..b56f5810bd --- /dev/null +++ b/sctk/src/handlers/data_device/data_offer.rs @@ -0,0 +1,57 @@ +use sctk::{ + data_device_manager::data_offer::{DataOfferHandler, DragOffer}, + reexports::client::{ + protocol::wl_data_device_manager::DndAction, Connection, QueueHandle, + }, +}; +use std::fmt::Debug; + +use crate::event_loop::state::SctkState; + +impl DataOfferHandler for SctkState { + fn source_actions( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + offer: &mut DragOffer, + actions: DndAction, + ) { + if self + .dnd_offer + .as_ref() + .map(|o| o.offer.inner() == offer.inner()) + .unwrap_or(false) + { + self.sctk_events + .push(crate::sctk_event::SctkEvent::DndOffer { + event: crate::sctk_event::DndOfferEvent::SourceActions( + actions, + ), + surface: offer.surface.clone(), + }); + } + } + + fn selected_action( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + offer: &mut DragOffer, + actions: DndAction, + ) { + if self + .dnd_offer + .as_ref() + .map(|o| o.offer.inner() == offer.inner()) + .unwrap_or(false) + { + self.sctk_events + .push(crate::sctk_event::SctkEvent::DndOffer { + event: crate::sctk_event::DndOfferEvent::SelectedAction( + actions, + ), + surface: offer.surface.clone(), + }); + } + } +} diff --git a/sctk/src/handlers/data_device/data_source.rs b/sctk/src/handlers/data_device/data_source.rs new file mode 100644 index 0000000000..834cccc483 --- /dev/null +++ b/sctk/src/handlers/data_device/data_source.rs @@ -0,0 +1,200 @@ +use crate::event_loop::state::SctkState; +use crate::sctk_event::{DataSourceEvent, SctkEvent}; +use sctk::data_device_manager::WritePipe; +use sctk::{ + data_device_manager::data_source::DataSourceHandler, + reexports::{ + calloop::PostAction, + client::{ + protocol::{ + wl_data_device_manager::DndAction, wl_data_source::WlDataSource, + }, + Connection, QueueHandle, + }, + }, +}; +use std::io::{BufWriter, Write}; +use tracing::error; + +impl DataSourceHandler for SctkState { + fn accept_mime( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + source: &WlDataSource, + mime: Option, + ) { + let is_active_source = self + .dnd_source + .as_ref() + .and_then(|s| (s.source.as_ref().map(|s| s.0.inner() == source))) + .unwrap_or(false); + if is_active_source { + self.sctk_events.push(SctkEvent::DataSource( + DataSourceEvent::MimeAccepted(mime), + )); + } + } + + fn send_request( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + source: &WlDataSource, + mime: String, + pipe: WritePipe, + ) { + let is_active_source = self + .dnd_source + .as_ref() + .and_then(|s| s.source.as_ref().map(|s| s.0.inner() == source)) + .unwrap_or(false); + + if !is_active_source { + source.destroy(); + return; + } + + if let Some(source) = self.dnd_source.as_mut().filter(|s| { + s.source + .as_ref() + .map(|s| (s.0.inner() == source)) + .unwrap_or(false) + }) { + let (_my_source, data) = match source.source.as_ref() { + Some((source, data)) => (source, data), + None => return, + }; + match self.loop_handle.insert_source( + pipe, + move |_, f, state| -> PostAction { + let loop_handle = &state.loop_handle; + let dnd_source = match state.dnd_source.as_mut() { + Some(s) => s, + None => return PostAction::Continue, + }; + let (data, mut cur_index, token) = + match dnd_source.cur_write.take() { + Some(s) => s, + None => return PostAction::Continue, + }; + let mut writer = BufWriter::new(f.as_ref()); + let slice = &data.as_slice()[cur_index + ..(cur_index + writer.capacity()).min(data.len())]; + match writer.write(slice) { + Ok(num_written) => { + cur_index += num_written; + if cur_index == data.len() { + loop_handle.remove(token); + } else { + dnd_source.cur_write = + Some((data, cur_index, token)); + } + if let Err(err) = writer.flush() { + loop_handle.remove(token); + error!("Failed to flush pipe: {}", err); + } + } + Err(e) + if matches!( + e.kind(), + std::io::ErrorKind::Interrupted + ) => + { + // try again + dnd_source.cur_write = + Some((data, cur_index, token)); + } + Err(_) => { + loop_handle.remove(token); + error!("Failed to write to pipe"); + } + }; + PostAction::Continue + }, + ) { + Ok(s) => { + source.cur_write = Some(( + data.from_mime_type(&mime).unwrap_or_default(), + 0, + s, + )); + } + Err(_) => { + error!("Failed to insert source"); + } + }; + } + } + + fn cancelled( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + source: &WlDataSource, + ) { + let is_active_source = self + .dnd_source + .as_ref() + .and_then(|s| s.source.as_ref().map(|s| s.0.inner() == source)) + .unwrap_or(false); + if is_active_source { + self.sctk_events + .push(SctkEvent::DataSource(DataSourceEvent::DndCancelled)); + } + } + + fn dnd_dropped( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + source: &WlDataSource, + ) { + let is_active_source = self + .dnd_source + .as_ref() + .and_then(|s| (s.source.as_ref().map(|s| s.0.inner() == source))) + .unwrap_or(false); + if is_active_source { + self.sctk_events + .push(SctkEvent::DataSource(DataSourceEvent::DndDropPerformed)); + } + } + + fn dnd_finished( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + source: &WlDataSource, + ) { + let is_active_source = self + .dnd_source + .as_ref() + .and_then(|s| (s.source.as_ref().map(|s| s.0.inner() == source))) + .unwrap_or(false); + if is_active_source { + self.sctk_events + .push(SctkEvent::DataSource(DataSourceEvent::DndFinished)); + } + } + + fn action( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + source: &WlDataSource, + action: DndAction, + ) { + let is_active_source = self + .dnd_source + .as_ref() + .and_then(|s| (s.source.as_ref().map(|s| s.0.inner() == source))) + .unwrap_or(false); + if is_active_source { + self.sctk_events + .push(crate::sctk_event::SctkEvent::DataSource( + DataSourceEvent::DndActionAccepted(action), + )); + } + } +} diff --git a/sctk/src/handlers/data_device/mod.rs b/sctk/src/handlers/data_device/mod.rs new file mode 100644 index 0000000000..f0f2d482e9 --- /dev/null +++ b/sctk/src/handlers/data_device/mod.rs @@ -0,0 +1,9 @@ +use crate::handlers::SctkState; +use sctk::delegate_data_device; +use std::fmt::Debug; + +pub mod data_device; +pub mod data_offer; +pub mod data_source; + +delegate_data_device!(@ SctkState); diff --git a/sctk/src/handlers/mod.rs b/sctk/src/handlers/mod.rs new file mode 100644 index 0000000000..332d296682 --- /dev/null +++ b/sctk/src/handlers/mod.rs @@ -0,0 +1,41 @@ +// handlers +pub mod activation; +pub mod compositor; +pub mod data_device; +pub mod output; +pub mod seat; +pub mod session_lock; +pub mod shell; +pub mod wp_fractional_scaling; +pub mod wp_viewporter; + +use sctk::{ + delegate_registry, delegate_shm, + output::OutputState, + registry::{ProvidesRegistryState, RegistryState}, + registry_handlers, + seat::SeatState, + shm::{Shm, ShmHandler}, +}; +use std::fmt::Debug; + +use crate::event_loop::state::SctkState; + +impl ShmHandler for SctkState { + fn shm_state(&mut self) -> &mut Shm { + &mut self.shm_state + } +} + +impl ProvidesRegistryState for SctkState +where + T: 'static, +{ + fn registry(&mut self) -> &mut RegistryState { + &mut self.registry_state + } + registry_handlers![OutputState, SeatState,]; +} + +delegate_shm!(@ SctkState); +delegate_registry!(@ SctkState); diff --git a/sctk/src/handlers/output.rs b/sctk/src/handlers/output.rs new file mode 100644 index 0000000000..f0725c08cc --- /dev/null +++ b/sctk/src/handlers/output.rs @@ -0,0 +1,48 @@ +use crate::{event_loop::state::SctkState, sctk_event::SctkEvent}; +use sctk::{delegate_output, output::OutputHandler}; +use std::fmt::Debug; + +impl OutputHandler for SctkState { + fn output_state(&mut self) -> &mut sctk::output::OutputState { + &mut self.output_state + } + + fn new_output( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + output: sctk::reexports::client::protocol::wl_output::WlOutput, + ) { + self.sctk_events.push(SctkEvent::NewOutput { + id: output.clone(), + info: self.output_state.info(&output), + }); + self.outputs.push(output); + } + + fn update_output( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + output: sctk::reexports::client::protocol::wl_output::WlOutput, + ) { + if let Some(info) = self.output_state.info(&output) { + self.sctk_events.push(SctkEvent::UpdateOutput { + id: output.clone(), + info, + }); + } + } + + fn output_destroyed( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + output: sctk::reexports::client::protocol::wl_output::WlOutput, + ) { + self.sctk_events.push(SctkEvent::RemovedOutput(output)); + // TODO clean up any layer surfaces on this output? + } +} + +delegate_output!(@ SctkState); diff --git a/sctk/src/handlers/seat/keyboard.rs b/sctk/src/handlers/seat/keyboard.rs new file mode 100644 index 0000000000..c150c077bf --- /dev/null +++ b/sctk/src/handlers/seat/keyboard.rs @@ -0,0 +1,200 @@ +use crate::{ + event_loop::state::SctkState, + sctk_event::{KeyboardEventVariant, SctkEvent}, +}; + +use sctk::{ + delegate_keyboard, + seat::keyboard::{KeyboardHandler, Keysym}, +}; +use std::fmt::Debug; + +impl KeyboardHandler for SctkState { + fn enter( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + keyboard: &sctk::reexports::client::protocol::wl_keyboard::WlKeyboard, + surface: &sctk::reexports::client::protocol::wl_surface::WlSurface, + _serial: u32, + _raw: &[u32], + _keysyms: &[Keysym], + ) { + let (i, mut is_active, seat) = { + let (i, is_active, my_seat) = + match self.seats.iter_mut().enumerate().find_map(|(i, s)| { + if s.kbd.as_ref() == Some(keyboard) { + Some((i, s)) + } else { + None + } + }) { + Some((i, s)) => (i, i == 0, s), + None => return, + }; + my_seat.kbd_focus.replace(surface.clone()); + + let seat = my_seat.seat.clone(); + (i, is_active, seat) + }; + + // TODO Ashley: thoroughly test this + // swap the active seat to be the current seat if the current "active" seat is not focused on the application anyway + if !is_active && self.seats[0].kbd_focus.is_none() { + is_active = true; + self.seats.swap(0, i); + } + + if is_active { + self.sctk_events.push(SctkEvent::KeyboardEvent { + variant: KeyboardEventVariant::Enter(surface.clone()), + kbd_id: keyboard.clone(), + seat_id: seat, + }) + } + } + + fn leave( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + keyboard: &sctk::reexports::client::protocol::wl_keyboard::WlKeyboard, + surface: &sctk::reexports::client::protocol::wl_surface::WlSurface, + _serial: u32, + ) { + let (is_active, seat, kbd) = { + let (is_active, my_seat) = + match self.seats.iter_mut().enumerate().find_map(|(i, s)| { + if s.kbd.as_ref() == Some(keyboard) { + Some((i, s)) + } else { + None + } + }) { + Some((i, s)) => (i == 0, s), + None => return, + }; + let seat = my_seat.seat.clone(); + let kbd = keyboard.clone(); + my_seat.kbd_focus.take(); + (is_active, seat, kbd) + }; + + if is_active { + self.sctk_events.push(SctkEvent::KeyboardEvent { + variant: KeyboardEventVariant::Leave(surface.clone()), + kbd_id: kbd, + seat_id: seat, + }); + // if there is another seat with a keyboard focused on a surface make that the new active seat + if let Some(i) = + self.seats.iter().position(|s| s.kbd_focus.is_some()) + { + self.seats.swap(0, i); + let s = &self.seats[0]; + self.sctk_events.push(SctkEvent::KeyboardEvent { + variant: KeyboardEventVariant::Enter( + s.kbd_focus.clone().unwrap(), + ), + kbd_id: s.kbd.clone().unwrap(), + seat_id: s.seat.clone(), + }) + } + } + } + + fn press_key( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + keyboard: &sctk::reexports::client::protocol::wl_keyboard::WlKeyboard, + serial: u32, + event: sctk::seat::keyboard::KeyEvent, + ) { + let (is_active, my_seat) = + match self.seats.iter_mut().enumerate().find_map(|(i, s)| { + if s.kbd.as_ref() == Some(keyboard) { + Some((i, s)) + } else { + None + } + }) { + Some((i, s)) => (i == 0, s), + None => return, + }; + let seat_id = my_seat.seat.clone(); + let kbd_id = keyboard.clone(); + my_seat.last_kbd_press.replace((event.clone(), serial)); + if is_active { + self.sctk_events.push(SctkEvent::KeyboardEvent { + variant: KeyboardEventVariant::Press(event), + kbd_id, + seat_id, + }); + } + } + + fn release_key( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + keyboard: &sctk::reexports::client::protocol::wl_keyboard::WlKeyboard, + _serial: u32, + event: sctk::seat::keyboard::KeyEvent, + ) { + let (is_active, my_seat) = + match self.seats.iter_mut().enumerate().find_map(|(i, s)| { + if s.kbd.as_ref() == Some(keyboard) { + Some((i, s)) + } else { + None + } + }) { + Some((i, s)) => (i == 0, s), + None => return, + }; + let seat_id = my_seat.seat.clone(); + let kbd_id = keyboard.clone(); + + if is_active { + self.sctk_events.push(SctkEvent::KeyboardEvent { + variant: KeyboardEventVariant::Release(event), + kbd_id, + seat_id, + }); + } + } + + fn update_modifiers( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + keyboard: &sctk::reexports::client::protocol::wl_keyboard::WlKeyboard, + _serial: u32, + modifiers: sctk::seat::keyboard::Modifiers, + ) { + let (is_active, my_seat) = + match self.seats.iter_mut().enumerate().find_map(|(i, s)| { + if s.kbd.as_ref() == Some(keyboard) { + Some((i, s)) + } else { + None + } + }) { + Some((i, s)) => (i == 0, s), + None => return, + }; + let seat_id = my_seat.seat.clone(); + let kbd_id = keyboard.clone(); + + if is_active { + self.sctk_events.push(SctkEvent::KeyboardEvent { + variant: KeyboardEventVariant::Modifiers(modifiers), + kbd_id, + seat_id, + }) + } + } +} + +delegate_keyboard!(@ SctkState); diff --git a/sctk/src/handlers/seat/mod.rs b/sctk/src/handlers/seat/mod.rs new file mode 100644 index 0000000000..38369b437b --- /dev/null +++ b/sctk/src/handlers/seat/mod.rs @@ -0,0 +1,5 @@ +// TODO support multi-seat handling +pub mod keyboard; +pub mod pointer; +pub mod seat; +pub mod touch; diff --git a/sctk/src/handlers/seat/pointer.rs b/sctk/src/handlers/seat/pointer.rs new file mode 100644 index 0000000000..9777a320e4 --- /dev/null +++ b/sctk/src/handlers/seat/pointer.rs @@ -0,0 +1,163 @@ +use crate::{event_loop::state::SctkState, sctk_event::SctkEvent}; +use sctk::{ + delegate_pointer, + reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge, + seat::pointer::{CursorIcon, PointerEventKind, PointerHandler, BTN_LEFT}, + shell::WaylandSurface, +}; +use std::fmt::Debug; + +impl PointerHandler for SctkState { + fn pointer_frame( + &mut self, + conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + pointer: &sctk::reexports::client::protocol::wl_pointer::WlPointer, + events: &[sctk::seat::pointer::PointerEvent], + ) { + let (is_active, my_seat) = + match self.seats.iter_mut().enumerate().find_map(|(i, s)| { + if s.ptr.as_ref().map(|p| p.pointer()) == Some(pointer) { + Some((i, s)) + } else { + None + } + }) { + Some((i, s)) => (i == 0, s), + None => return, + }; + + // track events, but only forward for the active seat + for e in events { + // check if it is over a resizable window's border and handle the event yourself if it is. + if let Some((resize_edge, window)) = self + .windows + .iter() + .find(|w| w.window.wl_surface() == &e.surface) + .and_then(|w| { + w.resizable.zip(w.current_size).and_then( + |(border, (width, height))| { + let (width, height) = + (width.get() as f64, height.get() as f64); + let (x, y) = e.position; + let left_edge = x < border; + let top_edge = y < border; + let right_edge = x > width - border; + let bottom_edge = y > height - border; + + if left_edge && top_edge { + Some((ResizeEdge::TopLeft, w)) + } else if left_edge && bottom_edge { + Some((ResizeEdge::BottomLeft, w)) + } else if right_edge && top_edge { + Some((ResizeEdge::TopRight, w)) + } else if right_edge && bottom_edge { + Some((ResizeEdge::BottomRight, w)) + } else if left_edge { + Some((ResizeEdge::Left, w)) + } else if right_edge { + Some((ResizeEdge::Right, w)) + } else if top_edge { + Some((ResizeEdge::Top, w)) + } else if bottom_edge { + Some((ResizeEdge::Bottom, w)) + } else { + None + } + }, + ) + }) + { + let icon = match resize_edge { + ResizeEdge::Top => CursorIcon::NResize, + ResizeEdge::Bottom => CursorIcon::SResize, + ResizeEdge::Left => CursorIcon::WResize, + ResizeEdge::TopLeft => CursorIcon::NwResize, + ResizeEdge::BottomLeft => CursorIcon::SwResize, + ResizeEdge::Right => CursorIcon::EResize, + ResizeEdge::TopRight => CursorIcon::NeResize, + ResizeEdge::BottomRight => CursorIcon::SeResize, + _ => unimplemented!(), + }; + match e.kind { + PointerEventKind::Press { + time, + button, + serial, + } if button == BTN_LEFT => { + my_seat.last_ptr_press.replace((time, button, serial)); + window.window.resize( + &my_seat.seat, + serial, + resize_edge, + ); + return; + } + PointerEventKind::Motion { .. } => { + if my_seat.icon != Some(icon) { + let _ = my_seat + .ptr + .as_ref() + .unwrap() + .set_cursor(conn, icon); + my_seat.icon = Some(icon); + } + return; + } + PointerEventKind::Enter { .. } => { + my_seat.ptr_focus.replace(e.surface.clone()); + if my_seat.icon != Some(icon) { + let _ = my_seat + .ptr + .as_ref() + .unwrap() + .set_cursor(conn, icon); + my_seat.icon = Some(icon); + } + } + PointerEventKind::Leave { .. } => { + my_seat.ptr_focus.take(); + my_seat.icon = None; + } + _ => {} + } + let _ = my_seat.ptr.as_ref().unwrap().set_cursor(conn, icon); + } else if my_seat.icon.is_some() { + let _ = my_seat + .ptr + .as_ref() + .unwrap() + .set_cursor(conn, CursorIcon::Default); + my_seat.icon = None; + } + + if is_active { + self.sctk_events.push(SctkEvent::PointerEvent { + variant: e.clone(), + ptr_id: pointer.clone(), + seat_id: my_seat.seat.clone(), + }); + } + match e.kind { + PointerEventKind::Enter { .. } => { + my_seat.ptr_focus.replace(e.surface.clone()); + } + PointerEventKind::Leave { .. } => { + my_seat.ptr_focus.take(); + my_seat.icon = None; + } + PointerEventKind::Press { + time, + button, + serial, + } => { + my_seat.last_ptr_press.replace((time, button, serial)); + } + // TODO revisit events that ought to be handled and change internal state + _ => {} + } + } + } +} + +delegate_pointer!(@ SctkState); diff --git a/sctk/src/handlers/seat/seat.rs b/sctk/src/handlers/seat/seat.rs new file mode 100644 index 0000000000..cf28572a7d --- /dev/null +++ b/sctk/src/handlers/seat/seat.rs @@ -0,0 +1,191 @@ +use crate::{ + event_loop::{state::SctkSeat, state::SctkState}, + sctk_event::{KeyboardEventVariant, SctkEvent, SeatEventVariant}, +}; +use iced_runtime::keyboard::Modifiers; +use sctk::{ + delegate_seat, + reexports::client::{protocol::wl_keyboard::WlKeyboard, Proxy}, + seat::{pointer::ThemeSpec, SeatHandler}, +}; +use std::fmt::Debug; + +impl SeatHandler for SctkState +where + T: 'static, +{ + fn seat_state(&mut self) -> &mut sctk::seat::SeatState { + &mut self.seat_state + } + + fn new_seat( + &mut self, + _conn: &sctk::reexports::client::Connection, + qh: &sctk::reexports::client::QueueHandle, + seat: sctk::reexports::client::protocol::wl_seat::WlSeat, + ) { + self.sctk_events.push(SctkEvent::SeatEvent { + variant: SeatEventVariant::New, + id: seat.clone(), + }); + let data_device = + self.data_device_manager_state.get_data_device(qh, &seat); + self.seats.push(SctkSeat { + seat, + kbd: None, + ptr: None, + _touch: None, + data_device, + _modifiers: Modifiers::default(), + kbd_focus: None, + ptr_focus: None, + last_ptr_press: None, + last_kbd_press: None, + icon: None, + }); + } + + fn new_capability( + &mut self, + _conn: &sctk::reexports::client::Connection, + qh: &sctk::reexports::client::QueueHandle, + seat: sctk::reexports::client::protocol::wl_seat::WlSeat, + capability: sctk::seat::Capability, + ) { + let my_seat = match self.seats.iter_mut().find(|s| s.seat == seat) { + Some(s) => s, + None => { + self.seats.push(SctkSeat { + seat: seat.clone(), + kbd: None, + ptr: None, + _touch: None, + data_device: self + .data_device_manager_state + .get_data_device(qh, &seat), + _modifiers: Modifiers::default(), + kbd_focus: None, + ptr_focus: None, + last_ptr_press: None, + last_kbd_press: None, + icon: None, + }); + self.seats.last_mut().unwrap() + } + }; + // TODO data device + match capability { + sctk::seat::Capability::Keyboard => { + let seat_clone = seat.clone(); + if let Ok(kbd) = self.seat_state.get_keyboard_with_repeat( + qh, + &seat, + None, + self.loop_handle.clone(), + Box::new(move |state, kbd: &WlKeyboard, e| { + state.sctk_events.push(SctkEvent::KeyboardEvent { + variant: KeyboardEventVariant::Repeat(e), + kbd_id: kbd.clone(), + seat_id: seat_clone.clone(), + }); + }), + ) { + self.sctk_events.push(SctkEvent::SeatEvent { + variant: SeatEventVariant::NewCapability( + capability, + kbd.id(), + ), + id: seat.clone(), + }); + my_seat.kbd.replace(kbd); + } + } + sctk::seat::Capability::Pointer => { + let surface = self.compositor_state.create_surface(qh); + + if let Ok(ptr) = self.seat_state.get_pointer_with_theme( + qh, + &seat, + self.shm_state.wl_shm(), + surface, + ThemeSpec::default(), + ) { + self.sctk_events.push(SctkEvent::SeatEvent { + variant: SeatEventVariant::NewCapability( + capability, + ptr.pointer().id(), + ), + id: seat.clone(), + }); + my_seat.ptr.replace(ptr); + } + } + sctk::seat::Capability::Touch => { + // TODO touch + } + _ => unimplemented!(), + } + } + + fn remove_capability( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + seat: sctk::reexports::client::protocol::wl_seat::WlSeat, + capability: sctk::seat::Capability, + ) { + let my_seat = match self.seats.iter_mut().find(|s| s.seat == seat) { + Some(s) => s, + None => return, + }; + + // TODO data device + match capability { + // TODO use repeating kbd? + sctk::seat::Capability::Keyboard => { + if let Some(kbd) = my_seat.kbd.take() { + self.sctk_events.push(SctkEvent::SeatEvent { + variant: SeatEventVariant::RemoveCapability( + capability, + kbd.id(), + ), + id: seat.clone(), + }); + } + } + sctk::seat::Capability::Pointer => { + if let Some(ptr) = my_seat.ptr.take() { + self.sctk_events.push(SctkEvent::SeatEvent { + variant: SeatEventVariant::RemoveCapability( + capability, + ptr.pointer().id(), + ), + id: seat.clone(), + }); + } + } + sctk::seat::Capability::Touch => { + // TODO touch + // my_seat.touch = self.seat_state.get_touch(qh, &seat).ok(); + } + _ => unimplemented!(), + } + } + + fn remove_seat( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + seat: sctk::reexports::client::protocol::wl_seat::WlSeat, + ) { + self.sctk_events.push(SctkEvent::SeatEvent { + variant: SeatEventVariant::Remove, + id: seat.clone(), + }); + if let Some(i) = self.seats.iter().position(|s| s.seat == seat) { + self.seats.remove(i); + } + } +} + +delegate_seat!(@ SctkState); diff --git a/sctk/src/handlers/seat/touch.rs b/sctk/src/handlers/seat/touch.rs new file mode 100644 index 0000000000..70b786d12e --- /dev/null +++ b/sctk/src/handlers/seat/touch.rs @@ -0,0 +1 @@ +// TODO diff --git a/sctk/src/handlers/session_lock.rs b/sctk/src/handlers/session_lock.rs new file mode 100644 index 0000000000..16d6322610 --- /dev/null +++ b/sctk/src/handlers/session_lock.rs @@ -0,0 +1,57 @@ +use crate::{handlers::SctkState, sctk_event::SctkEvent}; +use sctk::{ + delegate_session_lock, + reexports::client::{Connection, QueueHandle}, + session_lock::{ + SessionLock, SessionLockHandler, SessionLockSurface, + SessionLockSurfaceConfigure, + }, +}; +use std::fmt::Debug; + +impl SessionLockHandler for SctkState { + fn locked( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _session_lock: SessionLock, + ) { + self.sctk_events.push(SctkEvent::SessionLocked); + } + + fn finished( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _session_lock: SessionLock, + ) { + self.sctk_events.push(SctkEvent::SessionLockFinished); + } + + fn configure( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + session_lock_surface: SessionLockSurface, + configure: SessionLockSurfaceConfigure, + _serial: u32, + ) { + let lock_surface = match self.lock_surfaces.iter_mut().find(|s| { + s.session_lock_surface.wl_surface() + == session_lock_surface.wl_surface() + }) { + Some(l) => l, + None => return, + }; + let first = lock_surface.last_configure.is_none(); + lock_surface.last_configure.replace(configure.clone()); + self.sctk_events + .push(SctkEvent::SessionLockSurfaceConfigure { + surface: session_lock_surface.wl_surface().clone(), + configure, + first, + }); + } +} + +delegate_session_lock!(@ SctkState); diff --git a/sctk/src/handlers/shell/layer.rs b/sctk/src/handlers/shell/layer.rs new file mode 100644 index 0000000000..e45f5e9387 --- /dev/null +++ b/sctk/src/handlers/shell/layer.rs @@ -0,0 +1,113 @@ +use crate::{ + dpi::LogicalSize, + event_loop::state::SctkState, + sctk_event::{LayerSurfaceEventVariant, SctkEvent}, +}; +use sctk::{ + delegate_layer, + reexports::client::Proxy, + shell::{ + wlr_layer::{Anchor, KeyboardInteractivity, LayerShellHandler}, + WaylandSurface, + }, +}; +use std::fmt::Debug; + +impl LayerShellHandler for SctkState { + fn closed( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + layer: &sctk::shell::wlr_layer::LayerSurface, + ) { + let layer = match self.layer_surfaces.iter().position(|s| { + s.surface.wl_surface().id() == layer.wl_surface().id() + }) { + Some(w) => self.layer_surfaces.remove(w), + None => return, + }; + + self.sctk_events.push(SctkEvent::LayerSurfaceEvent { + variant: LayerSurfaceEventVariant::Done, + id: layer.surface.wl_surface().clone(), + }) + // TODO popup cleanup + } + + fn configure( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + layer: &sctk::shell::wlr_layer::LayerSurface, + mut configure: sctk::shell::wlr_layer::LayerSurfaceConfigure, + _serial: u32, + ) { + let layer = + match self.layer_surfaces.iter_mut().find(|s| { + s.surface.wl_surface().id() == layer.wl_surface().id() + }) { + Some(l) => l, + None => return, + }; + configure.new_size.0 = if let Some(w) = layer.requested_size.0 { + w + } else { + configure.new_size.0.max(1) + }; + configure.new_size.1 = if let Some(h) = layer.requested_size.1 { + h + } else { + configure.new_size.1.max(1) + }; + + layer.update_viewport(configure.new_size.0, configure.new_size.1); + let first = layer.last_configure.is_none(); + layer.last_configure.replace(configure.clone()); + + self.sctk_events.push(SctkEvent::LayerSurfaceEvent { + variant: LayerSurfaceEventVariant::Configure( + configure, + layer.surface.wl_surface().clone(), + first, + ), + id: layer.surface.wl_surface().clone(), + }); + self.frame_events.push(layer.surface.wl_surface().clone()); + } +} + +delegate_layer!(@ SctkState); + +#[allow(dead_code)] +/// A request to SCTK window from Winit window. +#[derive(Debug, Clone)] +pub enum LayerSurfaceRequest { + /// Set fullscreen. + /// + /// Passing `None` will set it on the current monitor. + Size(LogicalSize), + + /// Unset fullscreen. + UnsetFullscreen, + + /// Show cursor for the certain window or not. + ShowCursor(bool), + + /// Set anchor + Anchor(Anchor), + + /// Set margin + ExclusiveZone(i32), + + /// Set margin + Margin(u32), + + /// Passthrough mouse input to underlying windows. + KeyboardInteractivity(KeyboardInteractivity), + + /// Redraw was requested. + Redraw, + + /// Window should be closed. + Close, +} diff --git a/sctk/src/handlers/shell/mod.rs b/sctk/src/handlers/shell/mod.rs new file mode 100644 index 0000000000..5556c08d3e --- /dev/null +++ b/sctk/src/handlers/shell/mod.rs @@ -0,0 +1,3 @@ +pub mod layer; +pub mod xdg_popup; +pub mod xdg_window; diff --git a/sctk/src/handlers/shell/xdg_popup.rs b/sctk/src/handlers/shell/xdg_popup.rs new file mode 100644 index 0000000000..8be7166323 --- /dev/null +++ b/sctk/src/handlers/shell/xdg_popup.rs @@ -0,0 +1,86 @@ +use crate::{ + event_loop::state::{self, SctkState, SctkSurface}, + sctk_event::{PopupEventVariant, SctkEvent}, +}; +use sctk::{delegate_xdg_popup, shell::xdg::popup::PopupHandler}; +use std::fmt::Debug; + +impl PopupHandler for SctkState { + fn configure( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + popup: &sctk::shell::xdg::popup::Popup, + configure: sctk::shell::xdg::popup::PopupConfigure, + ) { + let sctk_popup = match self.popups.iter_mut().find(|s| { + s.popup.wl_surface().clone() == popup.wl_surface().clone() + }) { + Some(p) => p, + None => return, + }; + let first = sctk_popup.last_configure.is_none(); + sctk_popup.last_configure.replace(configure.clone()); + + self.sctk_events.push(SctkEvent::PopupEvent { + variant: PopupEventVariant::Configure( + configure, + popup.wl_surface().clone(), + first, + ), + id: popup.wl_surface().clone(), + toplevel_id: sctk_popup.data.toplevel.clone(), + parent_id: match &sctk_popup.data.parent { + SctkSurface::LayerSurface(s) => s.clone(), + SctkSurface::Window(s) => s.clone(), + SctkSurface::Popup(s) => s.clone(), + }, + }); + self.frame_events.push(popup.wl_surface().clone()); + } + + fn done( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + popup: &sctk::shell::xdg::popup::Popup, + ) { + let sctk_popup = match self.popups.iter().position(|s| { + s.popup.wl_surface().clone() == popup.wl_surface().clone() + }) { + Some(p) => self.popups.remove(p), + None => return, + }; + let mut to_destroy = vec![sctk_popup]; + while let Some(popup_to_destroy) = to_destroy.last() { + match popup_to_destroy.data.parent.clone() { + state::SctkSurface::LayerSurface(_) + | state::SctkSurface::Window(_) => { + break; + } + state::SctkSurface::Popup(popup_to_destroy_first) => { + let popup_to_destroy_first = self + .popups + .iter() + .position(|p| { + p.popup.wl_surface() == &popup_to_destroy_first + }) + .unwrap(); + let popup_to_destroy_first = + self.popups.remove(popup_to_destroy_first); + to_destroy.push(popup_to_destroy_first); + } + } + } + for popup in to_destroy.into_iter().rev() { + self.sctk_events.push(SctkEvent::PopupEvent { + variant: PopupEventVariant::Done, + toplevel_id: popup.data.toplevel.clone(), + parent_id: popup.data.parent.wl_surface().clone(), + id: popup.popup.wl_surface().clone(), + }); + self.popups.push(popup); + } + } +} +delegate_xdg_popup!(@ SctkState); diff --git a/sctk/src/handlers/shell/xdg_window.rs b/sctk/src/handlers/shell/xdg_window.rs new file mode 100644 index 0000000000..b03b69870f --- /dev/null +++ b/sctk/src/handlers/shell/xdg_window.rs @@ -0,0 +1,116 @@ +use crate::{ + dpi::LogicalSize, + event_loop::state::SctkState, + sctk_event::{SctkEvent, WindowEventVariant}, +}; +use sctk::{ + delegate_xdg_shell, delegate_xdg_window, + shell::{xdg::window::WindowHandler, WaylandSurface}, +}; +use std::{fmt::Debug, num::NonZeroU32}; + +impl WindowHandler for SctkState { + fn request_close( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + window: &sctk::shell::xdg::window::Window, + ) { + let window = match self + .windows + .iter() + .find(|s| s.window.wl_surface() == window.wl_surface()) + { + Some(w) => w, + None => return, + }; + + self.sctk_events.push(SctkEvent::WindowEvent { + variant: WindowEventVariant::Close, + id: window.window.wl_surface().clone(), + }) + // TODO popup cleanup + } + + fn configure( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + window: &sctk::shell::xdg::window::Window, + mut configure: sctk::shell::xdg::window::WindowConfigure, + _serial: u32, + ) { + let window = match self + .windows + .iter_mut() + .find(|w| w.window.wl_surface() == window.wl_surface()) + { + Some(w) => w, + None => return, + }; + + if window.last_configure.as_ref().map(|c| c.state) + != Some(configure.state) + { + self.sctk_events.push(SctkEvent::WindowEvent { + variant: WindowEventVariant::StateChanged(configure.state), + id: window.window.wl_surface().clone(), + }); + } + if window.last_configure.as_ref().map(|c| c.capabilities) + != Some(configure.capabilities) + { + self.sctk_events.push(SctkEvent::WindowEvent { + variant: WindowEventVariant::WmCapabilities( + configure.capabilities, + ), + id: window.window.wl_surface().clone(), + }); + } + + if configure.new_size.0.is_none() { + configure.new_size.0 = Some( + window + .requested_size + .and_then(|r| NonZeroU32::new(r.0)) + .unwrap_or_else(|| NonZeroU32::new(300).unwrap()), + ); + } + if configure.new_size.1.is_none() { + configure.new_size.1 = Some( + window + .requested_size + .and_then(|r| NonZeroU32::new(r.1)) + .unwrap_or_else(|| NonZeroU32::new(500).unwrap()), + ); + } + configure + .new_size + .0 + .zip(configure.new_size.1) + .map(|new_size| { + window.update_size(LogicalSize { + width: new_size.0, + height: new_size.1, + }); + }); + + let wl_surface = window.window.wl_surface(); + let id = wl_surface.clone(); + let first = window.last_configure.is_none(); + window.last_configure.replace(configure.clone()); + + self.sctk_events.push(SctkEvent::WindowEvent { + variant: WindowEventVariant::Configure( + configure, + wl_surface.clone(), + first, + ), + id, + }); + self.frame_events.push(wl_surface.clone()); + } +} + +delegate_xdg_window!(@ SctkState); +delegate_xdg_shell!(@ SctkState); diff --git a/sctk/src/handlers/wp_fractional_scaling.rs b/sctk/src/handlers/wp_fractional_scaling.rs new file mode 100644 index 0000000000..aed95e2087 --- /dev/null +++ b/sctk/src/handlers/wp_fractional_scaling.rs @@ -0,0 +1,97 @@ +// From: https://github.com/rust-windowing/winit/blob/master/src/platform_impl/linux/wayland/types/wp_fractional_scaling.rs +//! Handling of the fractional scaling. + +use std::marker::PhantomData; + +use sctk::reexports::client::globals::{BindError, GlobalList}; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::Dispatch; +use sctk::reexports::client::{delegate_dispatch, Connection, Proxy, QueueHandle}; +use sctk::reexports::protocols::wp::fractional_scale::v1::client::wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1; +use sctk::reexports::protocols::wp::fractional_scale::v1::client::wp_fractional_scale_v1::Event as FractionalScalingEvent; +use sctk::reexports::protocols::wp::fractional_scale::v1::client::wp_fractional_scale_v1::WpFractionalScaleV1; + +use sctk::globals::GlobalData; + +use crate::event_loop::state::SctkState; + +/// The scaling factor denominator. +const SCALE_DENOMINATOR: f64 = 120.; + +/// Fractional scaling manager. +#[derive(Debug)] +pub struct FractionalScalingManager { + manager: WpFractionalScaleManagerV1, + + _phantom: PhantomData, +} + +pub struct FractionalScaling { + /// The surface used for scaling. + surface: WlSurface, +} + +impl FractionalScalingManager { + /// Create new viewporter. + pub fn new( + globals: &GlobalList, + queue_handle: &QueueHandle>, + ) -> Result { + let manager = globals.bind(queue_handle, 1..=1, GlobalData)?; + Ok(Self { + manager, + _phantom: PhantomData, + }) + } + + pub fn fractional_scaling( + &self, + surface: &WlSurface, + queue_handle: &QueueHandle>, + ) -> WpFractionalScaleV1 { + let data = FractionalScaling { + surface: surface.clone(), + }; + self.manager + .get_fractional_scale(surface, queue_handle, data) + } +} + +impl Dispatch> + for FractionalScalingManager +{ + fn event( + _: &mut SctkState, + _: &WpFractionalScaleManagerV1, + _: ::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle>, + ) { + // No events. + } +} + +impl Dispatch> + for FractionalScalingManager +{ + fn event( + state: &mut SctkState, + _: &WpFractionalScaleV1, + event: ::Event, + data: &FractionalScaling, + _: &Connection, + _: &QueueHandle>, + ) { + if let FractionalScalingEvent::PreferredScale { scale } = event { + state.scale_factor_changed( + &data.surface, + scale as f64 / SCALE_DENOMINATOR, + false, + ); + } + } +} + +delegate_dispatch!(@ SctkState: [WpFractionalScaleManagerV1: GlobalData] => FractionalScalingManager); +delegate_dispatch!(@ SctkState: [WpFractionalScaleV1: FractionalScaling] => FractionalScalingManager); diff --git a/sctk/src/handlers/wp_viewporter.rs b/sctk/src/handlers/wp_viewporter.rs new file mode 100644 index 0000000000..31ca68777b --- /dev/null +++ b/sctk/src/handlers/wp_viewporter.rs @@ -0,0 +1,80 @@ +//! Handling of the wp-viewporter. + +use std::marker::PhantomData; + +use sctk::reexports::client::globals::{BindError, GlobalList}; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::Dispatch; +use sctk::reexports::client::{ + delegate_dispatch, Connection, Proxy, QueueHandle, +}; +use sctk::reexports::protocols::wp::viewporter::client::wp_viewport::WpViewport; +use sctk::reexports::protocols::wp::viewporter::client::wp_viewporter::WpViewporter; + +use sctk::globals::GlobalData; + +use crate::event_loop::state::SctkState; + +/// Viewporter. +#[derive(Debug)] +pub struct ViewporterState { + viewporter: WpViewporter, + _phantom: PhantomData, +} + +impl ViewporterState { + /// Create new viewporter. + pub fn new( + globals: &GlobalList, + queue_handle: &QueueHandle>, + ) -> Result { + let viewporter = globals.bind(queue_handle, 1..=1, GlobalData)?; + Ok(Self { + viewporter, + _phantom: PhantomData, + }) + } + + /// Get the viewport for the given object. + pub fn get_viewport( + &self, + surface: &WlSurface, + queue_handle: &QueueHandle>, + ) -> WpViewport { + self.viewporter + .get_viewport(surface, queue_handle, GlobalData) + } +} + +impl Dispatch> + for ViewporterState +{ + fn event( + _: &mut SctkState, + _: &WpViewporter, + _: ::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle>, + ) { + // No events. + } +} + +impl Dispatch> + for ViewporterState +{ + fn event( + _: &mut SctkState, + _: &WpViewport, + _: ::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle>, + ) { + // No events. + } +} + +delegate_dispatch!(@ SctkState: [WpViewporter: GlobalData] => ViewporterState); +delegate_dispatch!(@ SctkState: [WpViewport: GlobalData] => ViewporterState); diff --git a/sctk/src/keymap.rs b/sctk/src/keymap.rs new file mode 100644 index 0000000000..fe02f37bb7 --- /dev/null +++ b/sctk/src/keymap.rs @@ -0,0 +1,475 @@ +// Borrowed from winit + +pub fn keysym_to_key(keysym: u32) -> Key { + use xkbcommon_dl::keysyms; + Key::Named(match keysym { + // TTY function keys + keysyms::BackSpace => Named::Backspace, + keysyms::Tab => Named::Tab, + // keysyms::Linefeed => Named::Linefeed, + keysyms::Clear => Named::Clear, + keysyms::Return => Named::Enter, + keysyms::Pause => Named::Pause, + keysyms::Scroll_Lock => Named::ScrollLock, + keysyms::Sys_Req => Named::PrintScreen, + keysyms::Escape => Named::Escape, + keysyms::Delete => Named::Delete, + + // IME keys + keysyms::Multi_key => Named::Compose, + keysyms::Codeinput => Named::CodeInput, + keysyms::SingleCandidate => Named::SingleCandidate, + keysyms::MultipleCandidate => Named::AllCandidates, + keysyms::PreviousCandidate => Named::PreviousCandidate, + + // Japanese key + keysyms::Kanji => Named::KanjiMode, + keysyms::Muhenkan => Named::NonConvert, + keysyms::Henkan_Mode => Named::Convert, + keysyms::Romaji => Named::Romaji, + keysyms::Hiragana => Named::Hiragana, + keysyms::Hiragana_Katakana => Named::HiraganaKatakana, + keysyms::Zenkaku => Named::Zenkaku, + keysyms::Hankaku => Named::Hankaku, + keysyms::Zenkaku_Hankaku => Named::ZenkakuHankaku, + // keysyms::Touroku => Named::Touroku, + // keysyms::Massyo => Named::Massyo, + keysyms::Kana_Lock => Named::KanaMode, + keysyms::Kana_Shift => Named::KanaMode, + keysyms::Eisu_Shift => Named::Alphanumeric, + keysyms::Eisu_toggle => Named::Alphanumeric, + // NOTE: The next three items are aliases for values we've already mapped. + // keysyms::Kanji_Bangou => Named::CodeInput, + // keysyms::Zen_Koho => Named::AllCandidates, + // keysyms::Mae_Koho => Named::PreviousCandidate, + + // Cursor control & motion + keysyms::Home => Named::Home, + keysyms::Left => Named::ArrowLeft, + keysyms::Up => Named::ArrowUp, + keysyms::Right => Named::ArrowRight, + keysyms::Down => Named::ArrowDown, + // keysyms::Prior => Named::PageUp, + keysyms::Page_Up => Named::PageUp, + // keysyms::Next => Named::PageDown, + keysyms::Page_Down => Named::PageDown, + keysyms::End => Named::End, + // keysyms::Begin => Named::Begin, + + // Misc. functions + keysyms::Select => Named::Select, + keysyms::Print => Named::PrintScreen, + keysyms::Execute => Named::Execute, + keysyms::Insert => Named::Insert, + keysyms::Undo => Named::Undo, + keysyms::Redo => Named::Redo, + keysyms::Menu => Named::ContextMenu, + keysyms::Find => Named::Find, + keysyms::Cancel => Named::Cancel, + keysyms::Help => Named::Help, + keysyms::Break => Named::Pause, + keysyms::Mode_switch => Named::ModeChange, + // keysyms::script_switch => Named::ModeChange, + keysyms::Num_Lock => Named::NumLock, + + // Keypad keys + // keysyms::KP_Space => return Key::Character(" "), + keysyms::KP_Tab => Named::Tab, + keysyms::KP_Enter => Named::Enter, + keysyms::KP_F1 => Named::F1, + keysyms::KP_F2 => Named::F2, + keysyms::KP_F3 => Named::F3, + keysyms::KP_F4 => Named::F4, + keysyms::KP_Home => Named::Home, + keysyms::KP_Left => Named::ArrowLeft, + keysyms::KP_Up => Named::ArrowUp, + keysyms::KP_Right => Named::ArrowRight, + keysyms::KP_Down => Named::ArrowDown, + // keysyms::KP_Prior => Named::PageUp, + keysyms::KP_Page_Up => Named::PageUp, + // keysyms::KP_Next => Named::PageDown, + keysyms::KP_Page_Down => Named::PageDown, + keysyms::KP_End => Named::End, + // This is the key labeled "5" on the numpad when NumLock is off. + // keysyms::KP_Begin => Named::Begin, + keysyms::KP_Insert => Named::Insert, + keysyms::KP_Delete => Named::Delete, + // keysyms::KP_Equal => Named::Equal, + // keysyms::KP_Multiply => Named::Multiply, + // keysyms::KP_Add => Named::Add, + // keysyms::KP_Separator => Named::Separator, + // keysyms::KP_Subtract => Named::Subtract, + // keysyms::KP_Decimal => Named::Decimal, + // keysyms::KP_Divide => Named::Divide, + + // keysyms::KP_0 => return Key::Character("0"), + // keysyms::KP_1 => return Key::Character("1"), + // keysyms::KP_2 => return Key::Character("2"), + // keysyms::KP_3 => return Key::Character("3"), + // keysyms::KP_4 => return Key::Character("4"), + // keysyms::KP_5 => return Key::Character("5"), + // keysyms::KP_6 => return Key::Character("6"), + // keysyms::KP_7 => return Key::Character("7"), + // keysyms::KP_8 => return Key::Character("8"), + // keysyms::KP_9 => return Key::Character("9"), + + // Function keys + keysyms::F1 => Named::F1, + keysyms::F2 => Named::F2, + keysyms::F3 => Named::F3, + keysyms::F4 => Named::F4, + keysyms::F5 => Named::F5, + keysyms::F6 => Named::F6, + keysyms::F7 => Named::F7, + keysyms::F8 => Named::F8, + keysyms::F9 => Named::F9, + keysyms::F10 => Named::F10, + keysyms::F11 => Named::F11, + keysyms::F12 => Named::F12, + keysyms::F13 => Named::F13, + keysyms::F14 => Named::F14, + keysyms::F15 => Named::F15, + keysyms::F16 => Named::F16, + keysyms::F17 => Named::F17, + keysyms::F18 => Named::F18, + keysyms::F19 => Named::F19, + keysyms::F20 => Named::F20, + keysyms::F21 => Named::F21, + keysyms::F22 => Named::F22, + keysyms::F23 => Named::F23, + keysyms::F24 => Named::F24, + keysyms::F25 => Named::F25, + keysyms::F26 => Named::F26, + keysyms::F27 => Named::F27, + keysyms::F28 => Named::F28, + keysyms::F29 => Named::F29, + keysyms::F30 => Named::F30, + keysyms::F31 => Named::F31, + keysyms::F32 => Named::F32, + keysyms::F33 => Named::F33, + keysyms::F34 => Named::F34, + keysyms::F35 => Named::F35, + + // Modifiers + keysyms::Shift_L => Named::Shift, + keysyms::Shift_R => Named::Shift, + keysyms::Control_L => Named::Control, + keysyms::Control_R => Named::Control, + keysyms::Caps_Lock => Named::CapsLock, + // keysyms::Shift_Lock => Named::ShiftLock, + + // keysyms::Meta_L => Named::Meta, + // keysyms::Meta_R => Named::Meta, + keysyms::Alt_L => Named::Alt, + keysyms::Alt_R => Named::Alt, + keysyms::Super_L => Named::Super, + keysyms::Super_R => Named::Super, + keysyms::Hyper_L => Named::Hyper, + keysyms::Hyper_R => Named::Hyper, + + // XKB function and modifier keys + // keysyms::ISO_Lock => Named::IsoLock, + // keysyms::ISO_Level2_Latch => Named::IsoLevel2Latch, + keysyms::ISO_Level3_Shift => Named::AltGraph, + keysyms::ISO_Level3_Latch => Named::AltGraph, + keysyms::ISO_Level3_Lock => Named::AltGraph, + // keysyms::ISO_Level5_Shift => Named::IsoLevel5Shift, + // keysyms::ISO_Level5_Latch => Named::IsoLevel5Latch, + // keysyms::ISO_Level5_Lock => Named::IsoLevel5Lock, + // keysyms::ISO_Group_Shift => Named::IsoGroupShift, + // keysyms::ISO_Group_Latch => Named::IsoGroupLatch, + // keysyms::ISO_Group_Lock => Named::IsoGroupLock, + keysyms::ISO_Next_Group => Named::GroupNext, + // keysyms::ISO_Next_Group_Lock => Named::GroupNextLock, + keysyms::ISO_Prev_Group => Named::GroupPrevious, + // keysyms::ISO_Prev_Group_Lock => Named::GroupPreviousLock, + keysyms::ISO_First_Group => Named::GroupFirst, + // keysyms::ISO_First_Group_Lock => Named::GroupFirstLock, + keysyms::ISO_Last_Group => Named::GroupLast, + // keysyms::ISO_Last_Group_Lock => Named::GroupLastLock, + // + keysyms::ISO_Left_Tab => Named::Tab, + // keysyms::ISO_Move_Line_Up => Named::IsoMoveLineUp, + // keysyms::ISO_Move_Line_Down => Named::IsoMoveLineDown, + // keysyms::ISO_Partial_Line_Up => Named::IsoPartialLineUp, + // keysyms::ISO_Partial_Line_Down => Named::IsoPartialLineDown, + // keysyms::ISO_Partial_Space_Left => Named::IsoPartialSpaceLeft, + // keysyms::ISO_Partial_Space_Right => Named::IsoPartialSpaceRight, + // keysyms::ISO_Set_Margin_Left => Named::IsoSetMarginLeft, + // keysyms::ISO_Set_Margin_Right => Named::IsoSetMarginRight, + // keysyms::ISO_Release_Margin_Left => Named::IsoReleaseMarginLeft, + // keysyms::ISO_Release_Margin_Right => Named::IsoReleaseMarginRight, + // keysyms::ISO_Release_Both_Margins => Named::IsoReleaseBothMargins, + // keysyms::ISO_Fast_Cursor_Left => Named::IsoFastCursorLeft, + // keysyms::ISO_Fast_Cursor_Right => Named::IsoFastCursorRight, + // keysyms::ISO_Fast_Cursor_Up => Named::IsoFastCursorUp, + // keysyms::ISO_Fast_Cursor_Down => Named::IsoFastCursorDown, + // keysyms::ISO_Continuous_Underline => Named::IsoContinuousUnderline, + // keysyms::ISO_Discontinuous_Underline => Named::IsoDiscontinuousUnderline, + // keysyms::ISO_Emphasize => Named::IsoEmphasize, + // keysyms::ISO_Center_Object => Named::IsoCenterObject, + keysyms::ISO_Enter => Named::Enter, + + // dead_grave..dead_currency + + // dead_lowline..dead_longsolidusoverlay + + // dead_a..dead_capital_schwa + + // dead_greek + + // First_Virtual_Screen..Terminate_Server + + // AccessX_Enable..AudibleBell_Enable + + // Pointer_Left..Pointer_Drag5 + + // Pointer_EnableKeys..Pointer_DfltBtnPrev + + // ch..C_H + + // 3270 terminal keys + // keysyms::3270_Duplicate => Named::Duplicate, + // keysyms::3270_FieldMark => Named::FieldMark, + // keysyms::3270_Right2 => Named::Right2, + // keysyms::3270_Left2 => Named::Left2, + // keysyms::3270_BackTab => Named::BackTab, + keysyms::_3270_EraseEOF => Named::EraseEof, + // keysyms::3270_EraseInput => Named::EraseInput, + // keysyms::3270_Reset => Named::Reset, + // keysyms::3270_Quit => Named::Quit, + // keysyms::3270_PA1 => Named::Pa1, + // keysyms::3270_PA2 => Named::Pa2, + // keysyms::3270_PA3 => Named::Pa3, + // keysyms::3270_Test => Named::Test, + keysyms::_3270_Attn => Named::Attn, + // keysyms::3270_CursorBlink => Named::CursorBlink, + // keysyms::3270_AltCursor => Named::AltCursor, + // keysyms::3270_KeyClick => Named::KeyClick, + // keysyms::3270_Jump => Named::Jump, + // keysyms::3270_Ident => Named::Ident, + // keysyms::3270_Rule => Named::Rule, + // keysyms::3270_Copy => Named::Copy, + keysyms::_3270_Play => Named::Play, + // keysyms::3270_Setup => Named::Setup, + // keysyms::3270_Record => Named::Record, + // keysyms::3270_ChangeScreen => Named::ChangeScreen, + // keysyms::3270_DeleteWord => Named::DeleteWord, + keysyms::_3270_ExSelect => Named::ExSel, + keysyms::_3270_CursorSelect => Named::CrSel, + keysyms::_3270_PrintScreen => Named::PrintScreen, + keysyms::_3270_Enter => Named::Enter, + + keysyms::space => Named::Space, + // exclam..Sinh_kunddaliya + + // XFree86 + // keysyms::XF86_ModeLock => Named::ModeLock, + + // XFree86 - Backlight controls + keysyms::XF86_MonBrightnessUp => Named::BrightnessUp, + keysyms::XF86_MonBrightnessDown => Named::BrightnessDown, + // keysyms::XF86_KbdLightOnOff => Named::LightOnOff, + // keysyms::XF86_KbdBrightnessUp => Named::KeyboardBrightnessUp, + // keysyms::XF86_KbdBrightnessDown => Named::KeyboardBrightnessDown, + + // XFree86 - "Internet" + keysyms::XF86_Standby => Named::Standby, + keysyms::XF86_AudioLowerVolume => Named::AudioVolumeDown, + keysyms::XF86_AudioRaiseVolume => Named::AudioVolumeUp, + keysyms::XF86_AudioPlay => Named::MediaPlay, + keysyms::XF86_AudioStop => Named::MediaStop, + keysyms::XF86_AudioPrev => Named::MediaTrackPrevious, + keysyms::XF86_AudioNext => Named::MediaTrackNext, + keysyms::XF86_HomePage => Named::BrowserHome, + keysyms::XF86_Mail => Named::LaunchMail, + // keysyms::XF86_Start => Named::Start, + keysyms::XF86_Search => Named::BrowserSearch, + keysyms::XF86_AudioRecord => Named::MediaRecord, + + // XFree86 - PDA + keysyms::XF86_Calculator => Named::LaunchApplication2, + // keysyms::XF86_Memo => Named::Memo, + // keysyms::XF86_ToDoList => Named::ToDoList, + keysyms::XF86_Calendar => Named::LaunchCalendar, + keysyms::XF86_PowerDown => Named::Power, + // keysyms::XF86_ContrastAdjust => Named::AdjustContrast, + // keysyms::XF86_RockerUp => Named::RockerUp, + // keysyms::XF86_RockerDown => Named::RockerDown, + // keysyms::XF86_RockerEnter => Named::RockerEnter, + + // XFree86 - More "Internet" + keysyms::XF86_Back => Named::BrowserBack, + keysyms::XF86_Forward => Named::BrowserForward, + // keysyms::XF86_Stop => Named::Stop, + keysyms::XF86_Refresh => Named::BrowserRefresh, + keysyms::XF86_PowerOff => Named::Power, + keysyms::XF86_WakeUp => Named::WakeUp, + keysyms::XF86_Eject => Named::Eject, + keysyms::XF86_ScreenSaver => Named::LaunchScreenSaver, + keysyms::XF86_WWW => Named::LaunchWebBrowser, + keysyms::XF86_Sleep => Named::Standby, + keysyms::XF86_Favorites => Named::BrowserFavorites, + keysyms::XF86_AudioPause => Named::MediaPause, + // keysyms::XF86_AudioMedia => Named::AudioMedia, + keysyms::XF86_MyComputer => Named::LaunchApplication1, + // keysyms::XF86_VendorHome => Named::VendorHome, + // keysyms::XF86_LightBulb => Named::LightBulb, + // keysyms::XF86_Shop => Named::BrowserShop, + // keysyms::XF86_History => Named::BrowserHistory, + // keysyms::XF86_OpenURL => Named::OpenUrl, + // keysyms::XF86_AddFavorite => Named::AddFavorite, + // keysyms::XF86_HotLinks => Named::HotLinks, + // keysyms::XF86_BrightnessAdjust => Named::BrightnessAdjust, + // keysyms::XF86_Finance => Named::BrowserFinance, + // keysyms::XF86_Community => Named::BrowserCommunity, + keysyms::XF86_AudioRewind => Named::MediaRewind, + // keysyms::XF86_BackForward => Key::???, + // XF86_Launch0..XF86_LaunchF + + // XF86_ApplicationLeft..XF86_CD + keysyms::XF86_Calculater => Named::LaunchApplication2, // Nice typo, libxkbcommon :) + // XF86_Clear + keysyms::XF86_Close => Named::Close, + keysyms::XF86_Copy => Named::Copy, + keysyms::XF86_Cut => Named::Cut, + // XF86_Display..XF86_Documents + keysyms::XF86_Excel => Named::LaunchSpreadsheet, + // XF86_Explorer..XF86iTouch + keysyms::XF86_LogOff => Named::LogOff, + // XF86_Market..XF86_MenuPB + keysyms::XF86_MySites => Named::BrowserFavorites, + keysyms::XF86_New => Named::New, + // XF86_News..XF86_OfficeHome + keysyms::XF86_Open => Named::Open, + // XF86_Option + keysyms::XF86_Paste => Named::Paste, + keysyms::XF86_Phone => Named::LaunchPhone, + // XF86_Q + keysyms::XF86_Reply => Named::MailReply, + keysyms::XF86_Reload => Named::BrowserRefresh, + // XF86_RotateWindows..XF86_RotationKB + keysyms::XF86_Save => Named::Save, + // XF86_ScrollUp..XF86_ScrollClick + keysyms::XF86_Send => Named::MailSend, + keysyms::XF86_Spell => Named::SpellCheck, + keysyms::XF86_SplitScreen => Named::SplitScreenToggle, + // XF86_Support..XF86_User2KB + keysyms::XF86_Video => Named::LaunchMediaPlayer, + // XF86_WheelButton + keysyms::XF86_Word => Named::LaunchWordProcessor, + // XF86_Xfer + keysyms::XF86_ZoomIn => Named::ZoomIn, + keysyms::XF86_ZoomOut => Named::ZoomOut, + + // XF86_Away..XF86_Messenger + keysyms::XF86_WebCam => Named::LaunchWebCam, + keysyms::XF86_MailForward => Named::MailForward, + // XF86_Pictures + keysyms::XF86_Music => Named::LaunchMusicPlayer, + + // XF86_Battery..XF86_UWB + // + keysyms::XF86_AudioForward => Named::MediaFastForward, + // XF86_AudioRepeat + keysyms::XF86_AudioRandomPlay => Named::RandomToggle, + keysyms::XF86_Subtitle => Named::Subtitle, + keysyms::XF86_AudioCycleTrack => Named::MediaAudioTrack, + // XF86_CycleAngle..XF86_Blue + // + keysyms::XF86_Suspend => Named::Standby, + keysyms::XF86_Hibernate => Named::Hibernate, + // XF86_TouchpadToggle..XF86_TouchpadOff + // + keysyms::XF86_AudioMute => Named::AudioVolumeMute, + + // XF86_Switch_VT_1..XF86_Switch_VT_12 + + // XF86_Ungrab..XF86_ClearGrab + keysyms::XF86_Next_VMode => Named::VideoModeNext, + // keysyms::XF86_Prev_VMode => Named::VideoModePrevious, + // XF86_LogWindowTree..XF86_LogGrabInfo + + // SunFA_Grave..SunFA_Cedilla + + // keysyms::SunF36 => Named::F36 | Named::F11, + // keysyms::SunF37 => Named::F37 | Named::F12, + + // keysyms::SunSys_Req => Named::PrintScreen, + // The next couple of xkb (until SunStop) are already handled. + // SunPrint_Screen..SunPageDown + + // SunUndo..SunFront + keysyms::SUN_Copy => Named::Copy, + keysyms::SUN_Open => Named::Open, + keysyms::SUN_Paste => Named::Paste, + keysyms::SUN_Cut => Named::Cut, + + // SunPowerSwitch + keysyms::SUN_AudioLowerVolume => Named::AudioVolumeDown, + keysyms::SUN_AudioMute => Named::AudioVolumeMute, + keysyms::SUN_AudioRaiseVolume => Named::AudioVolumeUp, + // SUN_VideoDegauss + keysyms::SUN_VideoLowerBrightness => Named::BrightnessDown, + keysyms::SUN_VideoRaiseBrightness => Named::BrightnessUp, + // SunPowerSwitchShift + // + _ => return Key::Unidentified, + }) +} + +use iced_runtime::keyboard::{key::Named, Key, Location}; + +pub fn keysym_location(keysym: u32) -> Location { + use xkbcommon_dl::keysyms; + match keysym { + xkeysym::key::Shift_L + | keysyms::Control_L + | keysyms::Meta_L + | keysyms::Alt_L + | keysyms::Super_L + | keysyms::Hyper_L => Location::Left, + keysyms::Shift_R + | keysyms::Control_R + | keysyms::Meta_R + | keysyms::Alt_R + | keysyms::Super_R + | keysyms::Hyper_R => Location::Right, + keysyms::KP_0 + | keysyms::KP_1 + | keysyms::KP_2 + | keysyms::KP_3 + | keysyms::KP_4 + | keysyms::KP_5 + | keysyms::KP_6 + | keysyms::KP_7 + | keysyms::KP_8 + | keysyms::KP_9 + | keysyms::KP_Space + | keysyms::KP_Tab + | keysyms::KP_Enter + | keysyms::KP_F1 + | keysyms::KP_F2 + | keysyms::KP_F3 + | keysyms::KP_F4 + | keysyms::KP_Home + | keysyms::KP_Left + | keysyms::KP_Up + | keysyms::KP_Right + | keysyms::KP_Down + | keysyms::KP_Page_Up + | keysyms::KP_Page_Down + | keysyms::KP_End + | keysyms::KP_Begin + | keysyms::KP_Insert + | keysyms::KP_Delete + | keysyms::KP_Equal + | keysyms::KP_Multiply + | keysyms::KP_Add + | keysyms::KP_Separator + | keysyms::KP_Subtract + | keysyms::KP_Decimal + | keysyms::KP_Divide => Location::Numpad, + _ => Location::Standard, + } +} diff --git a/sctk/src/lib.rs b/sctk/src/lib.rs new file mode 100644 index 0000000000..48520f8889 --- /dev/null +++ b/sctk/src/lib.rs @@ -0,0 +1,25 @@ +pub mod application; +pub mod clipboard; +pub mod commands; +pub mod conversion; +pub mod dpi; +pub mod error; +pub mod event_loop; +mod handlers; +pub mod keymap; +pub mod result; +pub mod sctk_event; +pub mod settings; +#[cfg(feature = "system")] +pub mod system; +pub mod util; +pub mod window; + +pub use application::{run, Application}; +pub use clipboard::Clipboard; +pub use error::Error; +pub use event_loop::proxy::Proxy; +pub use iced_graphics::Viewport; +pub use iced_runtime as runtime; +pub use iced_runtime::core; +pub use settings::Settings; diff --git a/sctk/src/result.rs b/sctk/src/result.rs new file mode 100644 index 0000000000..fc9af5c566 --- /dev/null +++ b/sctk/src/result.rs @@ -0,0 +1,6 @@ +use crate::error::Error; + +/// The result of running an [`Application`]. +/// +/// [`Application`]: crate::Application +pub type Result = std::result::Result<(), Error>; diff --git a/sctk/src/sctk_event.rs b/sctk/src/sctk_event.rs new file mode 100755 index 0000000000..af81d84a92 --- /dev/null +++ b/sctk/src/sctk_event.rs @@ -0,0 +1,960 @@ +use crate::{ + application::SurfaceIdWrapper, + conversion::{ + modifiers_to_native, pointer_axis_to_native, pointer_button_to_native, + }, + dpi::PhysicalSize, + keymap::{self, keysym_to_key}, +}; + +use iced_futures::core::event::{ + wayland::{LayerEvent, PopupEvent, SessionLockEvent}, + PlatformSpecific, +}; +use iced_runtime::{ + command::platform_specific::wayland::data_device::DndIcon, + core::{event::wayland, keyboard, mouse, window, Point}, + keyboard::{key, Key, Location}, + window::Id as SurfaceId, +}; +use sctk::{ + output::OutputInfo, + reexports::client::{ + backend::ObjectId, + protocol::{ + wl_data_device_manager::DndAction, wl_keyboard::WlKeyboard, + wl_output::WlOutput, wl_pointer::WlPointer, wl_seat::WlSeat, + wl_surface::WlSurface, + }, + Proxy, + }, + reexports::csd_frame::WindowManagerCapabilities, + seat::{ + keyboard::{KeyEvent, Modifiers}, + pointer::{PointerEvent, PointerEventKind}, + Capability, + }, + session_lock::SessionLockSurfaceConfigure, + shell::{ + wlr_layer::LayerSurfaceConfigure, + xdg::{popup::PopupConfigure, window::WindowConfigure}, + }, +}; +use std::{collections::HashMap, time::Instant}; +use wayland_protocols::wp::viewporter::client::wp_viewport::WpViewport; + +pub enum IcedSctkEvent { + /// Emitted when new events arrive from the OS to be processed. + /// + /// This event type is useful as a place to put code that should be done before you start + /// processing events, such as updating frame timing information for benchmarking or checking + /// the [`StartCause`][crate::event::StartCause] to see if a timer set by + /// [`ControlFlow::WaitUntil`](crate::event_loop::ControlFlow::WaitUntil) has elapsed. + NewEvents(StartCause), + + /// Any user event from iced + UserEvent(T), + + /// An event produced by sctk + SctkEvent(SctkEvent), + + #[cfg(feature = "a11y")] + A11ySurfaceCreated( + SurfaceIdWrapper, + crate::event_loop::adapter::IcedSctkAdapter, + ), + + /// emitted after first accessibility tree is requested + #[cfg(feature = "a11y")] + A11yEnabled, + + /// accessibility event + #[cfg(feature = "a11y")] + A11yEvent(ActionRequestEvent), + + /// Emitted when all of the event loop's input events have been processed and redraw processing + /// is about to begin. + /// + /// This event is useful as a place to put your code that should be run after all + /// state-changing events have been handled and you want to do stuff (updating state, performing + /// calculations, etc) that happens as the "main body" of your event loop. If your program only draws + /// graphics when something changes, it's usually better to do it in response to + /// [`Event::RedrawRequested`](crate::event::Event::RedrawRequested), which gets emitted + /// immediately after this event. Programs that draw graphics continuously, like most games, + /// can render here unconditionally for simplicity. + MainEventsCleared, + + /// Emitted after [`MainEventsCleared`] when a window should be redrawn. + /// + /// This gets triggered in two scenarios: + /// - The OS has performed an operation that's invalidated the window's contents (such as + /// resizing the window). + /// - The application has explicitly requested a redraw via [`Window::request_redraw`]. + /// + /// During each iteration of the event loop, Winit will aggregate duplicate redraw requests + /// into a single event, to help avoid duplicating rendering work. + /// + /// Mainly of interest to applications with mostly-static graphics that avoid redrawing unless + /// something changes, like most non-game GUIs. + /// + /// [`MainEventsCleared`]: Self::MainEventsCleared + RedrawRequested(ObjectId), + + /// Emitted after all [`RedrawRequested`] events have been processed and control flow is about to + /// be taken away from the program. If there are no `RedrawRequested` events, it is emitted + /// immediately after `MainEventsCleared`. + /// + /// This event is useful for doing any cleanup or bookkeeping work after all the rendering + /// tasks have been completed. + /// + /// [`RedrawRequested`]: Self::RedrawRequested + RedrawEventsCleared, + + /// Emitted when the event loop is being shut down. + /// + /// This is irreversible - if this event is emitted, it is guaranteed to be the last event that + /// gets emitted. You generally want to treat this as an "do on quit" event. + LoopDestroyed, + + /// Dnd source created with an icon surface. + DndSurfaceCreated(WlSurface, DndIcon, SurfaceId), + + /// Frame callback event + Frame(WlSurface), +} + +#[derive(Debug, Clone)] +pub enum SctkEvent { + // + // Input events + // + SeatEvent { + variant: SeatEventVariant, + id: WlSeat, + }, + PointerEvent { + variant: PointerEvent, + ptr_id: WlPointer, + seat_id: WlSeat, + }, + KeyboardEvent { + variant: KeyboardEventVariant, + kbd_id: WlKeyboard, + seat_id: WlSeat, + }, + // TODO data device & touch + + // + // Surface Events + // + WindowEvent { + variant: WindowEventVariant, + id: WlSurface, + }, + LayerSurfaceEvent { + variant: LayerSurfaceEventVariant, + id: WlSurface, + }, + PopupEvent { + variant: PopupEventVariant, + /// this may be the Id of a window or layer surface + toplevel_id: WlSurface, + /// this may be any SurfaceId + parent_id: WlSurface, + /// the id of this popup + id: WlSurface, + }, + + // + // output events + // + NewOutput { + id: WlOutput, + info: Option, + }, + UpdateOutput { + id: WlOutput, + info: OutputInfo, + }, + RemovedOutput(WlOutput), + // + // compositor events + // + ScaleFactorChanged { + factor: f64, + id: WlOutput, + inner_size: PhysicalSize, + }, + DataSource(DataSourceEvent), + DndOffer { + event: DndOfferEvent, + surface: WlSurface, + }, + /// session lock events + SessionLocked, + SessionLockFinished, + SessionLockSurfaceCreated { + surface: WlSurface, + native_id: SurfaceId, + }, + SessionLockSurfaceConfigure { + surface: WlSurface, + configure: SessionLockSurfaceConfigure, + first: bool, + }, + SessionUnlocked, +} + +#[derive(Debug, Clone)] +pub enum DataSourceEvent { + /// A DnD action has been accepted by the compositor for your source. + DndActionAccepted(DndAction), + /// A DnD mime type has been accepted by a client for your source. + MimeAccepted(Option), + /// Dnd Finished event. + DndFinished, + /// Dnd Cancelled event. + DndCancelled, + /// Dnd Drop performed event. + DndDropPerformed, + /// Send the selection data to the clipboard. + SendSelectionData { + /// The mime type of the data to be sent + mime_type: String, + }, + /// Send the DnD data to the destination. + SendDndData { + /// The mime type of the data to be sent + mime_type: String, + }, +} + +#[derive(Debug, Clone)] +pub enum DndOfferEvent { + /// A DnD offer has been introduced with the given mime types. + Enter { + x: f64, + y: f64, + mime_types: Vec, + }, + /// The DnD device has left. + Leave, + /// Drag and Drop Motion event. + Motion { + /// x coordinate of the pointer + x: f64, + /// y coordinate of the pointer + y: f64, + }, + /// A drop has been performed. + DropPerformed, + /// Read the DnD data + Data { + /// The raw data + data: Vec, + /// mime type of the data to read + mime_type: String, + }, + SourceActions(DndAction), + SelectedAction(DndAction), +} + +#[cfg(feature = "a11y")] +#[derive(Debug, Clone)] +pub struct ActionRequestEvent { + pub surface_id: ObjectId, + pub request: iced_accessibility::accesskit::ActionRequest, +} + +#[derive(Debug, Clone)] +pub enum SeatEventVariant { + New, + Remove, + NewCapability(Capability, ObjectId), + RemoveCapability(Capability, ObjectId), +} + +#[derive(Debug, Clone)] +pub enum KeyboardEventVariant { + Leave(WlSurface), + Enter(WlSurface), + Press(KeyEvent), + Repeat(KeyEvent), + Release(KeyEvent), + Modifiers(Modifiers), +} + +#[derive(Debug, Clone)] +pub enum WindowEventVariant { + Created(ObjectId, SurfaceId), + /// + Close, + /// + WmCapabilities(WindowManagerCapabilities), + /// + ConfigureBounds { + width: u32, + height: u32, + }, + /// + Configure(WindowConfigure, WlSurface, bool), + + /// window state changed + StateChanged(sctk::reexports::csd_frame::WindowState), + /// Scale Factor + ScaleFactorChanged(f64, Option), +} + +#[derive(Debug, Clone)] +pub enum PopupEventVariant { + /// Popup Created + Created(ObjectId, SurfaceId), + /// + Done, + /// + Configure(PopupConfigure, WlSurface, bool), + /// + RepositionionedPopup { token: u32 }, + /// size + Size(u32, u32), + /// Scale Factor + ScaleFactorChanged(f64, Option), +} + +#[derive(Debug, Clone)] +pub enum LayerSurfaceEventVariant { + /// sent after creation of the layer surface + Created(ObjectId, SurfaceId), + /// + Done, + /// + Configure(LayerSurfaceConfigure, WlSurface, bool), + /// Scale Factor + ScaleFactorChanged(f64, Option), +} + +/// Describes the reason the event loop is resuming. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum StartCause { + /// Sent if the time specified by [`ControlFlow::WaitUntil`] has been reached. Contains the + /// moment the timeout was requested and the requested resume time. The actual resume time is + /// guaranteed to be equal to or after the requested resume time. + /// + /// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil + ResumeTimeReached { + start: Instant, + requested_resume: Instant, + }, + + /// Sent if the OS has new events to send to the window, after a wait was requested. Contains + /// the moment the wait was requested and the resume time, if requested. + WaitCancelled { + start: Instant, + requested_resume: Option, + }, + + /// Sent if the event loop is being resumed after the loop's control flow was set to + /// [`ControlFlow::Poll`]. + /// + /// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll + Poll, + + /// Sent once, immediately after `run` is called. Indicates that the loop was just initialized. + Init, +} + +/// Pending update to a window requested by the user. +#[derive(Default, Debug, Clone, Copy)] +pub struct SurfaceUserRequest { + /// Whether `redraw` was requested. + pub redraw_requested: bool, + + /// Wether the frame should be refreshed. + pub refresh_frame: bool, +} + +// The window update coming from the compositor. +#[derive(Default, Debug, Clone)] +pub struct SurfaceCompositorUpdate { + /// New window configure. + pub configure: Option, + + /// New scale factor. + pub scale_factor: Option, +} + +impl SctkEvent { + pub fn to_native( + self, + modifiers: &mut Modifiers, + surface_ids: &HashMap, + destroyed_surface_ids: &HashMap, + ) -> Vec { + match self { + // TODO Ashley: Platform specific multi-seat events? + SctkEvent::SeatEvent { .. } => Default::default(), + SctkEvent::PointerEvent { variant, .. } => match variant.kind { + PointerEventKind::Enter { .. } => { + vec![iced_runtime::core::Event::Mouse( + mouse::Event::CursorEntered, + )] + } + PointerEventKind::Leave { .. } => { + vec![iced_runtime::core::Event::Mouse( + mouse::Event::CursorLeft, + )] + } + PointerEventKind::Motion { .. } => { + vec![iced_runtime::core::Event::Mouse( + mouse::Event::CursorMoved { + position: Point::new( + variant.position.0 as f32, + variant.position.1 as f32, + ), + }, + )] + } + PointerEventKind::Press { + time: _, + button, + serial: _, + } => pointer_button_to_native(button) + .map(|b| { + iced_runtime::core::Event::Mouse( + mouse::Event::ButtonPressed(b), + ) + }) + .into_iter() + .collect(), // TODO Ashley: conversion + PointerEventKind::Release { + time: _, + button, + serial: _, + } => pointer_button_to_native(button) + .map(|b| { + iced_runtime::core::Event::Mouse( + mouse::Event::ButtonReleased(b), + ) + }) + .into_iter() + .collect(), // TODO Ashley: conversion + PointerEventKind::Axis { + time: _, + horizontal, + vertical, + source, + } => pointer_axis_to_native(source, horizontal, vertical) + .map(|a| { + iced_runtime::core::Event::Mouse( + mouse::Event::WheelScrolled { delta: a }, + ) + }) + .into_iter() + .collect(), // TODO Ashley: conversion + }, + SctkEvent::KeyboardEvent { + variant, + kbd_id: _, + seat_id, + } => match variant { + KeyboardEventVariant::Leave(surface) => surface_ids + .get(&surface.id()) + .and_then(|id| match id { + SurfaceIdWrapper::LayerSurface(_id) => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::Layer( + LayerEvent::Unfocused, + surface, + id.inner(), + ), + ), + )) + } + SurfaceIdWrapper::Window(id) => { + Some(iced_runtime::core::Event::Window( + *id, + window::Event::Unfocused, + )) + } + SurfaceIdWrapper::Popup(_id) => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::Popup( + PopupEvent::Unfocused, + surface, + id.inner(), + ), + ), + )) + } + SurfaceIdWrapper::Dnd(_) => None, + SurfaceIdWrapper::SessionLock(_) => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::SessionLock( + SessionLockEvent::Unfocused( + surface, + id.inner(), + ), + ), + ), + )) + } + }) + .into_iter() + .chain([iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::Seat( + wayland::SeatEvent::Leave, + seat_id, + )), + )]) + .collect(), + KeyboardEventVariant::Enter(surface) => surface_ids + .get(&surface.id()) + .and_then(|id| match id { + SurfaceIdWrapper::LayerSurface(_id) => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::Layer( + LayerEvent::Focused, + surface, + id.inner(), + ), + ), + )) + } + SurfaceIdWrapper::Window(id) => { + Some(iced_runtime::core::Event::Window( + *id, + window::Event::Focused, + )) + } + SurfaceIdWrapper::Popup(_id) => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::Popup( + PopupEvent::Focused, + surface, + id.inner(), + ), + ), + )) + } + SurfaceIdWrapper::Dnd(_) => None, + SurfaceIdWrapper::SessionLock(_) => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::SessionLock( + SessionLockEvent::Focused( + surface, + id.inner(), + ), + ), + ), + )) + } + }) + .into_iter() + .chain([iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::Seat( + wayland::SeatEvent::Enter, + seat_id, + )), + )]) + .collect(), + KeyboardEventVariant::Press(ke) => { + let (key, location) = keysym_to_vkey_location( + ke.keysym.raw(), + ke.utf8.as_deref(), + ); + Some(iced_runtime::core::Event::Keyboard( + keyboard::Event::KeyPressed { + key: key, + location: location, + text: ke.utf8.map(|s| s.into()), + modifiers: modifiers_to_native(*modifiers), + }, + )) + .into_iter() + .collect() + } + KeyboardEventVariant::Repeat(KeyEvent { + raw_code, + utf8, + .. + }) => { + let (key, location) = + keysym_to_vkey_location(raw_code, utf8.as_deref()); + Some(iced_runtime::core::Event::Keyboard( + keyboard::Event::KeyPressed { + key: key, + location: location, + text: utf8.map(|s| s.into()), + modifiers: modifiers_to_native(*modifiers), + }, + )) + .into_iter() + .collect() + } + KeyboardEventVariant::Release(ke) => { + let (k, location) = keysym_to_vkey_location( + ke.keysym.raw(), + ke.utf8.as_deref(), + ); + Some(iced_runtime::core::Event::Keyboard( + keyboard::Event::KeyReleased { + key: k, + location: location, + modifiers: modifiers_to_native(*modifiers), + }, + )) + .into_iter() + .collect() + } + KeyboardEventVariant::Modifiers(new_mods) => { + *modifiers = new_mods; + vec![iced_runtime::core::Event::Keyboard( + keyboard::Event::ModifiersChanged(modifiers_to_native( + new_mods, + )), + )] + } + }, + SctkEvent::WindowEvent { + variant, + id: surface, + } => match variant { + // TODO Ashley: platform specific events for window + WindowEventVariant::Created(..) => Default::default(), + WindowEventVariant::Close => destroyed_surface_ids + .get(&surface.id()) + .map(|id| { + iced_runtime::core::Event::Window( + id.inner(), + window::Event::CloseRequested, + ) + }) + .into_iter() + .collect(), + WindowEventVariant::WmCapabilities(caps) => surface_ids + .get(&surface.id()) + .map(|id| id.inner()) + .map(|id| { + iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::Window( + wayland::WindowEvent::WmCapabilities(caps), + surface, + id, + )), + ) + }) + .into_iter() + .collect(), + WindowEventVariant::ConfigureBounds { .. } => { + Default::default() + } + WindowEventVariant::Configure(configure, surface, _) => { + if configure.is_resizing() { + surface_ids + .get(&surface.id()) + .map(|id| { + iced_runtime::core::Event::Window( + id.inner(), + window::Event::Resized { + width: configure + .new_size + .0 + .unwrap() + .get(), + height: configure + .new_size + .1 + .unwrap() + .get(), + }, + ) + }) + .into_iter() + .collect() + } else { + Default::default() + } + } + WindowEventVariant::ScaleFactorChanged(..) => { + Default::default() + } + WindowEventVariant::StateChanged(s) => surface_ids + .get(&surface.id()) + .map(|id| { + iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::Window( + wayland::WindowEvent::State(s), + surface, + id.inner(), + )), + ) + }) + .into_iter() + .collect(), + }, + SctkEvent::LayerSurfaceEvent { + variant, + id: surface, + } => match variant { + LayerSurfaceEventVariant::Done => destroyed_surface_ids + .get(&surface.id()) + .map(|id| { + iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::Layer( + LayerEvent::Done, + surface, + id.inner(), + )), + ) + }) + .into_iter() + .collect(), + _ => Default::default(), + }, + SctkEvent::PopupEvent { + variant, + id: surface, + .. + } => { + match variant { + PopupEventVariant::Done => destroyed_surface_ids + .get(&surface.id()) + .map(|id| { + iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::Popup( + PopupEvent::Done, + surface, + id.inner(), + ), + ), + ) + }) + .into_iter() + .collect(), + PopupEventVariant::Created(_, _) => Default::default(), // TODO + PopupEventVariant::Configure(_, _, _) => Default::default(), // TODO + PopupEventVariant::RepositionionedPopup { token: _ } => { + Default::default() + } + PopupEventVariant::Size(_, _) => Default::default(), + PopupEventVariant::ScaleFactorChanged(..) => { + Default::default() + } // TODO + } + } + SctkEvent::NewOutput { id, info } => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::Output( + wayland::OutputEvent::Created(info), + id, + )), + )) + .into_iter() + .collect() + } + SctkEvent::UpdateOutput { id, info } => { + vec![iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::Output( + wayland::OutputEvent::InfoUpdate(info), + id, + )), + )] + } + SctkEvent::RemovedOutput(id) => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::Output( + wayland::OutputEvent::Removed, + id, + )), + )) + .into_iter() + .collect() + } + SctkEvent::ScaleFactorChanged { + factor: _, + id: _, + inner_size: _, + } => Default::default(), + SctkEvent::DndOffer { event, .. } => match event { + DndOfferEvent::Enter { mime_types, x, y } => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::DndOffer( + wayland::DndOfferEvent::Enter { mime_types, x, y }, + )), + )) + .into_iter() + .collect() + } + DndOfferEvent::Motion { x, y } => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::DndOffer( + wayland::DndOfferEvent::Motion { x, y }, + )), + )) + .into_iter() + .collect() + } + DndOfferEvent::DropPerformed => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::DndOffer( + wayland::DndOfferEvent::DropPerformed, + )), + )) + .into_iter() + .collect() + } + DndOfferEvent::Leave => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::DndOffer( + wayland::DndOfferEvent::Leave, + )), + )) + .into_iter() + .collect() + } + DndOfferEvent::Data { mime_type, data } => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::DndOffer( + wayland::DndOfferEvent::DndData { data, mime_type }, + )), + )) + .into_iter() + .collect() + } + DndOfferEvent::SourceActions(actions) => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::DndOffer( + wayland::DndOfferEvent::SourceActions(actions), + )), + )) + .into_iter() + .collect() + } + DndOfferEvent::SelectedAction(action) => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::DndOffer( + wayland::DndOfferEvent::SelectedAction(action), + )), + )) + .into_iter() + .collect() + } + }, + SctkEvent::DataSource(event) => match event { + DataSourceEvent::DndDropPerformed => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::DataSource( + wayland::DataSourceEvent::DndDropPerformed, + )), + )) + .into_iter() + .collect() + } + DataSourceEvent::DndFinished => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::DataSource( + wayland::DataSourceEvent::DndFinished, + )), + )) + .into_iter() + .collect() + } + DataSourceEvent::DndCancelled => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::DataSource( + wayland::DataSourceEvent::Cancelled, + )), + )) + .into_iter() + .collect() + } + DataSourceEvent::MimeAccepted(mime_type) => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::DataSource( + wayland::DataSourceEvent::MimeAccepted(mime_type), + )), + )) + .into_iter() + .collect() + } + DataSourceEvent::DndActionAccepted(action) => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::DataSource( + wayland::DataSourceEvent::DndActionAccepted(action), + )), + )) + .into_iter() + .collect() + } + DataSourceEvent::SendDndData { mime_type } => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::DataSource( + wayland::DataSourceEvent::SendDndData(mime_type), + )), + )) + .into_iter() + .collect() + } + DataSourceEvent::SendSelectionData { mime_type } => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::DataSource( + wayland::DataSourceEvent::SendSelectionData( + mime_type, + ), + )), + )) + .into_iter() + .collect() + } + }, + SctkEvent::SessionLocked => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::SessionLock( + wayland::SessionLockEvent::Locked, + )), + )) + .into_iter() + .collect() + } + SctkEvent::SessionLockFinished => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::SessionLock( + wayland::SessionLockEvent::Finished, + )), + )) + .into_iter() + .collect() + } + SctkEvent::SessionLockSurfaceCreated { .. } => vec![], + SctkEvent::SessionLockSurfaceConfigure { .. } => vec![], + SctkEvent::SessionUnlocked => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::SessionLock( + wayland::SessionLockEvent::Unlocked, + )), + )) + .into_iter() + .collect() + } + } + } +} + +fn keysym_to_vkey_location(keysym: u32, utf8: Option<&str>) -> (Key, Location) { + let mut key = keysym_to_key(keysym); + if matches!(key, key::Key::Unidentified) { + if let Some(utf8) = utf8 { + key = Key::Character(utf8.into()); + } + } + + let location = keymap::keysym_location(keysym); + (key, location) +} diff --git a/sctk/src/settings.rs b/sctk/src/settings.rs new file mode 100644 index 0000000000..f3299b06a8 --- /dev/null +++ b/sctk/src/settings.rs @@ -0,0 +1,32 @@ +use iced_runtime::command::platform_specific::wayland::{ + layer_surface::SctkLayerSurfaceSettings, window::SctkWindowSettings, +}; + +#[derive(Debug)] +pub struct Settings { + /// The data needed to initialize an [`Application`]. + /// + /// [`Application`]: crate::Application + pub flags: Flags, + /// optional keyboard repetition config + pub kbd_repeat: Option, + /// optional name and size of a custom pointer theme + pub ptr_theme: Option<(String, u32)>, + /// surface + pub surface: InitialSurface, + /// whether the application should exit on close of all windows + pub exit_on_close_request: bool, +} + +#[derive(Debug, Clone)] +pub enum InitialSurface { + LayerSurface(SctkLayerSurfaceSettings), + XdgWindow(SctkWindowSettings), + None, +} + +impl Default for InitialSurface { + fn default() -> Self { + Self::LayerSurface(SctkLayerSurfaceSettings::default()) + } +} diff --git a/sctk/src/system.rs b/sctk/src/system.rs new file mode 100644 index 0000000000..7b700e73af --- /dev/null +++ b/sctk/src/system.rs @@ -0,0 +1,41 @@ +//! Access the native system. +use crate::runtime::command::{self, Command}; +use crate::runtime::system::{Action, Information}; +use iced_graphics::compositor; + +/// Query for available system information. +pub fn fetch_information( + f: impl Fn(Information) -> Message + Send + 'static, +) -> Command { + Command::single(command::Action::System(Action::QueryInformation( + Box::new(f), + ))) +} + +pub(crate) fn information( + graphics_info: compositor::Information, +) -> Information { + use sysinfo::{CpuExt, ProcessExt, System, SystemExt}; + let mut system = System::new_all(); + system.refresh_all(); + + let cpu = system.global_cpu_info(); + + let memory_used = sysinfo::get_current_pid() + .and_then(|pid| system.process(pid).ok_or("Process not found")) + .map(|process| process.memory()) + .ok(); + + Information { + system_name: system.name(), + system_kernel: system.kernel_version(), + system_version: system.long_os_version(), + system_short_version: system.os_version(), + cpu_brand: cpu.brand().into(), + cpu_cores: system.physical_core_count(), + memory_total: system.total_memory(), + memory_used, + graphics_adapter: graphics_info.adapter, + graphics_backend: graphics_info.backend, + } +} diff --git a/sctk/src/util.rs b/sctk/src/util.rs new file mode 100644 index 0000000000..81329b263a --- /dev/null +++ b/sctk/src/util.rs @@ -0,0 +1,128 @@ +/// The behavior of cursor grabbing. +/// +/// Use this enum with [`Window::set_cursor_grab`] to grab the cursor. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum CursorGrabMode { + /// No grabbing of the cursor is performed. + None, + + /// The cursor is confined to the window area. + /// + /// There's no guarantee that the cursor will be hidden. You should hide it by yourself if you + /// want to do so. + /// + /// ## Platform-specific + /// + /// - **macOS:** Not implemented. Always returns [`ExternalError::NotSupported`] for now. + /// - **iOS / Android / Web:** Always returns an [`ExternalError::NotSupported`]. + Confined, + + /// The cursor is locked inside the window area to the certain position. + /// + /// There's no guarantee that the cursor will be hidden. You should hide it by yourself if you + /// want to do so. + /// + /// ## Platform-specific + /// + /// - **X11 / Windows:** Not implemented. Always returns [`ExternalError::NotSupported`] for now. + /// - **iOS / Android:** Always returns an [`ExternalError::NotSupported`]. + Locked, +} + +/// Describes the appearance of the mouse cursor. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum CursorIcon { + /// The platform-dependent default cursor. + Default, + /// A simple crosshair. + Crosshair, + /// A hand (often used to indicate links in web browsers). + Hand, + /// Self explanatory. + Arrow, + /// Indicates something is to be moved. + Move, + /// Indicates text that may be selected or edited. + Text, + /// Program busy indicator. + Wait, + /// Help indicator (often rendered as a "?") + Help, + /// Progress indicator. Shows that processing is being done. But in contrast + /// with "Wait" the user may still interact with the program. Often rendered + /// as a spinning beach ball, or an arrow with a watch or hourglass. + Progress, + + /// Cursor showing that something cannot be done. + NotAllowed, + ContextMenu, + Cell, + VerticalText, + Alias, + Copy, + NoDrop, + /// Indicates something can be grabbed. + Grab, + /// Indicates something is grabbed. + Grabbing, + AllScroll, + ZoomIn, + ZoomOut, + + /// Indicate that some edge is to be moved. For example, the 'SeResize' cursor + /// is used when the movement starts from the south-east corner of the box. + EResize, + NResize, + NeResize, + NwResize, + SResize, + SeResize, + SwResize, + WResize, + EwResize, + NsResize, + NeswResize, + NwseResize, + ColResize, + RowResize, +} + +impl Default for CursorIcon { + fn default() -> Self { + CursorIcon::Default + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Theme { + Light, + Dark, +} + +/// ## Platform-specific +/// +/// - **X11:** Sets the WM's `XUrgencyHint`. No distinction between [`Critical`] and [`Informational`]. +/// +/// [`Critical`]: Self::Critical +/// [`Informational`]: Self::Informational +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UserAttentionType { + /// ## Platform-specific + /// + /// - **macOS:** Bounces the dock icon until the application is in focus. + /// - **Windows:** Flashes both the window and the taskbar button until the application is in focus. + Critical, + /// ## Platform-specific + /// + /// - **macOS:** Bounces the dock icon once. + /// - **Windows:** Flashes the taskbar button until the application is in focus. + Informational, +} + +impl Default for UserAttentionType { + fn default() -> Self { + UserAttentionType::Informational + } +} diff --git a/sctk/src/widget.rs b/sctk/src/widget.rs new file mode 100644 index 0000000000..9f09cb8f21 --- /dev/null +++ b/sctk/src/widget.rs @@ -0,0 +1,232 @@ +//! Display information and interactive controls in your application. +pub use iced_native::widget::helpers::*; + +pub use iced_native::{column, row}; + +/// A container that distributes its contents vertically. +pub type Column<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::Column<'a, Message, Renderer>; + +/// A container that distributes its contents horizontally. +pub type Row<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::Row<'a, Message, Renderer>; + +pub mod text { + //! Write some text for your users to read. + pub use iced_native::widget::text::{Appearance, StyleSheet}; + + /// A paragraph of text. + pub type Text<'a, Renderer = crate::Renderer> = + iced_native::widget::Text<'a, Renderer>; +} + +pub mod button { + //! Allow your users to perform actions by pressing a button. + pub use iced_native::widget::button::{Appearance, StyleSheet}; + + /// A widget that produces a message when clicked. + pub type Button<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::Button<'a, Message, Renderer>; +} + +pub mod checkbox { + //! Show toggle controls using checkboxes. + pub use iced_native::widget::checkbox::{Appearance, StyleSheet}; + + /// A box that can be checked. + pub type Checkbox<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::Checkbox<'a, Message, Renderer>; +} + +pub mod container { + //! Decorate content and apply alignment. + pub use iced_native::widget::container::{Appearance, StyleSheet}; + + /// An element decorating some content. + pub type Container<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::Container<'a, Message, Renderer>; +} + +pub mod pane_grid { + //! Let your users split regions of your application and organize layout dynamically. + //! + //! [![Pane grid - Iced](https://thumbs.gfycat.com/MixedFlatJellyfish-small.gif)](https://gfycat.com/mixedflatjellyfish) + //! + //! # Example + //! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing, + //! drag and drop, and hotkey support. + //! + //! [`pane_grid` example]: https://github.com/iced-rs/iced/tree/0.4/examples/pane_grid + pub use iced_native::widget::pane_grid::{ + Axis, Configuration, Direction, DragEvent, Line, Node, Pane, + ResizeEvent, Split, State, StyleSheet, + }; + + /// A collection of panes distributed using either vertical or horizontal splits + /// to completely fill the space available. + /// + /// [![Pane grid - Iced](https://thumbs.gfycat.com/MixedFlatJellyfish-small.gif)](https://gfycat.com/mixedflatjellyfish) + pub type PaneGrid<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::PaneGrid<'a, Message, Renderer>; + + /// The content of a [`Pane`]. + pub type Content<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::pane_grid::Content<'a, Message, Renderer>; + + /// The title bar of a [`Pane`]. + pub type TitleBar<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::pane_grid::TitleBar<'a, Message, Renderer>; +} + +pub mod pick_list { + //! Display a dropdown list of selectable values. + pub use iced_native::widget::pick_list::{Appearance, StyleSheet}; + + /// A widget allowing the selection of a single value from a list of options. + pub type PickList<'a, T, Message, Renderer = crate::Renderer> = + iced_native::widget::PickList<'a, T, Message, Renderer>; +} + +pub mod radio { + //! Create choices using radio buttons. + pub use iced_native::widget::radio::{Appearance, StyleSheet}; + + /// A circular button representing a choice. + pub type Radio = + iced_native::widget::Radio; +} + +pub mod scrollable { + //! Navigate an endless amount of content with a scrollbar. + pub use iced_native::widget::scrollable::{ + snap_to, style::Scrollbar, style::Scroller, Id, StyleSheet, + }; + + /// A widget that can vertically display an infinite amount of content + /// with a scrollbar. + pub type Scrollable<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::Scrollable<'a, Message, Renderer>; +} + +pub mod toggler { + //! Show toggle controls using togglers. + pub use iced_native::widget::toggler::{Appearance, StyleSheet}; + + /// A toggler widget. + pub type Toggler<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::Toggler<'a, Message, Renderer>; +} + +pub mod text_input { + //! Display fields that can be filled with text. + pub use iced_native::widget::text_input::{ + focus, Appearance, Id, StyleSheet, + }; + + /// A field that can be filled with text. + pub type TextInput<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::TextInput<'a, Message, Renderer>; +} + +pub mod tooltip { + //! Display a widget over another. + pub use iced_native::widget::tooltip::Position; + + /// A widget allowing the selection of a single value from a list of options. + pub type Tooltip<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::Tooltip<'a, Message, Renderer>; +} + +pub use iced_native::widget::progress_bar; +pub use iced_native::widget::rule; +pub use iced_native::widget::slider; +pub use iced_native::widget::Space; + +pub use button::Button; +pub use checkbox::Checkbox; +pub use container::Container; +pub use pane_grid::PaneGrid; +pub use pick_list::PickList; +pub use progress_bar::ProgressBar; +pub use radio::Radio; +pub use rule::Rule; +pub use scrollable::Scrollable; +pub use slider::Slider; +pub use text::Text; +pub use text_input::TextInput; +pub use toggler::Toggler; +pub use tooltip::Tooltip; + +#[cfg(feature = "canvas")] +#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] +pub use iced_graphics::widget::canvas; + +#[cfg(feature = "canvas")] +#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] +/// Creates a new [`Canvas`]. +pub fn canvas(program: P) -> Canvas +where + P: canvas::Program, +{ + Canvas::new(program) +} + +#[cfg(feature = "image")] +#[cfg_attr(docsrs, doc(cfg(feature = "image")))] +pub mod image { + //! Display images in your user interface. + pub use iced_native::image::Handle; + + /// A frame that displays an image. + pub type Image = iced_native::widget::Image; + + pub use iced_native::widget::image::viewer; + pub use viewer::Viewer; +} + +#[cfg(feature = "qr_code")] +#[cfg_attr(docsrs, doc(cfg(feature = "qr_code")))] +pub use iced_graphics::widget::qr_code; + +#[cfg(feature = "svg")] +#[cfg_attr(docsrs, doc(cfg(feature = "svg")))] +pub mod svg { + //! Display vector graphics in your application. + pub use iced_native::svg::Handle; + pub use iced_native::widget::Svg; +} + +#[cfg(feature = "canvas")] +#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] +pub use canvas::Canvas; + +#[cfg(feature = "image")] +#[cfg_attr(docsrs, doc(cfg(feature = "image")))] +pub use image::Image; + +#[cfg(feature = "qr_code")] +#[cfg_attr(docsrs, doc(cfg(feature = "qr_code")))] +pub use qr_code::QRCode; + +#[cfg(feature = "svg")] +#[cfg_attr(docsrs, doc(cfg(feature = "svg")))] +pub use svg::Svg; + +use crate::Command; +use iced_native::widget::operation; + +/// Focuses the previous focusable widget. +pub fn focus_previous() -> Command +where + Message: 'static, +{ + Command::widget(operation::focusable::focus_previous()) +} + +/// Focuses the next focusable widget. +pub fn focus_next() -> Command +where + Message: 'static, +{ + Command::widget(operation::focusable::focus_next()) +} diff --git a/sctk/src/window.rs b/sctk/src/window.rs new file mode 100644 index 0000000000..2353a0c641 --- /dev/null +++ b/sctk/src/window.rs @@ -0,0 +1,3 @@ +pub fn resize() { + todo!() +} diff --git a/src/application.rs b/src/application.rs index 01b2032f43..67175ad5f4 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,4 +1,6 @@ //! Build interactive cross-platform applications. +use iced_core::window::Id; + use crate::{Command, Element, Executor, Settings, Subscription}; pub use crate::style::application::{Appearance, StyleSheet}; diff --git a/src/error.rs b/src/error.rs index 111bedf245..a1d2640057 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,6 @@ use crate::futures; use crate::graphics; +#[cfg(any(feature = "winit", feature = "wayland"))] use crate::shell; /// An error that occurred while running an application. @@ -18,15 +19,21 @@ pub enum Error { GraphicsCreationFailed(graphics::Error), } +#[cfg(any(feature = "winit", feature = "wayland"))] impl From for Error { fn from(error: shell::Error) -> Error { match error { shell::Error::ExecutorCreationFailed(error) => { Error::ExecutorCreationFailed(error) } + #[cfg(feature = "winit")] shell::Error::WindowCreationFailed(error) => { Error::WindowCreationFailed(Box::new(error)) } + #[cfg(feature = "wayland")] + shell::Error::WindowCreationFailed(error) => { + Error::WindowCreationFailed(error) + } shell::Error::GraphicsCreationFailed(error) => { Error::GraphicsCreationFailed(error) } diff --git a/src/lib.rs b/src/lib.rs index d82bc8868c..d0597982ea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -159,30 +159,55 @@ rustdoc::broken_intra_doc_links )] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![cfg_attr(docsrs, feature(doc_cfg))] + +#[cfg(all(feature = "wayland", feature = "winit"))] +compile_error!("cannot use `wayland` feature with `winit"); + +pub use iced_futures::futures; use iced_widget::graphics; use iced_widget::renderer; use iced_widget::style; -use iced_winit as shell; -use iced_winit::core; -use iced_winit::runtime; -pub use iced_futures::futures; +#[cfg(feature = "wayland")] +use iced_sctk as shell; +#[cfg(feature = "winit")] +use iced_winit as shell; +#[cfg(any(feature = "winit", feature = "wayland"))] +use shell::core; +#[cfg(any(feature = "winit", feature = "wayland"))] +use shell::runtime; #[cfg(feature = "highlighter")] pub use iced_highlighter as highlighter; +#[cfg(not(any(feature = "winit", feature = "wayland")))] +pub use iced_widget::core; +#[cfg(not(any(feature = "winit", feature = "wayland")))] +pub use iced_widget::runtime; mod error; -mod sandbox; -pub mod application; pub mod settings; pub mod time; pub mod window; +#[cfg(feature = "winit")] +pub mod application; +#[cfg(feature = "winit")] +mod sandbox; + +/// wayland application +#[cfg(feature = "wayland")] +pub mod wayland; +#[cfg(feature = "wayland")] +pub use wayland::sandbox; +#[cfg(feature = "wayland")] +pub use wayland::Application; + #[cfg(feature = "advanced")] pub mod advanced; -#[cfg(feature = "multi-window")] +#[cfg(all(feature = "winit", feature = "multi-window"))] pub mod multi_window; pub use style::theme; @@ -226,6 +251,8 @@ pub mod font { pub mod event { //! Handle events of a user interface. + #[cfg(feature = "wayland")] + pub use crate::core::event::wayland; pub use crate::core::event::{Event, MacOS, PlatformSpecific, Status}; pub use iced_futures::event::{listen, listen_raw, listen_with}; } @@ -300,6 +327,7 @@ pub mod widget { mod runtime {} } +#[cfg(feature = "winit")] pub use application::Application; pub use command::Command; pub use error::Error; @@ -307,6 +335,7 @@ pub use event::Event; pub use executor::Executor; pub use font::Font; pub use renderer::Renderer; +#[cfg(any(feature = "winit", feature = "wayland"))] pub use sandbox::Sandbox; pub use settings::Settings; pub use subscription::Subscription; diff --git a/src/settings.rs b/src/settings.rs index d9476b614e..d908b9aaaf 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,11 +1,15 @@ -//! Configure your application. +//! Configure your application + +#[cfg(feature = "winit")] use crate::window; use crate::{Font, Pixels}; +#[cfg(feature = "wayland")] +use iced_sctk::settings::InitialSurface; use std::borrow::Cow; /// The settings of an application. -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct Settings { /// The identifier of the application. /// @@ -16,8 +20,13 @@ pub struct Settings { /// The window settings. /// /// They will be ignored on the Web. + #[cfg(feature = "winit")] pub window: window::Settings, + /// The window settings. + #[cfg(feature = "wayland")] + pub initial_surface: InitialSurface, + /// The data needed to initialize the [`Application`]. /// /// [`Application`]: crate::Application @@ -46,15 +55,53 @@ pub struct Settings { /// /// [`Canvas`]: crate::widget::Canvas pub antialiasing: bool, + + /// If set to true the application will exit when the main window is closed. + pub exit_on_close_request: bool, } +#[cfg(not(any(feature = "winit", feature = "wayland")))] impl Settings { /// Initialize [`Application`] settings using the given data. /// /// [`Application`]: crate::Application pub fn with_flags(flags: Flags) -> Self { let default_settings = Settings::<()>::default(); + Self { + flags, + id: default_settings.id, + default_font: default_settings.default_font, + default_text_size: default_settings.default_text_size, + antialiasing: default_settings.antialiasing, + exit_on_close_request: default_settings.exit_on_close_request, + } + } +} +#[cfg(not(any(feature = "winit", feature = "wayland")))] +impl Default for Settings +where + Flags: Default, +{ + fn default() -> Self { + Self { + id: None, + flags: Default::default(), + default_font: Default::default(), + default_text_size: 16.0, + antialiasing: false, + exit_on_close_request: true, + } + } +} + +#[cfg(feature = "winit")] +impl Settings { + /// Initialize [`Application`] settings using the given data. + /// + /// [`Application`]: crate::Application + pub fn with_flags(flags: Flags) -> Self { + let default_settings = Settings::<()>::default(); Self { flags, id: default_settings.id, @@ -63,10 +110,12 @@ impl Settings { default_font: default_settings.default_font, default_text_size: default_settings.default_text_size, antialiasing: default_settings.antialiasing, + exit_on_close_request: default_settings.exit_on_close_request, } } } +#[cfg(feature = "winit")] impl Default for Settings where Flags: Default, @@ -80,10 +129,12 @@ where default_font: Font::default(), default_text_size: Pixels(16.0), antialiasing: false, + exit_on_close_request: false, } } } +#[cfg(feature = "winit")] impl From> for iced_winit::Settings { fn from(settings: Settings) -> iced_winit::Settings { iced_winit::Settings { @@ -94,3 +145,56 @@ impl From> for iced_winit::Settings { } } } + +#[cfg(feature = "wayland")] +impl Settings { + /// Initialize [`Application`] settings using the given data. + /// + /// [`Application`]: crate::Application + pub fn with_flags(flags: Flags) -> Self { + let default_settings = Settings::<()>::default(); + + Self { + flags, + id: default_settings.id, + initial_surface: default_settings.initial_surface, + default_font: default_settings.default_font, + default_text_size: default_settings.default_text_size, + antialiasing: default_settings.antialiasing, + exit_on_close_request: default_settings.exit_on_close_request, + fonts: default_settings.fonts, + } + } +} + +#[cfg(feature = "wayland")] +impl Default for Settings +where + Flags: Default, +{ + fn default() -> Self { + Self { + id: None, + initial_surface: Default::default(), + flags: Default::default(), + default_font: Default::default(), + default_text_size: Pixels(16.0), + antialiasing: false, + fonts: Vec::new(), + exit_on_close_request: true, + } + } +} + +#[cfg(feature = "wayland")] +impl From> for iced_sctk::Settings { + fn from(settings: Settings) -> iced_sctk::Settings { + iced_sctk::Settings { + kbd_repeat: Default::default(), + surface: settings.initial_surface, + flags: settings.flags, + exit_on_close_request: settings.exit_on_close_request, + ptr_theme: None, + } + } +} diff --git a/src/wayland/mod.rs b/src/wayland/mod.rs new file mode 100644 index 0000000000..0265f16482 --- /dev/null +++ b/src/wayland/mod.rs @@ -0,0 +1,196 @@ +use crate::runtime::window::Id; +use crate::{Command, Element, Executor, Settings, Subscription}; + +/// wayland sandbox +pub mod sandbox; +pub use crate::runtime::command::platform_specific::wayland as actions; +pub use crate::style::application::{Appearance, StyleSheet}; +use iced_renderer::graphics::Antialiasing; +pub use iced_sctk::{application::SurfaceIdWrapper, commands::*, settings::*}; + +/// A pure version of [`Application`]. +/// +/// Unlike the impure version, the `view` method of this trait takes an +/// immutable reference to `self` and returns a pure [`Element`]. +/// +/// [`Application`]: crate::Application +/// [`Element`]: pure::Element +pub trait Application: Sized { + /// The [`Executor`] that will run commands and subscriptions. + /// + /// The [default executor] can be a good starting point! + /// + /// [`Executor`]: Self::Executor + /// [default executor]: crate::executor::Default + type Executor: Executor; + + /// The type of __messages__ your [`Application`] will produce. + type Message: std::fmt::Debug + Send; + + /// The theme of your [`Application`]. + type Theme: Default + StyleSheet; + + /// The data needed to initialize your [`Application`]. + type Flags: Clone; + + /// Initializes the [`Application`] with the flags provided to + /// [`run`] as part of the [`Settings`]. + /// + /// Here is where you should return the initial state of your app. + /// + /// Additionally, you can return a [`Command`] if you need to perform some + /// async action in the background on startup. This is useful if you want to + /// load state from a file, perform an initial HTTP request, etc. + /// + /// [`run`]: Self::run + fn new(flags: Self::Flags) -> (Self, Command); + + /// Returns the current title of the [`Application`]. + /// + /// This title can be dynamic! The runtime will automatically update the + /// title of your application when necessary. + fn title(&self, id: Id) -> String; + + /// Handles a __message__ and updates the state of the [`Application`]. + /// + /// This is where you define your __update logic__. All the __messages__, + /// produced by either user interactions or commands, will be handled by + /// this method. + /// + /// Any [`Command`] returned will be executed immediately in the background. + fn update(&mut self, message: Self::Message) -> Command; + + /// Returns the current [`Theme`] of the [`Application`]. + /// + /// [`Theme`]: Self::Theme + fn theme(&self, id: Id) -> Self::Theme { + Self::Theme::default() + } + + /// Returns the current [`Style`] of the [`Theme`]. + /// + /// [`Style`]: ::Style + /// [`Theme`]: Self::Theme + fn style(&self) -> ::Style { + Default::default() + } + + /// Returns the event [`Subscription`] for the current state of the + /// application. + /// + /// A [`Subscription`] will be kept alive as long as you keep returning it, + /// and the __messages__ produced will be handled by + /// [`update`](#tymethod.update). + /// + /// By default, this method returns an empty [`Subscription`]. + fn subscription(&self) -> Subscription { + Subscription::none() + } + + /// Returns the widgets to display in the [`Application`]. + /// + /// These widgets can produce __messages__ based on user interaction. + fn view( + &self, + id: Id, + ) -> Element<'_, Self::Message, Self::Theme, crate::Renderer>; + + /// Returns the scale factor of the [`Application`]. + /// + /// It can be used to dynamically control the size of the UI at runtime + /// (i.e. zooming). + /// + /// For instance, a scale factor of `2.0` will make widgets twice as big, + /// while a scale factor of `0.5` will shrink them to half their size. + /// + /// By default, it returns `1.0`. + fn scale_factor(&self, id: Id) -> f64 { + 1.0 + } + + /// Runs the [`Application`]. + /// + /// On native platforms, this method will take control of the current thread + /// until the [`Application`] exits. + /// + /// On the web platform, this method __will NOT return__ unless there is an + /// [`Error`] during startup. + /// + /// [`Error`]: crate::Error + fn run(settings: Settings) -> crate::Result + where + Self: 'static, + { + #[allow(clippy::needless_update)] + let renderer_settings = crate::renderer::Settings { + default_font: settings.default_font, + default_text_size: settings.default_text_size, + antialiasing: if settings.antialiasing { + Some(Antialiasing::MSAAx4) + } else { + None + }, + ..crate::renderer::Settings::default() + }; + + Ok(crate::shell::application::run::< + Instance, + Self::Executor, + crate::renderer::Compositor, + >(settings.into(), renderer_settings)?) + } +} + +struct Instance(A); + +impl crate::runtime::multi_window::Program for Instance +where + A: Application, +{ + type Theme = A::Theme; + type Renderer = crate::Renderer; + type Message = A::Message; + + fn update(&mut self, message: Self::Message) -> Command { + self.0.update(message) + } + + fn view( + &self, + id: Id, + ) -> Element<'_, Self::Message, Self::Theme, Self::Renderer> { + self.0.view(id) + } +} + +impl crate::shell::Application for Instance +where + A: Application, +{ + type Flags = A::Flags; + + fn new(flags: Self::Flags) -> (Self, Command) { + let (app, command) = A::new(flags); + + (Instance(app), command) + } + + fn title(&self, window: Id) -> String { + self.0.title(window) + } + + fn theme(&self, id: Id) -> A::Theme { + self.0.theme(id) + } + + fn style(&self) -> ::Style { + self.0.style() + } + fn subscription(&self) -> Subscription { + self.0.subscription() + } + + fn scale_factor(&self, window: Id) -> f64 { + self.0.scale_factor(window) + } +} diff --git a/src/wayland/sandbox.rs b/src/wayland/sandbox.rs new file mode 100644 index 0000000000..ead4975350 --- /dev/null +++ b/src/wayland/sandbox.rs @@ -0,0 +1,207 @@ +use iced_core::window::Id; + +use crate::style::Theme; +use crate::theme::{self}; +use crate::{ + wayland::Application, Command, Element, Error, Settings, Subscription, +}; + +/// A sandboxed [`Application`]. +/// +/// If you are a just getting started with the library, this trait offers a +/// simpler interface than [`Application`]. +/// +/// Unlike an [`Application`], a [`Sandbox`] cannot run any asynchronous +/// actions or be initialized with some external flags. However, both traits +/// are very similar and upgrading from a [`Sandbox`] is very straightforward. +/// +/// Therefore, it is recommended to always start by implementing this trait and +/// upgrade only once necessary. +/// +/// # Examples +/// [The repository has a bunch of examples] that use the [`Sandbox`] trait: +/// +/// - [`bezier_tool`], a Paint-like tool for drawing Bézier curves using the +/// [`Canvas widget`]. +/// - [`counter`], the classic counter example explained in [the overview]. +/// - [`custom_widget`], a demonstration of how to build a custom widget that +/// draws a circle. +/// - [`geometry`], a custom widget showcasing how to draw geometry with the +/// `Mesh2D` primitive in [`iced_wgpu`]. +/// - [`pane_grid`], a grid of panes that can be split, resized, and +/// reorganized. +/// - [`progress_bar`], a simple progress bar that can be filled by using a +/// slider. +/// - [`styling`], an example showcasing custom styling with a light and dark +/// theme. +/// - [`svg`], an application that renders the [Ghostscript Tiger] by leveraging +/// the [`Svg` widget]. +/// - [`tour`], a simple UI tour that can run both on native platforms and the +/// web! +/// +/// [The repository has a bunch of examples]: https://github.com/iced-rs/iced/tree/0.4/examples +/// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.4/examples/bezier_tool +/// [`counter`]: https://github.com/iced-rs/iced/tree/0.4/examples/counter +/// [`custom_widget`]: https://github.com/iced-rs/iced/tree/0.4/examples/custom_widget +/// [`geometry`]: https://github.com/iced-rs/iced/tree/0.4/examples/geometry +/// [`pane_grid`]: https://github.com/iced-rs/iced/tree/0.4/examples/pane_grid +/// [`progress_bar`]: https://github.com/iced-rs/iced/tree/0.4/examples/progress_bar +/// [`styling`]: https://github.com/iced-rs/iced/tree/0.4/examples/styling +/// [`svg`]: https://github.com/iced-rs/iced/tree/0.4/examples/svg +/// [`tour`]: https://github.com/iced-rs/iced/tree/0.4/examples/tour +/// [`Canvas widget`]: crate::widget::Canvas +/// [the overview]: index.html#overview +/// [`iced_wgpu`]: https://github.com/iced-rs/iced/tree/0.4/wgpu +/// [`Svg` widget]: crate::widget::Svg +/// [Ghostscript Tiger]: https://commons.wikimedia.org/wiki/File:Ghostscript_Tiger.svg +/// +/// ## A simple "Hello, world!" +/// +/// If you just want to get started, here is a simple [`Sandbox`] that +/// says "Hello, world!": +/// +/// ```no_run +/// use iced::{Element, Sandbox, Settings}; +/// +/// pub fn main() -> iced::Result { +/// Hello::run(Settings::default()) +/// } +/// +/// struct Hello; +/// +/// impl Sandbox for Hello { +/// type Message = (); +/// +/// fn new() -> Hello { +/// Hello +/// } +/// +/// fn title(&self) -> String { +/// String::from("A cool application") +/// } +/// +/// fn update(&mut self, _message: Self::Message) { +/// // This application has no interactions +/// } +/// +/// fn view(&self) -> Element { +/// "Hello, world!".into() +/// } +/// } +/// ``` +pub trait Sandbox { + /// The type of __messages__ your [`Sandbox`] will produce. + type Message: std::fmt::Debug + Send; + + /// Initializes the [`Sandbox`]. + /// + /// Here is where you should return the initial state of your app. + fn new() -> Self; + + /// Returns the current title of the [`Sandbox`]. + /// + /// This title can be dynamic! The runtime will automatically update the + /// title of your application when necessary. + fn title(&self) -> String; + + /// Handles a __message__ and updates the state of the [`Sandbox`]. + /// + /// This is where you define your __update logic__. All the __messages__, + /// produced by user interactions, will be handled by this method. + fn update(&mut self, message: Self::Message); + + /// Returns the widgets to display in the [`Sandbox`] window. + /// + /// These widgets can produce __messages__ based on user interaction. + fn view(&self, id: Id) -> Element<'_, Self::Message, Theme>; + + /// Returns the current [`Theme`] of the [`Sandbox`]. + /// + /// If you want to use your own custom theme type, you will have to use an + /// [`Application`]. + /// + /// By default, it returns [`Theme::default`]. + fn theme(&self) -> Theme { + Theme::default() + } + + /// Returns the current style variant of [`theme::Application`]. + /// + /// By default, it returns [`theme::Application::default`]. + fn style(&self) -> theme::Application { + theme::Application::default() + } + + /// Returns the scale factor of the [`Sandbox`]. + /// + /// It can be used to dynamically control the size of the UI at runtime + /// (i.e. zooming). + /// + /// For instance, a scale factor of `2.0` will make widgets twice as big, + /// while a scale factor of `0.5` will shrink them to half their size. + /// + /// By default, it returns `1.0`. + fn scale_factor(&self) -> f64 { + 1.0 + } + + /// Runs the [`Sandbox`]. + /// + /// On native platforms, this method will take control of the current thread + /// and __will NOT return__. + /// + /// It should probably be that last thing you call in your `main` function. + fn run(settings: Settings<()>) -> Result<(), Error> + where + Self: 'static + Sized, + { + ::run(settings) + } +} + +impl Application for T +where + T: Sandbox, +{ + type Executor = iced_futures::backend::null::Executor; + type Flags = (); + type Message = T::Message; + type Theme = Theme; + + fn new(_flags: ()) -> (Self, Command) { + (T::new(), Command::none()) + } + + fn title(&self, _id: Id) -> String { + T::title(self) + } + + fn update(&mut self, message: T::Message) -> Command { + T::update(self, message); + + Command::none() + } + + fn theme(&self, _id: Id) -> Self::Theme { + T::theme(self) + } + + fn style(&self) -> theme::Application { + T::style(self) + } + + fn subscription(&self) -> Subscription { + Subscription::none() + } + + fn scale_factor(&self, _id: Id) -> f64 { + T::scale_factor(self) + } + + /// Returns the widgets to display in the [`Sandbox`] window. + /// + /// These widgets can produce __messages__ based on user interaction. + fn view(&self, id: Id) -> Element<'_, Self::Message> { + T::view(self, id) + } +} diff --git a/src/window.rs b/src/window.rs index 9f96da5245..1ceb3f9094 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,8 +1,13 @@ //! Configure the window of your application in native platforms. +#[cfg(feature = "winit")] pub mod icon; +#[cfg(feature = "winit")] pub use icon::Icon; +#[cfg(feature = "winit")] +pub use settings::{PlatformSpecific, Settings}; + pub use crate::core::window::*; pub use crate::runtime::window::*; diff --git a/widget/Cargo.toml b/widget/Cargo.toml index 0880477a0b..64dde4be8a 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -22,6 +22,7 @@ canvas = ["iced_renderer/geometry"] qr_code = ["canvas", "qrcode"] wgpu = ["iced_renderer/wgpu"] a11y = ["iced_accessibility"] +wayland = ["sctk"] [dependencies] iced_renderer.workspace = true @@ -29,7 +30,8 @@ iced_runtime.workspace = true iced_style.workspace = true iced_accessibility.workspace = true iced_accessibility.optional = true - +sctk.workspace = true +sctk.optional = true num-traits.workspace = true thiserror.workspace = true unicode-segmentation.workspace = true diff --git a/widget/src/dnd_listener.rs b/widget/src/dnd_listener.rs new file mode 100644 index 0000000000..1529af6a37 --- /dev/null +++ b/widget/src/dnd_listener.rs @@ -0,0 +1,511 @@ +//! A container for capturing mouse events. + +use crate::core::event::wayland::DndOfferEvent; +use crate::core::event::{self, Event, PlatformSpecific}; +use crate::core::layout; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::widget::OperationOutputWrapper; +use crate::core::widget::{tree, Operation, Tree}; +use crate::core::{ + overlay, Clipboard, Element, Layout, Length, Point, Rectangle, Shell, + Widget, +}; +use sctk::reexports::client::protocol::wl_data_device_manager::DndAction; + +use std::u32; + +/// Emit messages on mouse events. +#[allow(missing_debug_implementations)] +pub struct DndListener<'a, Message, Theme, Renderer> { + content: Element<'a, Message, Theme, Renderer>, + + /// Sets the message to emit on a drag enter. + on_enter: + Option, (f32, f32)) -> Message + 'a>>, + + /// Sets the message to emit on a drag motion. + /// x and y are the coordinates of the pointer relative to the widget in the range (0.0, 1.0) + on_motion: Option Message + 'a>>, + + /// Sets the message to emit on a drag exit. + on_exit: Option, + + /// Sets the message to emit on a drag drop. + on_drop: Option, + + /// Sets the message to emit on a drag mime type event. + on_mime_type: Option Message + 'a>>, + + /// Sets the message to emit on a drag action event. + on_source_actions: Option Message + 'a>>, + + /// Sets the message to emit on a drag action event. + on_selected_action: Option Message + 'a>>, + + /// Sets the message to emit on a Data event. + on_data: Option) -> Message + 'a>>, +} + +impl<'a, Message, Theme, Renderer> DndListener<'a, Message, Theme, Renderer> { + /// The message to emit on a drag enter. + #[must_use] + pub fn on_enter( + mut self, + message: impl Fn(DndAction, Vec, (f32, f32)) -> Message + 'a, + ) -> Self { + self.on_enter = Some(Box::new(message)); + self + } + + /// The message to emit on a drag motion. + #[must_use] + pub fn on_motion( + mut self, + message: impl Fn(f32, f32) -> Message + 'a, + ) -> Self { + self.on_motion = Some(Box::new(message)); + self + } + + /// The message to emit on a selected drag action. + #[must_use] + pub fn on_selected_action( + mut self, + message: impl Fn(DndAction) -> Message + 'a, + ) -> Self { + self.on_selected_action = Some(Box::new(message)); + self + } + + /// The message to emit on a drag exit. + #[must_use] + pub fn on_exit(mut self, message: Message) -> Self { + self.on_exit = Some(message); + self + } + + /// The message to emit on a drag drop. + #[must_use] + pub fn on_drop(mut self, message: Message) -> Self { + self.on_drop = Some(message); + self + } + + /// The message to emit on a drag mime type event. + #[must_use] + pub fn on_mime_type( + mut self, + message: impl Fn(String) -> Message + 'a, + ) -> Self { + self.on_mime_type = Some(Box::new(message)); + self + } + + /// The message to emit on a drag action event. + #[must_use] + pub fn on_action( + mut self, + message: impl Fn(DndAction) -> Message + 'a, + ) -> Self { + self.on_source_actions = Some(Box::new(message)); + self + } + + /// The message to emit on a drag read data event. + #[must_use] + pub fn on_data( + mut self, + message: impl Fn(String, Vec) -> Message + 'a, + ) -> Self { + self.on_data = Some(Box::new(message)); + self + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +enum DndState { + #[default] + None, + External(DndAction, Vec), + Hovered(DndAction, Vec), + Dropped, +} + +/// Local state of the [`DndListener`]. +#[derive(Default)] +struct State { + dnd: DndState, +} + +impl<'a, Message, Theme, Renderer> DndListener<'a, Message, Theme, Renderer> { + /// Creates an empty [`DndListener`]. + pub fn new( + content: impl Into>, + ) -> Self { + DndListener { + content: content.into(), + on_enter: None, + on_motion: None, + on_exit: None, + on_drop: None, + on_mime_type: None, + on_source_actions: None, + on_selected_action: None, + on_data: None, + } + } +} + +impl<'a, Message, Theme, Renderer> Widget + for DndListener<'a, Message, Theme, Renderer> +where + Renderer: crate::core::Renderer, + Message: Clone, +{ + fn children(&self) -> Vec { + vec![Tree::new(&self.content)] + } + + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(std::slice::from_mut(&mut self.content)); + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let size = self.size(); + + layout( + renderer, + limits, + size.width, + size.height, + u32::MAX, + u32::MAX, + |renderer, limits| { + self.content.as_widget().layout(tree, renderer, limits) + }, + ) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation>, + ) { + operation.container(None, layout.bounds(), &mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + operation, + ); + }); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + if let event::Status::Captured = self.content.as_widget_mut().on_event( + &mut tree.children[0], + event.clone(), + layout.children().next().unwrap(), + cursor_position, + renderer, + clipboard, + shell, + viewport, + ) { + return event::Status::Captured; + } + + update( + self, + &event, + layout, + shell, + tree.state.downcast_mut::(), + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout.children().next().unwrap(), + cursor_position, + viewport, + renderer, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + renderer_style: &renderer::Style, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + viewport: &Rectangle, + ) { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + renderer_style, + layout.children().next().unwrap(), + cursor_position, + viewport, + ); + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option> { + self.content.as_widget_mut().overlay( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + ) + } + + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::default()) + } + + fn size(&self) -> iced_renderer::core::Size { + self.content.as_widget().size() + } +} + +impl<'a, Message, Theme, Renderer> + From> + for Element<'a, Message, Theme, Renderer> +where + Message: 'a + Clone, + Renderer: 'a + crate::core::Renderer, + Theme: 'a, +{ + fn from( + listener: DndListener<'a, Message, Theme, Renderer>, + ) -> Element<'a, Message, Theme, Renderer> { + Element::new(listener) + } +} + +/// Processes the given [`Event`] and updates the [`State`] of an [`DndListener`] +/// accordingly. +fn update( + widget: &mut DndListener<'_, Message, Theme, Renderer>, + event: &Event, + layout: Layout<'_>, + shell: &mut Shell<'_, Message>, + state: &mut State, +) -> event::Status { + match event { + Event::PlatformSpecific(PlatformSpecific::Wayland( + event::wayland::Event::DndOffer(DndOfferEvent::Enter { + x, + y, + mime_types, + }), + )) => { + let bounds = layout.bounds(); + let p = Point { + x: *x as f32, + y: *y as f32, + }; + if layout.bounds().contains(p) { + state.dnd = + DndState::Hovered(DndAction::empty(), mime_types.clone()); + if let Some(message) = widget.on_enter.as_ref() { + let normalized_x: f32 = (p.x - bounds.x) / bounds.width; + let normalized_y: f32 = (p.y - bounds.y) / bounds.height; + shell.publish(message( + DndAction::empty(), + mime_types.clone(), + (normalized_x, normalized_y), + )); + return event::Status::Captured; + } + } else { + state.dnd = + DndState::External(DndAction::empty(), mime_types.clone()); + } + } + Event::PlatformSpecific(PlatformSpecific::Wayland( + event::wayland::Event::DndOffer(DndOfferEvent::Motion { x, y }), + )) => { + let bounds = layout.bounds(); + let p = Point { + x: *x as f32, + y: *y as f32, + }; + // motion can trigger an enter, motion or leave event on the widget + if let DndState::Hovered(action, mime_types) = &state.dnd { + if !bounds.contains(p) { + state.dnd = DndState::External(*action, mime_types.clone()); + if let Some(message) = widget.on_exit.clone() { + shell.publish(message); + return event::Status::Captured; + } + } else if let Some(message) = widget.on_motion.as_ref() { + let normalized_x: f32 = (p.x - bounds.x) / bounds.width; + let normalized_y: f32 = (p.y - bounds.y) / bounds.height; + shell.publish(message(normalized_x, normalized_y)); + return event::Status::Captured; + } + } else if bounds.contains(p) { + state.dnd = match &state.dnd { + DndState::External(a, m) => { + DndState::Hovered(*a, m.clone()) + } + _ => DndState::Hovered(DndAction::empty(), vec![]), + }; + let (action, mime_types) = match &state.dnd { + DndState::Hovered(action, mime_types) => { + (action, mime_types) + } + _ => return event::Status::Ignored, + }; + + if let Some(message) = widget.on_enter.as_ref() { + let normalized_x: f32 = (p.x - bounds.x) / bounds.width; + let normalized_y: f32 = (p.y - bounds.y) / bounds.height; + shell.publish(message( + *action, + mime_types.clone(), + (normalized_x, normalized_y), + )); + return event::Status::Captured; + } + } + } + Event::PlatformSpecific(PlatformSpecific::Wayland( + event::wayland::Event::DndOffer(DndOfferEvent::Leave), + )) => { + if !matches!(state.dnd, DndState::Dropped) { + state.dnd = DndState::None; + } + + if let Some(message) = widget.on_exit.clone() { + shell.publish(message); + return event::Status::Captured; + } + } + Event::PlatformSpecific(PlatformSpecific::Wayland( + event::wayland::Event::DndOffer(DndOfferEvent::DropPerformed), + )) => { + if matches!(state.dnd, DndState::Hovered(..)) { + state.dnd = DndState::Dropped; + } + if let Some(message) = widget.on_drop.clone() { + shell.publish(message); + return event::Status::Captured; + } + } + Event::PlatformSpecific(PlatformSpecific::Wayland( + event::wayland::Event::DndOffer(DndOfferEvent::DndData { + mime_type, + data, + }), + )) => { + match &mut state.dnd { + DndState::Hovered(_, mime_types) => { + if !mime_types.contains(mime_type) { + return event::Status::Ignored; + } + } + DndState::None | DndState::External(..) => { + return event::Status::Ignored + } + DndState::Dropped => {} + }; + if let Some(message) = widget.on_data.as_ref() { + shell.publish(message(mime_type.clone(), data.clone())); + return event::Status::Captured; + } + } + Event::PlatformSpecific(PlatformSpecific::Wayland( + event::wayland::Event::DndOffer(DndOfferEvent::SourceActions( + actions, + )), + )) => { + match &mut state.dnd { + DndState::Hovered(ref mut action, _) => *action = *actions, + DndState::External(ref mut action, _) => *action = *actions, + DndState::Dropped => {} + DndState::None => { + state.dnd = DndState::External(*actions, vec![]) + } + }; + if let Some(message) = widget.on_source_actions.as_ref() { + shell.publish(message(*actions)); + return event::Status::Captured; + } + } + Event::PlatformSpecific(PlatformSpecific::Wayland( + event::wayland::Event::DndOffer(DndOfferEvent::SelectedAction( + action, + )), + )) => { + if let Some(message) = widget.on_selected_action.as_ref() { + shell.publish(message(*action)); + return event::Status::Captured; + } + } + _ => {} + }; + event::Status::Ignored +} + +/// Computes the layout of a [`DndListener`]. +pub fn layout( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + height: Length, + max_height: u32, + max_width: u32, + layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, +) -> layout::Node { + let limits = limits + .loose() + .max_height(max_height as f32) + .max_width(max_width as f32) + .width(width) + .height(height); + + let content = layout_content(renderer, &limits); + let size = limits.resolve(width, height, content.size()); + + layout::Node::with_children(size, vec![content]) +} diff --git a/widget/src/dnd_source.rs b/widget/src/dnd_source.rs new file mode 100644 index 0000000000..bf306c64f1 --- /dev/null +++ b/widget/src/dnd_source.rs @@ -0,0 +1,423 @@ +//! A widget that can be dragged and dropped. + +use sctk::reexports::client::protocol::wl_data_device_manager::DndAction; + +use crate::core::{ + event, layout, mouse, overlay, touch, Clipboard, Element, Event, Length, + Point, Rectangle, Shell, Size, Widget, +}; + +use crate::core::widget::{ + operation::OperationOutputWrapper, tree, Operation, Tree, +}; + +/// A widget that can be dragged and dropped. +#[allow(missing_debug_implementations)] +pub struct DndSource<'a, Message, Theme, Renderer> { + content: Element<'a, Message, Theme, Renderer>, + + on_drag: Option Message + 'a>>, + + on_cancelled: Option, + + on_finished: Option, + + on_dropped: Option, + + on_selection_action: Option Message + 'a>>, + + drag_threshold: f32, + + /// Whether or not captured events should be handled by the widget. + handle_captured_events: bool, +} + +impl<'a, Message, Widget, Renderer> DndSource<'a, Message, Widget, Renderer> { + /// The message to produce when the drag starts. + /// + /// Receives the size of the source widget, so the caller is able to size the + /// drag surface to match. + #[must_use] + pub fn on_drag(mut self, f: F) -> Self + where + F: Fn(Size) -> Message + 'a, + { + self.on_drag = Some(Box::new(f)); + self + } + + /// The message to produce when the drag is cancelled. + #[must_use] + pub fn on_cancelled(mut self, message: Message) -> Self { + self.on_cancelled = Some(message); + self + } + + /// The message to produce when the drag is finished. + #[must_use] + pub fn on_finished(mut self, message: Message) -> Self { + self.on_finished = Some(message); + self + } + + /// The message to produce when the drag is dropped. + #[must_use] + pub fn on_dropped(mut self, message: Message) -> Self { + self.on_dropped = Some(message); + self + } + + /// The message to produce when the selection action is triggered. + #[must_use] + pub fn on_selection_action(mut self, f: F) -> Self + where + F: Fn(DndAction) -> Message + 'a, + { + self.on_selection_action = Some(Box::new(f)); + self + } + + /// The drag radius threshold. + /// if the mouse is moved more than this radius while pressed, the drag event is triggered + #[must_use] + pub fn drag_threshold(mut self, radius: f32) -> Self { + self.drag_threshold = radius.powi(2); + self + } + + /// Whether or not captured events should be handled by the widget. + #[must_use] + pub fn handle_captured_events( + mut self, + handle_captured_events: bool, + ) -> Self { + self.handle_captured_events = handle_captured_events; + self + } +} + +/// Local state of the [`MouseListener`]. +#[derive(Default)] +struct State { + hovered: bool, + left_pressed_position: Option, + is_dragging: bool, +} + +impl<'a, Message, Widget, Renderer> DndSource<'a, Message, Widget, Renderer> { + /// Creates a new [`DndSource`]. + #[must_use] + pub fn new( + content: impl Into>, + ) -> Self { + Self { + content: content.into(), + on_drag: None, + on_cancelled: None, + on_finished: None, + on_dropped: None, + on_selection_action: None, + drag_threshold: 25.0, + handle_captured_events: true, + } + } +} + +impl<'a, Message, Theme, Renderer> From> + for Element<'a, Message, Theme, Renderer> +where + Renderer: crate::core::Renderer + 'a, + Message: Clone + 'a, + Theme: 'a, +{ + fn from( + dnd_source: DndSource<'a, Message, Theme, Renderer>, + ) -> Element<'a, Message, Theme, Renderer> { + Element::new(dnd_source) + } +} + +impl<'a, Message, Theme, Renderer> Widget + for DndSource<'a, Message, Theme, Renderer> +where + Renderer: crate::core::Renderer, + Message: Clone, +{ + fn children(&self) -> Vec { + vec![Tree::new(&self.content)] + } + + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(std::slice::from_mut(&mut self.content)); + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let size = self.size(); + layout( + renderer, + limits, + size.width, + size.height, + u32::MAX, + u32::MAX, + |renderer, limits| { + self.content.as_widget().layout(tree, renderer, limits) + }, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + renderer_style: &crate::core::renderer::Style, + layout: crate::core::Layout<'_>, + cursor_position: mouse::Cursor, + viewport: &crate::core::Rectangle, + ) { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + renderer_style, + layout.children().next().unwrap(), + cursor_position, + viewport, + ); + } + + fn operate( + &self, + tree: &mut Tree, + layout: layout::Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation>, + ) { + operation.container(None, layout.bounds(), &mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + operation, + ); + }); + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: layout::Layout<'_>, + renderer: &Renderer, + ) -> Option> { + self.content.as_widget_mut().overlay( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + ) + } + + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::default()) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: layout::Layout<'_>, + cursor_position: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + let captured = self.content.as_widget_mut().on_event( + &mut tree.children[0], + event.clone(), + layout.children().next().unwrap(), + cursor_position, + renderer, + clipboard, + shell, + viewport, + ); + + if captured == event::Status::Captured && !self.handle_captured_events { + return event::Status::Captured; + } + + let state = tree.state.downcast_mut::(); + + if matches!( + event, + Event::PlatformSpecific(event::PlatformSpecific::Wayland( + event::wayland::Event::Seat( + event::wayland::SeatEvent::Leave, + _ + ) + )) | Event::Mouse(mouse::Event::ButtonReleased( + mouse::Button::Left + )) | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) + ) { + state.left_pressed_position = None; + return event::Status::Captured; + } + + if state.is_dragging { + if let Event::PlatformSpecific(event::PlatformSpecific::Wayland( + event::wayland::Event::DataSource( + event::wayland::DataSourceEvent::Cancelled, + ), + )) = event + { + if let Some(on_cancelled) = self.on_cancelled.clone() { + state.is_dragging = false; + shell.publish(on_cancelled); + return event::Status::Captured; + } + } + + if let Event::PlatformSpecific(event::PlatformSpecific::Wayland( + event::wayland::Event::DataSource( + event::wayland::DataSourceEvent::DndFinished, + ), + )) = event + { + if let Some(on_finished) = self.on_finished.clone() { + state.is_dragging = false; + shell.publish(on_finished); + return event::Status::Captured; + } + } + + if let Event::PlatformSpecific(event::PlatformSpecific::Wayland( + event::wayland::Event::DataSource( + event::wayland::DataSourceEvent::DndDropPerformed, + ), + )) = event + { + if let Some(on_dropped) = self.on_dropped.clone() { + shell.publish(on_dropped); + return event::Status::Captured; + } + } + + if let Event::PlatformSpecific(event::PlatformSpecific::Wayland( + event::wayland::Event::DataSource( + event::wayland::DataSourceEvent::DndActionAccepted(action), + ), + )) = event + { + if let Some(on_action) = self.on_selection_action.as_deref() { + shell.publish(on_action(action)); + return event::Status::Captured; + } + } + } + + let Some(cursor_position) = cursor_position.position() else { + return captured; + }; + + if cursor_position.x > 0.0 + && cursor_position.y > 0.0 + && !layout.bounds().contains(cursor_position) + { + // XXX if the widget is not hovered but the mouse is pressed, + // we are triggering on_drag + if let (Some(on_drag), Some(_)) = + (self.on_drag.as_ref(), state.left_pressed_position.take()) + { + shell.publish(on_drag(layout.bounds().size())); + state.is_dragging = true; + return event::Status::Captured; + }; + return captured; + } + + state.hovered = true; + if let (Some(on_drag), Some(pressed_pos)) = + (self.on_drag.as_ref(), state.left_pressed_position.clone()) + { + if cursor_position.x < 0.0 || cursor_position.y < 0.0 { + return captured; + } + let distance = (cursor_position.x - pressed_pos.x).powi(2) + + (cursor_position.y - pressed_pos.y).powi(2); + if distance > self.drag_threshold { + state.left_pressed_position = None; + state.is_dragging = true; + shell.publish(on_drag(layout.bounds().size())); + return event::Status::Captured; + } + } + + if self.on_drag.is_some() { + if let Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerPressed { .. }) = event + { + state.left_pressed_position = Some(cursor_position); + return event::Status::Captured; + } + } + + captured + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: layout::Layout<'_>, + cursor_position: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout.children().next().unwrap(), + cursor_position, + viewport, + renderer, + ) + } + + fn size(&self) -> Size { + self.content.as_widget().size() + } +} + +/// Computes the layout of a [`DndSource`]. +pub fn layout( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + height: Length, + max_height: u32, + max_width: u32, + layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, +) -> layout::Node { + let limits = limits + .loose() + .max_height(max_height as f32) + .max_width(max_width as f32) + .width(width) + .height(height); + + let content = layout_content(renderer, &limits); + let size = limits.resolve(width, height, content.size()); + + layout::Node::with_children(size, vec![content]) +} diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 2f22858786..bfc8abea91 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -22,6 +22,11 @@ use crate::toggler::{self, Toggler}; use crate::tooltip::{self, Tooltip}; use crate::{Column, MouseArea, Row, Space, Themer, VerticalSlider}; +#[cfg(feature = "wayland")] +use crate::dnd_listener::DndListener; +#[cfg(feature = "wayland")] +use crate::dnd_source::DndSource; + use std::borrow::Cow; use std::ops::RangeInclusive; @@ -367,7 +372,9 @@ pub fn image<'a, Handle>( /// [`Svg`]: crate::Svg /// [`Handle`]: crate::svg::Handle #[cfg(feature = "svg")] -pub fn svg(handle: impl Into) -> crate::Svg +pub fn svg<'a, Theme>( + handle: impl Into, +) -> crate::Svg<'a, Theme> where Theme: crate::svg::StyleSheet, { @@ -435,3 +442,25 @@ where { Themer::new(theme, content) } + +#[cfg(feature = "wayland")] +/// A container for a dnd source +pub fn dnd_source<'a, Message, Theme, Renderer>( + widget: impl Into>, +) -> DndSource<'a, Message, Theme, Renderer> +where + Renderer: core::Renderer, +{ + DndSource::new(widget) +} + +#[cfg(feature = "wayland")] +/// A container for a dnd target +pub fn dnd_listener<'a, Message, Theme, Renderer>( + widget: impl Into>, +) -> DndListener<'a, Message, Theme, Renderer> +where + Renderer: core::Renderer, +{ + DndListener::new(widget) +} diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index f4ba3f1bbf..06ae31c9a3 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -512,11 +512,11 @@ where ) }) } - fn id(&self) -> Option { + fn id(&self) -> Option { self.with_element(|element| element.as_widget().id()) } - fn set_id(&mut self, _id: iced_accessibility::Id) { + fn set_id(&mut self, _id: iced_runtime::core::id::Id) { self.with_element_mut(|element| element.as_widget_mut().set_id(_id)); } diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index 7eb28fab58..946fb159b6 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -347,7 +347,7 @@ where self.content.borrow().element.as_widget().id() } - fn set_id(&mut self, _id: iced_accessibility::Id) { + fn set_id(&mut self, _id: iced_runtime::core::id::Id) { self.content .borrow_mut() .element diff --git a/widget/src/lib.rs b/widget/src/lib.rs index cefafdbebe..d89a01058c 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -137,3 +137,9 @@ pub use qr_code::QRCode; pub use renderer::Renderer; pub use style::theme::{self, Theme}; +#[cfg(feature = "wayland")] +#[doc(no_inline)] +pub mod dnd_listener; +#[cfg(feature = "wayland")] +#[doc(no_inline)] +pub mod dnd_source; diff --git a/widget/src/slider.rs b/widget/src/slider.rs index b913f4c133..5171b4acbb 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -13,13 +13,16 @@ use crate::core::{ Shell, Size, Widget, }; -use std::borrow::Cow; use std::ops::RangeInclusive; +use iced_renderer::core::border::Radius; pub use iced_style::slider::{ Appearance, Handle, HandleShape, Rail, StyleSheet, }; +#[cfg(feature = "a11y")] +use std::borrow::Cow; + /// An horizontal bar and a handle that selects a single value from a range of /// values. /// @@ -489,16 +492,35 @@ pub fn draw( } else { theme.active(style) }; + let border_width = style + .handle + .border_width + .min(bounds.height / 2.0) + .min(bounds.width / 2.0); let (handle_width, handle_height, handle_border_radius) = match style.handle.shape { HandleShape::Circle { radius } => { - (radius * 2.0, radius * 2.0, radius.into()) + let radius = (radius) + .max(2.0 * border_width) + .min(bounds.height / 2.0) + .min(bounds.width / 2.0); + (radius * 2.0, radius * 2.0, Radius::from(radius)) } HandleShape::Rectangle { width, border_radius, - } => (f32::from(width), bounds.height, border_radius), + } => { + let width = (f32::from(width)) + .max(2.0 * border_width) + .min(bounds.width); + let height = bounds.height; + let mut border_radius: [f32; 4] = border_radius.into(); + for r in &mut border_radius { + *r = (*r).min(height / 2.0).min(width / 2.0).max(0.0); + } + (width, height, border_radius.into()) + } }; let value = value.into() as f32; @@ -517,6 +539,7 @@ pub fn draw( let rail_y = bounds.y + bounds.height / 2.0; + // rail renderer.fill_quad( renderer::Quad { bounds: Rectangle { @@ -531,6 +554,7 @@ pub fn draw( style.rail.colors.0, ); + // right rail renderer.fill_quad( renderer::Quad { bounds: Rectangle { @@ -545,11 +569,12 @@ pub fn draw( style.rail.colors.1, ); + // handle renderer.fill_quad( renderer::Quad { bounds: Rectangle { x: bounds.x + offset, - y: rail_y - handle_height / 2.0, + y: rail_y - (handle_height / 2.0), width: handle_width, height: handle_height, }, diff --git a/widget/src/svg.rs b/widget/src/svg.rs index eee5bf5a2d..6ee0dfd9c6 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -23,7 +23,7 @@ pub use svg::Handle; /// [`Svg`] images can have a considerable rendering cost when resized, /// specially when they are complex. #[allow(missing_debug_implementations)] -pub struct Svg +pub struct Svg<'a, Theme = crate::Theme> where Theme: StyleSheet, { @@ -41,7 +41,7 @@ where style: ::Style, } -impl Svg +impl<'a, Theme> Svg<'a, Theme> where Theme: StyleSheet, { @@ -138,7 +138,8 @@ where } } -impl Widget for Svg +impl<'a, Message, Theme, Renderer> Widget + for Svg<'a, Theme> where Theme: iced_style::svg::StyleSheet, Renderer: svg::Renderer, @@ -293,13 +294,13 @@ where } } -impl<'a, Message, Theme, Renderer> From> +impl<'a, Message, Theme, Renderer> From> for Element<'a, Message, Theme, Renderer> where Theme: iced_style::svg::StyleSheet + 'a, Renderer: svg::Renderer + 'a, { - fn from(icon: Svg) -> Element<'a, Message, Theme, Renderer> { + fn from(icon: Svg<'a, Theme>) -> Element<'a, Message, Theme, Renderer> { Element::new(icon) } } diff --git a/widget/src/text_input/mod.rs b/widget/src/text_input/mod.rs new file mode 100644 index 0000000000..8289bc2ba9 --- /dev/null +++ b/widget/src/text_input/mod.rs @@ -0,0 +1,10 @@ +//! Display fields that can be filled with text. +//! +//! A [`TextInput`] has some local [`State`]. +pub(crate) mod editor; +pub(crate) mod value; + +pub mod cursor; + +mod text_input; +pub use text_input::*; diff --git a/widget/src/text_input.rs b/widget/src/text_input/text_input.rs similarity index 99% rename from widget/src/text_input.rs rename to widget/src/text_input/text_input.rs index b5e45ea868..3c65e100c9 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input/text_input.rs @@ -1,16 +1,14 @@ //! Display fields that can be filled with text. //! //! A [`TextInput`] has some local [`State`]. -mod editor; -mod value; +pub use super::cursor::Cursor; +pub use super::value::Value; -pub mod cursor; - -pub use cursor::Cursor; +use super::cursor; +use super::editor; +use super::editor::Editor; +use super::value; use iced_renderer::core::widget::OperationOutputWrapper; -pub use value::Value; - -use editor::Editor; use crate::core::alignment; use crate::core::event::{self, Event}; diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index 489c9e8902..5b4652b359 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -152,6 +152,9 @@ pub fn window_event( WindowEvent::CloseRequested => { Some(Event::Window(id, window::Event::CloseRequested)) } + WindowEvent::CloseRequested => { + Some(Event::Window(id, window::Event::CloseRequested)) + } WindowEvent::CursorMoved { position, .. } => { let position = position.to_logical::(scale_factor); From 97d5dc8ba747b3f5de6dc3723b6c83df360891b2 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 30 May 2023 22:47:57 +0200 Subject: [PATCH 03/93] chore: default line height, text size, and shaping for cosmic --- core/src/renderer/null.rs | 2 +- core/src/text.rs | 2 +- core/src/widget/text.rs | 2 +- graphics/src/geometry/text.rs | 6 +++--- renderer/src/compositor.rs | 2 +- renderer/src/settings.rs | 4 ++-- src/settings.rs | 6 +++--- tiny_skia/src/settings.rs | 2 +- wgpu/src/layer.rs | 2 +- wgpu/src/settings.rs | 4 ++-- widget/src/checkbox.rs | 4 ++-- widget/src/overlay/menu.rs | 2 +- widget/src/radio.rs | 2 +- widget/src/toggler.rs | 4 ++-- 14 files changed, 22 insertions(+), 22 deletions(-) diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index 455daa42f8..55eccf6f76 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -52,7 +52,7 @@ impl text::Renderer for Null { } fn default_size(&self) -> Pixels { - Pixels(16.0) + Pixels(14.0) } fn load_font(&mut self, _font: Cow<'static, [u8]>) {} diff --git a/core/src/text.rs b/core/src/text.rs index edef79c236..3ff76be479 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -88,7 +88,7 @@ impl LineHeight { impl Default for LineHeight { fn default() -> Self { - Self::Relative(1.3) + Self::Relative(1.4) } } diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index d54149297b..32f2a8d3f7 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -50,7 +50,7 @@ where height: Length::Shrink, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, - shaping: Shaping::Basic, + shaping: Shaping::Advanced, style: Default::default(), } } diff --git a/graphics/src/geometry/text.rs b/graphics/src/geometry/text.rs index d314e85ec9..7a2cd7b45e 100644 --- a/graphics/src/geometry/text.rs +++ b/graphics/src/geometry/text.rs @@ -171,12 +171,12 @@ impl Default for Text { content: String::new(), position: Point::ORIGIN, color: Color::BLACK, - size: Pixels(16.0), - line_height: LineHeight::Relative(1.2), + size: Pixels(14.0), + line_height: LineHeight::default(), font: Font::default(), horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, - shaping: Shaping::Basic, + shaping: Shaping::Advanced, } } } diff --git a/renderer/src/compositor.rs b/renderer/src/compositor.rs index dc2c50ffd5..6dcd396620 100644 --- a/renderer/src/compositor.rs +++ b/renderer/src/compositor.rs @@ -258,7 +258,7 @@ impl Candidate { } #[cfg(not(feature = "wgpu"))] Self::Wgpu => { - panic!("`wgpu` feature was not enabled in `iced_renderer`") + panic!("wgpu is not enabled") } } } diff --git a/renderer/src/settings.rs b/renderer/src/settings.rs index 432eb8a0d4..d68c1d3566 100644 --- a/renderer/src/settings.rs +++ b/renderer/src/settings.rs @@ -9,7 +9,7 @@ pub struct Settings { /// The default size of text. /// - /// By default, it will be set to `16.0`. + /// By default, it will be set to `14.0`. pub default_text_size: Pixels, /// The antialiasing strategy that will be used for triangle primitives. @@ -22,7 +22,7 @@ impl Default for Settings { fn default() -> Settings { Settings { default_font: Font::default(), - default_text_size: Pixels(16.0), + default_text_size: Pixels(14.0), antialiasing: None, } } diff --git a/src/settings.rs b/src/settings.rs index d908b9aaaf..a41fa5cfc5 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -88,7 +88,7 @@ where id: None, flags: Default::default(), default_font: Default::default(), - default_text_size: 16.0, + default_text_size: 14.0, antialiasing: false, exit_on_close_request: true, } @@ -127,7 +127,7 @@ where flags: Default::default(), fonts: Vec::new(), default_font: Font::default(), - default_text_size: Pixels(16.0), + default_text_size: Pixels(14.0), antialiasing: false, exit_on_close_request: false, } @@ -178,7 +178,7 @@ where initial_surface: Default::default(), flags: Default::default(), default_font: Default::default(), - default_text_size: Pixels(16.0), + default_text_size: Pixels(14.0), antialiasing: false, fonts: Vec::new(), exit_on_close_request: true, diff --git a/tiny_skia/src/settings.rs b/tiny_skia/src/settings.rs index ec27b2186e..f3e10edabd 100644 --- a/tiny_skia/src/settings.rs +++ b/tiny_skia/src/settings.rs @@ -18,7 +18,7 @@ impl Default for Settings { fn default() -> Settings { Settings { default_font: Font::default(), - default_text_size: Pixels(16.0), + default_text_size: Pixels(14.0), } } } diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index e213c95f18..1d30bbfeb1 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -74,8 +74,8 @@ impl<'a> Layer<'a> { font: Font::MONOSPACE, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, - shaping: core::text::Shaping::Basic, clip_bounds: Rectangle::with_size(Size::INFINITY), + shaping: core::text::Shaping::Advanced, }; overlay.text.push(Text::Cached(text.clone())); diff --git a/wgpu/src/settings.rs b/wgpu/src/settings.rs index c9338fec3f..04119c37b8 100644 --- a/wgpu/src/settings.rs +++ b/wgpu/src/settings.rs @@ -20,7 +20,7 @@ pub struct Settings { /// The default size of text. /// - /// By default, it will be set to `16.0`. + /// By default, it will be set to `14.0`. pub default_text_size: Pixels, /// The antialiasing strategy that will be used for triangle primitives. @@ -59,7 +59,7 @@ impl Default for Settings { present_mode: wgpu::PresentMode::AutoVsync, internal_backend: wgpu::Backends::all(), default_font: Font::default(), - default_text_size: Pixels(16.0), + default_text_size: Pixels(14.0), antialiasing: None, } } diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index b7f5878cac..af90585e6d 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -105,14 +105,14 @@ where spacing: Self::DEFAULT_SPACING, text_size: None, text_line_height: text::LineHeight::default(), - text_shaping: text::Shaping::Basic, + text_shaping: text::Shaping::Advanced, font: None, icon: Icon { font: Renderer::ICON_FONT, code_point: Renderer::CHECKMARK_ICON, size: None, line_height: text::LineHeight::default(), - shaping: text::Shaping::Basic, + shaping: text::Shaping::Advanced, }, style: Default::default(), } diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index 161c2a1ce5..e342468bac 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -69,7 +69,7 @@ where padding: Padding::ZERO, text_size: None, text_line_height: text::LineHeight::default(), - text_shaping: text::Shaping::Basic, + text_shaping: text::Shaping::Advanced, font: None, style: Default::default(), } diff --git a/widget/src/radio.rs b/widget/src/radio.rs index 68e9bc7eff..cbc8d2924b 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -126,7 +126,7 @@ where spacing: Self::DEFAULT_SPACING, //15 text_size: None, text_line_height: text::LineHeight::default(), - text_shaping: text::Shaping::Basic, + text_shaping: text::Shaping::Advanced, font: None, style: Default::default(), } diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index 260df6ddea..56e5587fec 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -108,8 +108,8 @@ where text_size: None, text_line_height: text::LineHeight::default(), text_alignment: alignment::Horizontal::Left, - text_shaping: text::Shaping::Basic, - spacing: Self::DEFAULT_SIZE / 2.0, + text_shaping: text::Shaping::Advanced, + spacing: 0.0, font: None, style: Default::default(), } From 33e6beee973f54aff3d372073bd293b1a1174cea Mon Sep 17 00:00:00 2001 From: Victoria Brekenfeld <4404502+Drakulix@users.noreply.github.com> Date: Tue, 13 Jun 2023 18:41:25 +0200 Subject: [PATCH 04/93] runtime: Handle widget operations in `program::State` helper (#46) --- runtime/src/program/state.rs | 63 +++++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/runtime/src/program/state.rs b/runtime/src/program/state.rs index 5ccc9090a1..f084370f47 100644 --- a/runtime/src/program/state.rs +++ b/runtime/src/program/state.rs @@ -1,3 +1,4 @@ +use iced_core::widget::operation::{OperationWrapper, Outcome}; use iced_core::widget::OperationOutputWrapper; use crate::core::event::{self, Event}; @@ -6,7 +7,7 @@ use crate::core::renderer; use crate::core::widget::operation::{self, Operation}; use crate::core::{Clipboard, Size}; use crate::user_interface::{self, UserInterface}; -use crate::{Command, Debug, Program}; +use crate::{command::Action, Command, Debug, Program}; /// The execution state of a [`Program`]. It leverages caching, event /// processing, and rendering primitive storage. @@ -100,7 +101,7 @@ where style: &renderer::Style, clipboard: &mut dyn Clipboard, debug: &mut Debug, - ) -> (Vec, Option>) { + ) -> (Vec, Vec>) { let mut user_interface = build_user_interface( id, &mut self.program, @@ -135,7 +136,7 @@ where messages.append(&mut self.queued_messages); debug.event_processing_finished(); - let command = if messages.is_empty() { + let actions = if messages.is_empty() { debug.draw_started(); self.mouse_interaction = user_interface.draw(renderer, theme, style, cursor); @@ -143,13 +144,13 @@ where self.cache = Some(user_interface.into_cache()); - None + Vec::new() } else { // When there are messages, we are forced to rebuild twice // for now :^) let temp_cache = user_interface.into_cache(); - let commands = + let (actions, widget_actions) = Command::batch(messages.into_iter().map(|message| { debug.log_message(&message); @@ -158,7 +159,12 @@ where debug.update_finished(); command - })); + })) + .actions() + .into_iter() + .partition::, _>(|action| { + !matches!(action, Action::Widget(_)) + }); let mut user_interface = build_user_interface( id, @@ -169,6 +175,47 @@ where debug, ); + let had_operations = !widget_actions.is_empty(); + for operation in widget_actions + .into_iter() + .map(|action| match action { + Action::Widget(widget_action) => widget_action, + _ => unreachable!(), + }) + .map(OperationWrapper::Message) + { + let mut current_operation = Some(operation); + while let Some(mut operation) = current_operation.take() { + user_interface.operate(renderer, &mut operation); + match operation.finish() { + Outcome::Some(OperationOutputWrapper::Message( + message, + )) => self.queued_messages.push(message), + Outcome::Chain(op) => { + current_operation = + Some(OperationWrapper::Wrapper(op)); + } + _ => {} + }; + } + } + + let mut user_interface = if had_operations { + // When there were operations, we are forced to rebuild thrice ... + let temp_cache = user_interface.into_cache(); + + build_user_interface( + id, + &mut self.program, + temp_cache, + renderer, + bounds, + debug, + ) + } else { + user_interface + }; + debug.draw_started(); self.mouse_interaction = user_interface.draw(renderer, theme, style, cursor); @@ -176,10 +223,10 @@ where self.cache = Some(user_interface.into_cache()); - Some(commands) + actions }; - (uncaptured_events, command) + (uncaptured_events, actions) } /// Applies [`Operation`]s to the [`State`] From 9d9da78203c46ee111ecd8c56b621dc2e16818e7 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 10 Jul 2023 13:00:33 -0400 Subject: [PATCH 05/93] fix: reset button state if the cursor leaves --- widget/src/button.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/widget/src/button.rs b/widget/src/button.rs index f370d729c0..d410652372 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -555,7 +555,8 @@ pub fn update<'a, Message: Clone>( } } } - Event::Touch(touch::Event::FingerLost { .. }) => { + Event::Touch(touch::Event::FingerLost { .. }) + | Event::Mouse(mouse::Event::CursorLeft) => { let state = state(); state.is_hovered = false; state.is_pressed = false; From 9497cdb6102fd44cbfeb48152be92b32140825e5 Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Sat, 18 Feb 2023 14:31:38 -0800 Subject: [PATCH 06/93] Introduce internal `overlay::Nested` for `UserInterface` --- runtime/src/user_interface/overlay.rs | 292 ++++++++++++++++++++++++++ src/settings.rs | 4 +- 2 files changed, 295 insertions(+), 1 deletion(-) create mode 100644 runtime/src/user_interface/overlay.rs diff --git a/runtime/src/user_interface/overlay.rs b/runtime/src/user_interface/overlay.rs new file mode 100644 index 0000000000..21120d86ef --- /dev/null +++ b/runtime/src/user_interface/overlay.rs @@ -0,0 +1,292 @@ +use crate::core::event; +use crate::core::layout; +use crate::core::mouse; +use crate::core::overlay; +use crate::core::renderer; +use crate::core::widget; +use crate::core::{ + Clipboard, Event, Layout, Overlay, Point, Rectangle, Shell, Size, +}; + +use std::cell::RefCell; + +/// An [`Overlay`] container that displays nested overlays +#[allow(missing_debug_implementations)] +pub struct Nested<'a, Message, Renderer> { + overlay: Inner<'a, Message, Renderer>, +} + +impl<'a, Message, Renderer> Nested<'a, Message, Renderer> { + /// Creates a nested overlay from the provided [`overlay::Element`] + pub fn new(element: overlay::Element<'a, Message, Renderer>) -> Self { + Self { + overlay: Inner(RefCell::new(element)), + } + } +} + +struct Inner<'a, Message, Renderer>( + RefCell>, +); + +impl<'a, Message, Renderer> Inner<'a, Message, Renderer> { + fn with_element_mut( + &self, + mut f: impl FnMut(&mut overlay::Element<'_, Message, Renderer>) -> T, + ) -> T { + (f)(&mut self.0.borrow_mut()) + } +} + +impl<'a, Message, Renderer> Overlay + for Nested<'a, Message, Renderer> +where + Renderer: renderer::Renderer, +{ + fn layout( + &self, + renderer: &Renderer, + bounds: Size, + position: Point, + ) -> layout::Node { + fn recurse( + element: &mut overlay::Element<'_, Message, Renderer>, + renderer: &Renderer, + bounds: Size, + position: Point, + ) -> Vec + where + Renderer: renderer::Renderer, + { + let translation = position - Point::ORIGIN; + + let node = element.layout(renderer, bounds, translation); + + if let Some(mut overlay) = + element.overlay(Layout::new(&node), renderer) + { + vec![node] + .into_iter() + .chain(recurse(&mut overlay, renderer, bounds, position)) + .collect() + } else { + vec![node] + } + } + + self.overlay.with_element_mut(|element| { + layout::Node::with_children( + bounds, + recurse(element, renderer, bounds, position), + ) + }) + } + + fn draw( + &self, + renderer: &mut Renderer, + theme: &::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + ) { + fn recurse<'a, Message, Renderer>( + element: &mut overlay::Element<'_, Message, Renderer>, + mut layouts: impl Iterator>, + renderer: &mut Renderer, + theme: &::Theme, + style: &renderer::Style, + cursor: mouse::Cursor, + ) where + Renderer: renderer::Renderer, + { + let layout = layouts.next().unwrap(); + + element.draw(renderer, theme, style, layout, cursor); + + if let Some(mut overlay) = element.overlay(layout, renderer) { + recurse(&mut overlay, layouts, renderer, theme, style, cursor); + } + } + + self.overlay.with_element_mut(|element| { + let layouts = layout.children(); + + recurse(element, layouts, renderer, theme, style, cursor); + }) + } + + fn operate( + &mut self, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation< + widget::OperationOutputWrapper, + >, + ) { + fn recurse<'a, Message, Renderer>( + element: &mut overlay::Element<'_, Message, Renderer>, + mut layouts: impl Iterator>, + renderer: &Renderer, + operation: &mut dyn widget::Operation< + widget::OperationOutputWrapper, + >, + ) where + Renderer: renderer::Renderer, + { + let layout = layouts.next().unwrap(); + + element.operate(layout, renderer, operation); + + if let Some(mut overlay) = element.overlay(layout, renderer) { + recurse(&mut overlay, layouts, renderer, operation); + } + } + + let layouts = layout.children(); + + recurse(self.overlay.0.get_mut(), layouts, renderer, operation) + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + fn recurse<'a, Message, Renderer>( + element: &mut overlay::Element<'_, Message, Renderer>, + mut layouts: impl Iterator>, + event: Event, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status + where + Renderer: renderer::Renderer, + { + let layout = layouts.next().unwrap(); + + let status = + if let Some(mut overlay) = element.overlay(layout, renderer) { + recurse( + &mut overlay, + layouts, + event.clone(), + cursor, + renderer, + clipboard, + shell, + ) + } else { + event::Status::Ignored + }; + + if matches!(status, event::Status::Ignored) { + element + .on_event(event, layout, cursor, renderer, clipboard, shell) + } else { + status + } + } + + let layouts = layout.children(); + + recurse( + self.overlay.0.get_mut(), + layouts, + event, + cursor, + renderer, + clipboard, + shell, + ) + } + + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + fn recurse<'a, Message, Renderer>( + element: &mut overlay::Element<'_, Message, Renderer>, + mut layouts: impl Iterator>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction + where + Renderer: renderer::Renderer, + { + let layout = layouts.next().unwrap(); + + let interaction = + if let Some(mut overlay) = element.overlay(layout, renderer) { + recurse(&mut overlay, layouts, cursor, viewport, renderer) + } else { + mouse::Interaction::default() + }; + + element + .mouse_interaction(layout, cursor, viewport, renderer) + .max(interaction) + } + + self.overlay.with_element_mut(|element| { + let layouts = layout.children(); + + recurse(element, layouts, cursor, viewport, renderer) + }) + } + + fn is_over( + &self, + layout: Layout<'_>, + renderer: &Renderer, + cursor_position: Point, + ) -> bool { + fn recurse<'a, Message, Renderer>( + element: &mut overlay::Element<'_, Message, Renderer>, + mut layouts: impl Iterator>, + renderer: &Renderer, + cursor_position: Point, + ) -> bool + where + Renderer: renderer::Renderer, + { + let layout = layouts.next().unwrap(); + + let is_over = element.is_over(layout, renderer, cursor_position); + + if is_over { + return true; + } + + if let Some(mut overlay) = element.overlay(layout, renderer) { + recurse(&mut overlay, layouts, renderer, cursor_position) + } else { + false + } + } + + self.overlay.with_element_mut(|element| { + let layouts = layout.children(); + + recurse(element, layouts, renderer, cursor_position) + }) + } + + fn overlay<'b>( + &'b mut self, + _layout: Layout<'_>, + _renderer: &Renderer, + ) -> Option> { + None + } +} diff --git a/src/settings.rs b/src/settings.rs index a41fa5cfc5..1cb56290ee 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -70,6 +70,7 @@ impl Settings { Self { flags, id: default_settings.id, + fonts: default_settings.fonts, default_font: default_settings.default_font, default_text_size: default_settings.default_text_size, antialiasing: default_settings.antialiasing, @@ -88,7 +89,8 @@ where id: None, flags: Default::default(), default_font: Default::default(), - default_text_size: 14.0, + default_text_size: iced_core::Pixels(14.0), + fonts: Vec::new(), antialiasing: false, exit_on_close_request: true, } From ebe3374aa4987c7b5547e0aca9eb08b46196b6bc Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Sat, 18 Feb 2023 16:08:05 -0800 Subject: [PATCH 07/93] Use layout with children for nesting --- runtime/src/user_interface/overlay.rs | 177 +++++++++++++++++--------- 1 file changed, 115 insertions(+), 62 deletions(-) diff --git a/runtime/src/user_interface/overlay.rs b/runtime/src/user_interface/overlay.rs index 21120d86ef..cd2abcfbb3 100644 --- a/runtime/src/user_interface/overlay.rs +++ b/runtime/src/user_interface/overlay.rs @@ -54,7 +54,7 @@ where renderer: &Renderer, bounds: Size, position: Point, - ) -> Vec + ) -> layout::Node where Renderer: renderer::Renderer, { @@ -62,23 +62,23 @@ where let node = element.layout(renderer, bounds, translation); - if let Some(mut overlay) = + if let Some(mut nested) = element.overlay(Layout::new(&node), renderer) { - vec![node] - .into_iter() - .chain(recurse(&mut overlay, renderer, bounds, position)) - .collect() + layout::Node::with_children( + node.size(), + vec![ + node, + recurse(&mut nested, renderer, bounds, position), + ], + ) } else { - vec![node] + layout::Node::with_children(node.size(), vec![node]) } } self.overlay.with_element_mut(|element| { - layout::Node::with_children( - bounds, - recurse(element, renderer, bounds, position), - ) + recurse(element, renderer, bounds, position) }) } @@ -90,9 +90,9 @@ where layout: Layout<'_>, cursor: mouse::Cursor, ) { - fn recurse<'a, Message, Renderer>( + fn recurse( element: &mut overlay::Element<'_, Message, Renderer>, - mut layouts: impl Iterator>, + layout: Layout<'_>, renderer: &mut Renderer, theme: &::Theme, style: &renderer::Style, @@ -100,19 +100,44 @@ where ) where Renderer: renderer::Renderer, { - let layout = layouts.next().unwrap(); + let mut layouts = layout.children(); + + if let Some(layout) = layouts.next() { + let nested_layout = layouts.next(); + + let is_over = cursor + .position() + .zip(nested_layout) + .and_then(|(cursor_position, nested_layout)| { + element.overlay(layout, renderer).map(|nested| { + nested.is_over( + nested_layout, + renderer, + cursor_position, + ) + }) + }) + .unwrap_or_default(); element.draw(renderer, theme, style, layout, cursor); - if let Some(mut overlay) = element.overlay(layout, renderer) { - recurse(&mut overlay, layouts, renderer, theme, style, cursor); + if let Some((mut nested, nested_layout)) = + element.overlay(layout, renderer).zip(nested_layout) + { + recurse( + &mut nested, + nested_layout, + renderer, + theme, + style, + cursor, + ); + } } } self.overlay.with_element_mut(|element| { - let layouts = layout.children(); - - recurse(element, layouts, renderer, theme, style, cursor); + recurse(element, layout, renderer, theme, style, cursor); }) } @@ -124,9 +149,9 @@ where widget::OperationOutputWrapper, >, ) { - fn recurse<'a, Message, Renderer>( + fn recurse( element: &mut overlay::Element<'_, Message, Renderer>, - mut layouts: impl Iterator>, + layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn widget::Operation< widget::OperationOutputWrapper, @@ -134,18 +159,20 @@ where ) where Renderer: renderer::Renderer, { - let layout = layouts.next().unwrap(); + let mut layouts = layout.children(); - element.operate(layout, renderer, operation); + if let Some(layout) = layouts.next() { + element.operate(layout, renderer, operation); - if let Some(mut overlay) = element.overlay(layout, renderer) { - recurse(&mut overlay, layouts, renderer, operation); + if let Some((mut nested, nested_layout)) = + element.overlay(layout, renderer).zip(layouts.next()) + { + recurse(&mut nested, nested_layout, renderer, operation); + } } } - let layouts = layout.children(); - - recurse(self.overlay.0.get_mut(), layouts, renderer, operation) + recurse(self.overlay.0.get_mut(), layout, renderer, operation) } fn on_event( @@ -157,9 +184,9 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> event::Status { - fn recurse<'a, Message, Renderer>( + fn recurse( element: &mut overlay::Element<'_, Message, Renderer>, - mut layouts: impl Iterator>, + layout: Layout<'_>, event: Event, cursor: mouse::Cursor, renderer: &Renderer, @@ -169,13 +196,15 @@ where where Renderer: renderer::Renderer, { - let layout = layouts.next().unwrap(); + let mut layouts = layout.children(); - let status = - if let Some(mut overlay) = element.overlay(layout, renderer) { + if let Some(layout) = layouts.next() { + let status = if let Some((mut nested, nested_layout)) = + element.overlay(layout, renderer).zip(layouts.next()) + { recurse( - &mut overlay, - layouts, + &mut nested, + nested_layout, event.clone(), cursor, renderer, @@ -194,11 +223,9 @@ where } } - let layouts = layout.children(); - recurse( self.overlay.0.get_mut(), - layouts, + layout, event, cursor, renderer, @@ -214,9 +241,9 @@ where viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - fn recurse<'a, Message, Renderer>( + fn recurse( element: &mut overlay::Element<'_, Message, Renderer>, - mut layouts: impl Iterator>, + layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, @@ -224,7 +251,10 @@ where where Renderer: renderer::Renderer, { - let layout = layouts.next().unwrap(); + let mut layouts = layout.children(); + + let layout = layouts.next()?; + let cursor_position = cursor.position()?; let interaction = if let Some(mut overlay) = element.overlay(layout, renderer) { @@ -233,16 +263,32 @@ where mouse::Interaction::default() }; - element - .mouse_interaction(layout, cursor, viewport, renderer) - .max(interaction) + Some( + element + .overlay(layout, renderer) + .zip(layouts.next()) + .and_then(|(mut overlay, layout)| { + recurse( + &mut overlay, + layout, + cursor, + viewport, + renderer, + ) + }) + .unwrap_or_else(|| { + element.mouse_interaction( + layout, cursor, viewport, renderer, + ) + }), + ) } - self.overlay.with_element_mut(|element| { - let layouts = layout.children(); - - recurse(element, layouts, cursor, viewport, renderer) - }) + self.overlay + .with_element_mut(|element| { + recurse(element, layout, cursor, viewport, renderer) + }) + .unwrap_or_default() } fn is_over( @@ -251,34 +297,41 @@ where renderer: &Renderer, cursor_position: Point, ) -> bool { - fn recurse<'a, Message, Renderer>( + fn recurse( element: &mut overlay::Element<'_, Message, Renderer>, - mut layouts: impl Iterator>, + layout: Layout<'_>, renderer: &Renderer, cursor_position: Point, ) -> bool where Renderer: renderer::Renderer, { - let layout = layouts.next().unwrap(); + let mut layouts = layout.children(); - let is_over = element.is_over(layout, renderer, cursor_position); + if let Some(layout) = layouts.next() { + if element.is_over(layout, renderer, cursor_position) { + return true; + } - if is_over { - return true; - } - - if let Some(mut overlay) = element.overlay(layout, renderer) { - recurse(&mut overlay, layouts, renderer, cursor_position) + if let Some((mut nested, nested_layout)) = + element.overlay(layout, renderer).zip(layouts.next()) + { + recurse( + &mut nested, + nested_layout, + renderer, + cursor_position, + ) + } else { + false + } } else { false } } self.overlay.with_element_mut(|element| { - let layouts = layout.children(); - - recurse(element, layouts, renderer, cursor_position) + recurse(element, layout, renderer, cursor_position) }) } From a82a39847ff3f7fd071b806c6412d86b62a8a302 Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Sun, 19 Feb 2023 17:43:13 -0800 Subject: [PATCH 08/93] Use nested for lazy widgets --- runtime/src/user_interface/overlay.rs | 345 -------------------------- 1 file changed, 345 deletions(-) delete mode 100644 runtime/src/user_interface/overlay.rs diff --git a/runtime/src/user_interface/overlay.rs b/runtime/src/user_interface/overlay.rs deleted file mode 100644 index cd2abcfbb3..0000000000 --- a/runtime/src/user_interface/overlay.rs +++ /dev/null @@ -1,345 +0,0 @@ -use crate::core::event; -use crate::core::layout; -use crate::core::mouse; -use crate::core::overlay; -use crate::core::renderer; -use crate::core::widget; -use crate::core::{ - Clipboard, Event, Layout, Overlay, Point, Rectangle, Shell, Size, -}; - -use std::cell::RefCell; - -/// An [`Overlay`] container that displays nested overlays -#[allow(missing_debug_implementations)] -pub struct Nested<'a, Message, Renderer> { - overlay: Inner<'a, Message, Renderer>, -} - -impl<'a, Message, Renderer> Nested<'a, Message, Renderer> { - /// Creates a nested overlay from the provided [`overlay::Element`] - pub fn new(element: overlay::Element<'a, Message, Renderer>) -> Self { - Self { - overlay: Inner(RefCell::new(element)), - } - } -} - -struct Inner<'a, Message, Renderer>( - RefCell>, -); - -impl<'a, Message, Renderer> Inner<'a, Message, Renderer> { - fn with_element_mut( - &self, - mut f: impl FnMut(&mut overlay::Element<'_, Message, Renderer>) -> T, - ) -> T { - (f)(&mut self.0.borrow_mut()) - } -} - -impl<'a, Message, Renderer> Overlay - for Nested<'a, Message, Renderer> -where - Renderer: renderer::Renderer, -{ - fn layout( - &self, - renderer: &Renderer, - bounds: Size, - position: Point, - ) -> layout::Node { - fn recurse( - element: &mut overlay::Element<'_, Message, Renderer>, - renderer: &Renderer, - bounds: Size, - position: Point, - ) -> layout::Node - where - Renderer: renderer::Renderer, - { - let translation = position - Point::ORIGIN; - - let node = element.layout(renderer, bounds, translation); - - if let Some(mut nested) = - element.overlay(Layout::new(&node), renderer) - { - layout::Node::with_children( - node.size(), - vec![ - node, - recurse(&mut nested, renderer, bounds, position), - ], - ) - } else { - layout::Node::with_children(node.size(), vec![node]) - } - } - - self.overlay.with_element_mut(|element| { - recurse(element, renderer, bounds, position) - }) - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - ) { - fn recurse( - element: &mut overlay::Element<'_, Message, Renderer>, - layout: Layout<'_>, - renderer: &mut Renderer, - theme: &::Theme, - style: &renderer::Style, - cursor: mouse::Cursor, - ) where - Renderer: renderer::Renderer, - { - let mut layouts = layout.children(); - - if let Some(layout) = layouts.next() { - let nested_layout = layouts.next(); - - let is_over = cursor - .position() - .zip(nested_layout) - .and_then(|(cursor_position, nested_layout)| { - element.overlay(layout, renderer).map(|nested| { - nested.is_over( - nested_layout, - renderer, - cursor_position, - ) - }) - }) - .unwrap_or_default(); - - element.draw(renderer, theme, style, layout, cursor); - - if let Some((mut nested, nested_layout)) = - element.overlay(layout, renderer).zip(nested_layout) - { - recurse( - &mut nested, - nested_layout, - renderer, - theme, - style, - cursor, - ); - } - } - } - - self.overlay.with_element_mut(|element| { - recurse(element, layout, renderer, theme, style, cursor); - }) - } - - fn operate( - &mut self, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn widget::Operation< - widget::OperationOutputWrapper, - >, - ) { - fn recurse( - element: &mut overlay::Element<'_, Message, Renderer>, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn widget::Operation< - widget::OperationOutputWrapper, - >, - ) where - Renderer: renderer::Renderer, - { - let mut layouts = layout.children(); - - if let Some(layout) = layouts.next() { - element.operate(layout, renderer, operation); - - if let Some((mut nested, nested_layout)) = - element.overlay(layout, renderer).zip(layouts.next()) - { - recurse(&mut nested, nested_layout, renderer, operation); - } - } - } - - recurse(self.overlay.0.get_mut(), layout, renderer, operation) - } - - fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - fn recurse( - element: &mut overlay::Element<'_, Message, Renderer>, - layout: Layout<'_>, - event: Event, - cursor: mouse::Cursor, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status - where - Renderer: renderer::Renderer, - { - let mut layouts = layout.children(); - - if let Some(layout) = layouts.next() { - let status = if let Some((mut nested, nested_layout)) = - element.overlay(layout, renderer).zip(layouts.next()) - { - recurse( - &mut nested, - nested_layout, - event.clone(), - cursor, - renderer, - clipboard, - shell, - ) - } else { - event::Status::Ignored - }; - - if matches!(status, event::Status::Ignored) { - element - .on_event(event, layout, cursor, renderer, clipboard, shell) - } else { - status - } - } - - recurse( - self.overlay.0.get_mut(), - layout, - event, - cursor, - renderer, - clipboard, - shell, - ) - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - fn recurse( - element: &mut overlay::Element<'_, Message, Renderer>, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction - where - Renderer: renderer::Renderer, - { - let mut layouts = layout.children(); - - let layout = layouts.next()?; - let cursor_position = cursor.position()?; - - let interaction = - if let Some(mut overlay) = element.overlay(layout, renderer) { - recurse(&mut overlay, layouts, cursor, viewport, renderer) - } else { - mouse::Interaction::default() - }; - - Some( - element - .overlay(layout, renderer) - .zip(layouts.next()) - .and_then(|(mut overlay, layout)| { - recurse( - &mut overlay, - layout, - cursor, - viewport, - renderer, - ) - }) - .unwrap_or_else(|| { - element.mouse_interaction( - layout, cursor, viewport, renderer, - ) - }), - ) - } - - self.overlay - .with_element_mut(|element| { - recurse(element, layout, cursor, viewport, renderer) - }) - .unwrap_or_default() - } - - fn is_over( - &self, - layout: Layout<'_>, - renderer: &Renderer, - cursor_position: Point, - ) -> bool { - fn recurse( - element: &mut overlay::Element<'_, Message, Renderer>, - layout: Layout<'_>, - renderer: &Renderer, - cursor_position: Point, - ) -> bool - where - Renderer: renderer::Renderer, - { - let mut layouts = layout.children(); - - if let Some(layout) = layouts.next() { - if element.is_over(layout, renderer, cursor_position) { - return true; - } - - if let Some((mut nested, nested_layout)) = - element.overlay(layout, renderer).zip(layouts.next()) - { - recurse( - &mut nested, - nested_layout, - renderer, - cursor_position, - ) - } else { - false - } - } else { - false - } - } - - self.overlay.with_element_mut(|element| { - recurse(element, layout, renderer, cursor_position) - }) - } - - fn overlay<'b>( - &'b mut self, - _layout: Layout<'_>, - _renderer: &Renderer, - ) -> Option> { - None - } -} From 701038b276c93ac0bf9fb6d3fbacdb02bcddb614 Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Thu, 22 Jun 2023 19:16:45 -0700 Subject: [PATCH 09/93] Provide access to font from each crate --- tiny_skia/fonts/Iced-Icons.ttf | Bin 0 -> 5108 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tiny_skia/fonts/Iced-Icons.ttf diff --git a/tiny_skia/fonts/Iced-Icons.ttf b/tiny_skia/fonts/Iced-Icons.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e3273141c45fff98c2f411b421e2e3fd225668aa GIT binary patch literal 5108 zcmds5Uu;`f8UN1ppQLdbdrRt;xn;+@Nt(vjPU^O4)@GgOKk?dh%t`;?+X3^9?fmJI z*f`#35f2Cisw_Z*2LwFufT%PHgpQLTm5zr>yub@D2#J4UlMvE235kaxjY+KhzI(6T zrAwK#N1WXI{l4$~&Ue1^o$s9MYZ62h!p%!GJA3h&AKd-wYea*8fc0QLo13M>bO8D- z=bKS{>-+tL7@}!6Y z^<`_b4togx6lCx6+V!RUTfhAh_!a1PR;pIz(0}$C@YN4}Y6S-Wzdc3hS?I$n>&~yQJ74+B_c7!!VwkVjHk)s~k}Y8T zYiOUYH>&l&KNb5W(E!Kub>dv$I{CxbUVT0|^DlbT^Cm3xr#o+7X8A_tbsk4}Zff6y z>?Mt@_@Iwx;qF!3ZNBHG?a66#?mpY%qp;hekso*yjR`trQ-tRs@iO^{v@dFih`!-S z%=Df%G;Nx)?U1aB8iUEzq?+XRNoL+D z6eWUaSyJ^CiX~VixAifaVl-8XluXl%C=oMFT~VP}HO;v47&0f7cf>+KzD%L0e0o~> z^)$jal__p7f1iTJyzHA!03Ov7s5uTJga$NZg)su;Bp_uH{_@U})9Vyn+meLlNlqkc$@{Pw;k0Ck8K&E`lON*c& z%v8x_N-1jx*=ua|1SpeBk4L-7-ec@cexKt-#1KLTGcL(oOSgC->`YOFv#DeRL3W~4 zo)O*3Izt2=&|M9q7dm&R_Q6j5Tr+xlE70f36{8V7YK}+aYOm4Ow4AC~*|-`oFaar5 zf94#|3qVhsYA9Smaqth5ToxJXgD>2^Qg_Tr{Q7%Sl!DHPjW zxky&Jh2`a8$D=yBb#TOK=PsyyZZ$EJJmSxnu!uXB>H!Pb#v!dE#Ti9swuBINH4GZm zCM|cdsDgT0=2QSh*@uBkX<5SXKM&zSHbCigtJHqjA5)E3}`H0c>vy2$+9+n4EM zR53%1$J98si-Ahbqoj7-FS<3Y^I7#j8K{@Iig(vQgq1O_#J)*8%ZU$WWoOfbAy3$G zPJ$zHAKtDw1$GUK?G%31esW~b=+o^9u@5sjig_c@UAvS}wbpc8C5^=XnYcRkQB+h| z8T8{=3JQlc0z*8)qK2>oFnr4PpEfI6}gxe*D`MwI>s+wi3FO zGcEXO(k&$g})bog$<^!%5MHJQ=HkGe*l;5#W-Ejd_0`3nAgu)n~2gh>j6vF)s*v6!_a^5Cp;}p%=Rw zx%Z)vaBklh2|QZpJYoldJ02r^gxwnpRdSMZAThdc)XjlDgVaphX4Q+`E9I=7=fQZj z9=B7*6L2$17mEowg9jf+>>AchoV(%qFojB$0!jUdxmni4;I z@1J64DNh@^OmNm_aq7;PiFQm3VdKqo%~;KQ<|n#lo89TUgLB4ECt{tJ9ZsJ#)Ksj6 z8scJUVLN_kU@S{Q#V~%#P8SzoC%ar+I(9_@o5YFnDI69!pu3gYneRuJVt%*6_&C+a zdE;|sMxTyEKd|0W(~U9B$>=0C!}E`F<~c-n9^ENtG39;E=ES2M`*1L!j$$w79$=rv zSu2JHRT7-Mp^ig5%~{JKuW}a8tWKWJ8eER*X{7a>u|@P0z#M>JaNgJww&fWB+wv*q zq|x*&a~Kqu!(g5{3>KJk2H*m77+hoygGPJkJ~kCFU@&n8RSvP$#=-F0-Oe!(6e^3{cgsCzzjrvSg^~E_<03 zo4sNqX0O_asaFm4c$fMDD>ik_Moe9|5mPq|b*f9Pv0_u}He%}MZN$`uvAxHub-o#; zW2(2RJi~=+{HTc&zGVM;0bfAGw{R#sU#shF@})6*8fUQGr-@z%4Umoe>D$^JH1C16 zCw-ez4)##CJuGg_<6Gm6V%t;6wntgJ-TP${-qejUzB++|FURI~zb5*}Z;88KQ{Hdl zLBVfO_kRxHJsE%e4(7ZIx&(eddL-!h(}Y&*Ivv_S z|!tViL#4*6cEl^Hosx^iC0{EAH}qQ zivu*MJ-5FDw%Bm7ANKFKxQ{N2 zZ5Ib`DJO*xi1B9oKJNx67k)?ixQu-2sYVsoX|*pw-` zRNH7)*Vbx@at+rsToU0-LY~6aaqJ++vD0!KgC*^Y6L1W!5YcR{v0RnO#H2hWd3f9~Je1?{WE`^r zZ8wj0s16su8X<@V;$O8xlXTP$hb~teo2#`Ac{DM3&&dPln}?GP+mU6bxq`Vk3$#fu zVV+4?n^*>3iiVwf+08{~Q6v_f+pfY-8SDg6zPeFuSWQd}tFrmha}lcYt<=7- r5wEN^wAu@>3H%}xw08^tN*2PY!@j(@SPJ)Y5WW7#+sxYW8`ggTroxeh literal 0 HcmV?d00001 From 40206bdf790bc09a6e98963eab59f807529ff4a8 Mon Sep 17 00:00:00 2001 From: Bingus Date: Sat, 25 Mar 2023 10:45:39 -0700 Subject: [PATCH 10/93] Added offscreen rendering support for wgpu & tiny-skia exposed with the window::screenshot command. --- examples/screenshot/Cargo.toml | 2 +- runtime/src/lib.rs | 2 + runtime/src/screenshot.rs | 80 ++++++++++++++++++++++ wgpu/src/backend.rs | 1 + wgpu/src/offscreen.rs | 102 ++++++++++++++++++++++++++++ wgpu/src/shader/offscreen_blit.wgsl | 22 ++++++ winit/src/application.rs | 2 +- 7 files changed, 209 insertions(+), 2 deletions(-) create mode 100644 runtime/src/screenshot.rs create mode 100644 wgpu/src/offscreen.rs create mode 100644 wgpu/src/shader/offscreen_blit.wgsl diff --git a/examples/screenshot/Cargo.toml b/examples/screenshot/Cargo.toml index 77b108bd51..479772afc0 100644 --- a/examples/screenshot/Cargo.toml +++ b/examples/screenshot/Cargo.toml @@ -14,4 +14,4 @@ image.features = ["png"] tokio.workspace = true -tracing-subscriber = "0.3" \ No newline at end of file +tracing-subscriber = "0.3" diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 03906f459a..44e2d3139d 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -37,6 +37,7 @@ mod debug; #[cfg(not(feature = "debug"))] #[path = "debug/null.rs"] mod debug; +mod screenshot; pub use iced_core as core; pub use iced_futures as futures; @@ -45,4 +46,5 @@ pub use command::Command; pub use debug::Debug; pub use font::Font; pub use program::Program; +pub use screenshot::{CropError, Screenshot}; pub use user_interface::UserInterface; diff --git a/runtime/src/screenshot.rs b/runtime/src/screenshot.rs new file mode 100644 index 0000000000..527e400f95 --- /dev/null +++ b/runtime/src/screenshot.rs @@ -0,0 +1,80 @@ +use iced_core::{Rectangle, Size}; +use std::fmt::{Debug, Formatter}; + +/// Data of a screenshot, captured with `window::screenshot()`. +/// +/// The `bytes` of this screenshot will always be ordered as `RGBA` in the sRGB color space. +#[derive(Clone)] +pub struct Screenshot { + /// The bytes of the [`Screenshot`]. + pub bytes: Vec, + /// The size of the [`Screenshot`]. + pub size: Size, +} + +impl Debug for Screenshot { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Screenshot: {{ \n bytes: {}\n size: {:?} }}", + self.bytes.len(), + self.size + ) + } +} + +impl Screenshot { + /// Creates a new [`Screenshot`]. + pub fn new(bytes: Vec, size: Size) -> Self { + Self { bytes, size } + } + + /// Crops a [`Screenshot`] to the provided `region`. This will always be relative to the + /// top-left corner of the [`Screenshot`]. + pub fn crop(&self, region: Rectangle) -> Result { + if region.width == 0 || region.height == 0 { + return Err(CropError::Zero); + } + + if region.x + region.width > self.size.width + || region.y + region.height > self.size.height + { + return Err(CropError::OutOfBounds); + } + + // Image is always RGBA8 = 4 bytes per pixel + const PIXEL_SIZE: usize = 4; + + let bytes_per_row = self.size.width as usize * PIXEL_SIZE; + let row_range = region.y as usize..(region.y + region.height) as usize; + let column_range = region.x as usize * PIXEL_SIZE + ..(region.x + region.width) as usize * PIXEL_SIZE; + + let chopped = self.bytes.chunks(bytes_per_row).enumerate().fold( + vec![], + |mut acc, (row, bytes)| { + if row_range.contains(&row) { + acc.extend(&bytes[column_range.clone()]); + } + + acc + }, + ); + + Ok(Self { + bytes: chopped, + size: Size::new(region.width, region.height), + }) + } +} + +#[derive(Debug, thiserror::Error)] +/// Errors that can occur when cropping a [`Screenshot`]. +pub enum CropError { + #[error("The cropped region is out of bounds.")] + /// The cropped region's size is out of bounds. + OutOfBounds, + #[error("The cropped region is not visible.")] + /// The cropped region's size is zero. + Zero, +} diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 25134d6869..11a9dd3c58 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -7,6 +7,7 @@ use crate::primitive::{self, Primitive}; use crate::quad; use crate::text; use crate::triangle; +use crate::{core, offscreen}; use crate::{Layer, Settings}; #[cfg(feature = "tracing")] diff --git a/wgpu/src/offscreen.rs b/wgpu/src/offscreen.rs new file mode 100644 index 0000000000..29913d0244 --- /dev/null +++ b/wgpu/src/offscreen.rs @@ -0,0 +1,102 @@ +use std::borrow::Cow; + +/// A simple compute pipeline to convert any texture to Rgba8UnormSrgb. +#[derive(Debug)] +pub struct Pipeline { + pipeline: wgpu::ComputePipeline, + layout: wgpu::BindGroupLayout, +} + +impl Pipeline { + pub fn new(device: &wgpu::Device) -> Self { + let shader = + device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("iced_wgpu.offscreen.blit.shader"), + source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!( + "shader/offscreen_blit.wgsl" + ))), + }); + + let bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("iced_wgpu.offscreen.blit.bind_group_layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::COMPUTE, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { + filterable: false, + }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::COMPUTE, + ty: wgpu::BindingType::StorageTexture { + access: wgpu::StorageTextureAccess::WriteOnly, + format: wgpu::TextureFormat::Rgba8Unorm, + view_dimension: wgpu::TextureViewDimension::D2, + }, + count: None, + }, + ], + }); + + let pipeline_layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("iced_wgpu.offscreen.blit.pipeline_layout"), + bind_group_layouts: &[&bind_group_layout], + push_constant_ranges: &[], + }); + + let pipeline = + device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor { + label: Some("iced_wgpu.offscreen.blit.pipeline"), + layout: Some(&pipeline_layout), + module: &shader, + entry_point: "main", + }); + + Self { + pipeline, + layout: bind_group_layout, + } + } + + pub fn convert( + &self, + device: &wgpu::Device, + extent: wgpu::Extent3d, + frame: &wgpu::TextureView, + view: &wgpu::TextureView, + encoder: &mut wgpu::CommandEncoder, + ) { + let bind = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("iced_wgpu.offscreen.blit.bind_group"), + layout: &self.layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(frame), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(view), + }, + ], + }); + + let mut compute_pass = + encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { + label: Some("iced_wgpu.offscreen.blit.compute_pass"), + }); + + compute_pass.set_pipeline(&self.pipeline); + compute_pass.set_bind_group(0, &bind, &[]); + compute_pass.dispatch_workgroups(extent.width, extent.height, 1); + } +} diff --git a/wgpu/src/shader/offscreen_blit.wgsl b/wgpu/src/shader/offscreen_blit.wgsl new file mode 100644 index 0000000000..9c764c36dc --- /dev/null +++ b/wgpu/src/shader/offscreen_blit.wgsl @@ -0,0 +1,22 @@ +@group(0) @binding(0) var u_texture: texture_2d; +@group(0) @binding(1) var out_texture: texture_storage_2d; + +fn srgb(color: f32) -> f32 { + if (color <= 0.0031308) { + return 12.92 * color; + } else { + return (1.055 * (pow(color, (1.0/2.4)))) - 0.055; + } +} + +@compute @workgroup_size(1) +fn main(@builtin(global_invocation_id) id: vec3) { + // texture coord must be i32 due to a naga bug: + // https://github.com/gfx-rs/naga/issues/1997 + let coords = vec2(i32(id.x), i32(id.y)); + + let src: vec4 = textureLoad(u_texture, coords, 0); + let srgb_color: vec4 = vec4(srgb(src.x), srgb(src.y), srgb(src.z), src.w); + + textureStore(out_texture, coords, srgb_color); +} diff --git a/winit/src/application.rs b/winit/src/application.rs index 80de7eded5..392ec555ef 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -21,7 +21,7 @@ use crate::graphics::compositor::{self, Compositor}; use crate::runtime::clipboard; use crate::runtime::program::Program; use crate::runtime::user_interface::{self, UserInterface}; -use crate::runtime::{Command, Debug}; +use crate::runtime::{Command, Debug, Screenshot}; use crate::style::application::{Appearance, StyleSheet}; use crate::{Clipboard, Error, Proxy, Settings}; use futures::channel::mpsc; From eed4f5c1d9b27b072880e4a854990c9738424e4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 27 Jun 2023 19:41:03 +0200 Subject: [PATCH 11/93] Move `Screenshot` inside `window` module --- runtime/src/lib.rs | 2 - runtime/src/screenshot.rs | 80 -------------------------------- runtime/src/window.rs | 1 + runtime/src/window/screenshot.rs | 2 +- winit/src/application.rs | 2 +- 5 files changed, 3 insertions(+), 84 deletions(-) delete mode 100644 runtime/src/screenshot.rs diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 44e2d3139d..03906f459a 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -37,7 +37,6 @@ mod debug; #[cfg(not(feature = "debug"))] #[path = "debug/null.rs"] mod debug; -mod screenshot; pub use iced_core as core; pub use iced_futures as futures; @@ -46,5 +45,4 @@ pub use command::Command; pub use debug::Debug; pub use font::Font; pub use program::Program; -pub use screenshot::{CropError, Screenshot}; pub use user_interface::UserInterface; diff --git a/runtime/src/screenshot.rs b/runtime/src/screenshot.rs deleted file mode 100644 index 527e400f95..0000000000 --- a/runtime/src/screenshot.rs +++ /dev/null @@ -1,80 +0,0 @@ -use iced_core::{Rectangle, Size}; -use std::fmt::{Debug, Formatter}; - -/// Data of a screenshot, captured with `window::screenshot()`. -/// -/// The `bytes` of this screenshot will always be ordered as `RGBA` in the sRGB color space. -#[derive(Clone)] -pub struct Screenshot { - /// The bytes of the [`Screenshot`]. - pub bytes: Vec, - /// The size of the [`Screenshot`]. - pub size: Size, -} - -impl Debug for Screenshot { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "Screenshot: {{ \n bytes: {}\n size: {:?} }}", - self.bytes.len(), - self.size - ) - } -} - -impl Screenshot { - /// Creates a new [`Screenshot`]. - pub fn new(bytes: Vec, size: Size) -> Self { - Self { bytes, size } - } - - /// Crops a [`Screenshot`] to the provided `region`. This will always be relative to the - /// top-left corner of the [`Screenshot`]. - pub fn crop(&self, region: Rectangle) -> Result { - if region.width == 0 || region.height == 0 { - return Err(CropError::Zero); - } - - if region.x + region.width > self.size.width - || region.y + region.height > self.size.height - { - return Err(CropError::OutOfBounds); - } - - // Image is always RGBA8 = 4 bytes per pixel - const PIXEL_SIZE: usize = 4; - - let bytes_per_row = self.size.width as usize * PIXEL_SIZE; - let row_range = region.y as usize..(region.y + region.height) as usize; - let column_range = region.x as usize * PIXEL_SIZE - ..(region.x + region.width) as usize * PIXEL_SIZE; - - let chopped = self.bytes.chunks(bytes_per_row).enumerate().fold( - vec![], - |mut acc, (row, bytes)| { - if row_range.contains(&row) { - acc.extend(&bytes[column_range.clone()]); - } - - acc - }, - ); - - Ok(Self { - bytes: chopped, - size: Size::new(region.width, region.height), - }) - } -} - -#[derive(Debug, thiserror::Error)] -/// Errors that can occur when cropping a [`Screenshot`]. -pub enum CropError { - #[error("The cropped region is out of bounds.")] - /// The cropped region's size is out of bounds. - OutOfBounds, - #[error("The cropped region is not visible.")] - /// The cropped region's size is zero. - Zero, -} diff --git a/runtime/src/window.rs b/runtime/src/window.rs index e20253c03d..4622b100e6 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -4,6 +4,7 @@ mod action; pub mod screenshot; pub use crate::core::window::Id; + pub use action::Action; pub use screenshot::Screenshot; diff --git a/runtime/src/window/screenshot.rs b/runtime/src/window/screenshot.rs index 21e0471888..c84286b68a 100644 --- a/runtime/src/window/screenshot.rs +++ b/runtime/src/window/screenshot.rs @@ -6,7 +6,7 @@ use std::sync::Arc; /// Data of a screenshot, captured with `window::screenshot()`. /// -/// The `bytes` of this screenshot will always be ordered as `RGBA` in the `sRGB` color space. +/// The `bytes` of this screenshot will always be ordered as `RGBA` in the sRGB color space. #[derive(Clone)] pub struct Screenshot { /// The bytes of the [`Screenshot`]. diff --git a/winit/src/application.rs b/winit/src/application.rs index 392ec555ef..80de7eded5 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -21,7 +21,7 @@ use crate::graphics::compositor::{self, Compositor}; use crate::runtime::clipboard; use crate::runtime::program::Program; use crate::runtime::user_interface::{self, UserInterface}; -use crate::runtime::{Command, Debug, Screenshot}; +use crate::runtime::{Command, Debug}; use crate::style::application::{Appearance, StyleSheet}; use crate::{Clipboard, Error, Proxy, Settings}; use futures::channel::mpsc; From 9d9d0786edfba5cc862315f63a8b6eb21370f912 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 10 May 2023 17:48:21 -0400 Subject: [PATCH 12/93] fix: quad rendering including border only inside of the bounds --- tiny_skia/src/backend.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index ea4a3ec6fe..f5bc75a211 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -177,12 +177,20 @@ impl Backend { .min(bounds.height / 2.0); let mut fill_border_radius = <[f32; 4]>::from(border.radius); + // Offset the fill by the border width + let path_bounds = Rectangle { + x: bounds.x + border_width, + y: bounds.y + border_width, + width: bounds.width - 2.0 * border_width, + height: bounds.height - 2.0 * border_width, + }; + // fill border radius is the border radius minus the border width for radius in &mut fill_border_radius { - *radius = (*radius) - .min(bounds.width / 2.0) - .min(bounds.height / 2.0); + *radius = (*radius - border_width / 2.0) + .min(path_bounds.width / 2.0) + .min(path_bounds.height / 2.0); } - let path = rounded_rectangle(*bounds, fill_border_radius); + let path = rounded_rectangle(path_bounds, fill_border_radius); if shadow.color.a > 0.0 { let shadow_bounds = (Rectangle { From aa65b614ed31e1fac3eea2d13d7b6d1aa1fa47ae Mon Sep 17 00:00:00 2001 From: "Austin M. Reppert" Date: Fri, 26 May 2023 20:27:17 -0400 Subject: [PATCH 13/93] Make vertical scroll properties optional Co-Authored-By: Austin M. Reppert --- examples/scrollable/Cargo.toml | 4 ++-- examples/scrollable/src/main.rs | 6 +++--- wgpu/src/backend.rs | 1 - widget/src/scrollable.rs | 1 - 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/examples/scrollable/Cargo.toml b/examples/scrollable/Cargo.toml index f8c735c014..50a9faff97 100644 --- a/examples/scrollable/Cargo.toml +++ b/examples/scrollable/Cargo.toml @@ -7,6 +7,6 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["debug"] - +iced.features = ["debug", "winit"] +iced_core.workspace = true once_cell.workspace = true diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index 07348b35cb..9cf37822a0 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -1,15 +1,15 @@ -use iced::executor; -use iced::id::Id; -use iced::theme; use iced::widget::scrollable::{Properties, Scrollbar, Scroller}; use iced::widget::{ button, column, container, horizontal_space, progress_bar, radio, row, scrollable, slider, text, vertical_space, }; +use iced::{executor, theme, Alignment, Application, Color}; use iced::{ Alignment, Application, Border, Color, Command, Element, Length, Settings, Theme, }; +use iced::{Command, Element, Length, Settings, Theme}; +use iced_core::id::Id; use once_cell::sync::Lazy; diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 11a9dd3c58..25134d6869 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -7,7 +7,6 @@ use crate::primitive::{self, Primitive}; use crate::quad; use crate::text; use crate::triangle; -use crate::{core, offscreen}; use crate::{Layer, Settings}; #[cfg(feature = "tracing")] diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index d8eb2fd34a..cd654ccfed 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -99,7 +99,6 @@ where self.direction = direction; self } - /// Sets a function to call when the [`Scrollable`] is scrolled. /// /// The function takes the [`Viewport`] of the [`Scrollable`] From 60360cb8bc96780f38719681be9998acc4915928 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 18 Aug 2023 21:39:55 +0200 Subject: [PATCH 14/93] feat(winit): client-side resize drag support --- core/src/renderer.rs | 3 +++ examples/integration/src/controls.rs | 1 + examples/integration/src/main.rs | 2 ++ examples/scrollable/src/main.rs | 3 +-- sctk/src/application.rs | 20 ++++++++++++++++---- src/lib.rs | 2 +- widget/src/button.rs | 3 ++- widget/src/container.rs | 1 + widget/src/pane_grid/title_bar.rs | 1 + widget/src/tooltip.rs | 1 + winit/src/application.rs | 1 + winit/src/multi_window.rs | 5 +++++ 12 files changed, 35 insertions(+), 8 deletions(-) diff --git a/core/src/renderer.rs b/core/src/renderer.rs index 0af74bb323..33d8447612 100644 --- a/core/src/renderer.rs +++ b/core/src/renderer.rs @@ -56,12 +56,15 @@ impl Default for Quad { pub struct Style { /// The text color pub text_color: Color, + /// The scale factor + pub scale_factor: f64, } impl Default for Style { fn default() -> Self { Style { text_color: Color::BLACK, + scale_factor: 1.0, } } } diff --git a/examples/integration/src/controls.rs b/examples/integration/src/controls.rs index c9bab8284f..404f3c9264 100644 --- a/examples/integration/src/controls.rs +++ b/examples/integration/src/controls.rs @@ -1,3 +1,4 @@ +use iced_wgpu::core::window::Id; use iced_wgpu::Renderer; use iced_widget::{slider, text_input, Column, Row, Text}; use iced_winit::core::{Alignment, Color, Element, Length}; diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index ed61459f22..ad6717b19c 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -4,6 +4,7 @@ mod scene; use controls::Controls; use scene::Scene; +use iced_wgpu::core::window::Id; use iced_wgpu::graphics::Viewport; use iced_wgpu::{wgpu, Backend, Renderer, Settings}; use iced_winit::conversion; @@ -159,6 +160,7 @@ pub fn main() -> Result<(), Box> { ); let mut state = program::State::new( + Id(0), controls, viewport.logical_size(), &mut renderer, diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index 9cf37822a0..d9b43d3dbb 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -3,12 +3,11 @@ use iced::widget::{ button, column, container, horizontal_space, progress_bar, radio, row, scrollable, slider, text, vertical_space, }; -use iced::{executor, theme, Alignment, Application, Color}; +use iced::{executor, theme}; use iced::{ Alignment, Application, Border, Color, Command, Element, Length, Settings, Theme, }; -use iced::{Command, Element, Length, Settings, Theme}; use iced_core::id::Id; use once_cell::sync::Lazy; diff --git a/sctk/src/application.rs b/sctk/src/application.rs index 3181ed4017..4d535da3ac 100644 --- a/sctk/src/application.rs +++ b/sctk/src/application.rs @@ -900,6 +900,7 @@ where state.theme(), &Style { text_color: state.text_color(), + scale_factor: state.scale_factor(), }, state.cursor(), ); @@ -1347,7 +1348,6 @@ where user_interface = user_interface .relayout(logical_size, &mut renderer); debug.layout_finished(); - state.viewport_changed = false; } debug.draw_started(); @@ -1356,13 +1356,20 @@ where state.theme(), &Style { text_color: state.text_color(), + scale_factor: state.scale_factor(), }, state.cursor(), ); + debug.draw_finished(); - ev_proxy - .send_event(Event::SetCursor(new_mouse_interaction)); - interfaces.insert(native_id.inner(), user_interface); + if new_mouse_interaction != mouse_interaction { + mouse_interaction = new_mouse_interaction; + ev_proxy + .send_event(Event::SetCursor(mouse_interaction)); + } + + let _ = + interfaces.insert(native_id.inner(), user_interface); let _ = compositor.present( &mut renderer, @@ -1938,6 +1945,11 @@ where Event::SctkEvent(IcedSctkEvent::UserEvent(e)) }))); } + command::Action::Stream(stream) => { + runtime.run(Box::pin( + stream.map(|e| Event::SctkEvent(IcedSctkEvent::UserEvent(e))), + )); + } command::Action::Clipboard(action) => match action { clipboard::Action::Read(s_to_msg) => { if matches!(clipboard.state, crate::clipboard::State::Connected(_)) { diff --git a/src/lib.rs b/src/lib.rs index d0597982ea..470f63044d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -213,7 +213,7 @@ pub mod multi_window; pub use style::theme; pub use crate::core::alignment; -pub use crate::core::border; +pub use crate::core::border::{self, Border, Radius}; pub use crate::core::color; pub use crate::core::gradient; pub use crate::core::{ diff --git a/widget/src/button.rs b/widget/src/button.rs index d410652372..7a88c4ff19 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -291,7 +291,7 @@ where tree: &Tree, renderer: &mut Renderer, theme: &Theme, - _style: &renderer::Style, + renderer_style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, _viewport: &Rectangle, @@ -315,6 +315,7 @@ where theme, &renderer::Style { text_color: styling.text_color, + scale_factor: renderer_style.scale_factor, }, content_layout, cursor, diff --git a/widget/src/container.rs b/widget/src/container.rs index 6305b87bf1..589da31d02 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -267,6 +267,7 @@ where text_color: style .text_color .unwrap_or(renderer_style.text_color), + scale_factor: renderer_style.scale_factor, }, layout.children().next().unwrap(), cursor, diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs index 85b4843fe6..6fc8bb2989 100644 --- a/widget/src/pane_grid/title_bar.rs +++ b/widget/src/pane_grid/title_bar.rs @@ -133,6 +133,7 @@ where let style = theme.appearance(&self.style); let inherited_style = renderer::Style { text_color: style.text_color.unwrap_or(inherited_style.text_color), + scale_factor: inherited_style.scale_factor, }; container::draw_background(renderer, &style, bounds); diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs index cc81cc343d..41bca6836b 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -446,6 +446,7 @@ where let defaults = renderer::Style { text_color: style.text_color.unwrap_or(inherited_style.text_color), + scale_factor: inherited_style.scale_factor, }; Widget::<(), Theme, Renderer>::draw( diff --git a/winit/src/application.rs b/winit/src/application.rs index 80de7eded5..6496ed056d 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -511,6 +511,7 @@ async fn run_instance( state.theme(), &renderer::Style { text_color: state.text_color(), + scale_factor: state.scale_factor(), }, state.cursor(), ); diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index b806e4b0da..3564cfc09b 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -546,6 +546,7 @@ async fn run_instance( window.state.theme(), &renderer::Style { text_color: window.state.text_color(), + scale_factor: window.state.scale_factor(), }, cursor, ); @@ -616,6 +617,9 @@ async fn run_instance( window.state.theme(), &renderer::Style { text_color: window.state.text_color(), + scale_factor: window + .state + .scale_factor(), }, window.state.cursor(), ); @@ -862,6 +866,7 @@ async fn run_instance( state.theme(), &renderer::Style { text_color: state.text_color(), + scale_factor: state.scale_factor(), }, cursor, ) From c69703fc99a6c8c957d63fb7a6d35c13dbc7cbd2 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 18 Aug 2023 21:39:55 +0200 Subject: [PATCH 15/93] feat(winit): client-side resize drag support --- core/src/window/settings.rs | 8 +- winit/src/application.rs | 23 +++++ winit/src/application/drag_resize.rs | 132 +++++++++++++++++++++++++++ 3 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 winit/src/application/drag_resize.rs diff --git a/core/src/window/settings.rs b/core/src/window/settings.rs index fbbf86abd8..c5a16d743a 100644 --- a/core/src/window/settings.rs +++ b/core/src/window/settings.rs @@ -34,6 +34,9 @@ pub struct Settings { /// The initial logical dimensions of the window. pub size: Size, + /// The border area for the drag resize handle. + pub resize_border: u32, + /// The initial position of the window. pub position: Position, @@ -76,9 +79,10 @@ pub struct Settings { } impl Default for Settings { - fn default() -> Self { - Self { + fn default() -> Settings { + Settings { size: Size::new(1024.0, 768.0), + resize_border: 8, position: Position::default(), min_size: None, max_size: None, diff --git a/winit/src/application.rs b/winit/src/application.rs index 6496ed056d..9beafbcd5b 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -1,4 +1,7 @@ //! Create interactive, native cross-platform applications. +mod drag_resize; +#[cfg(feature = "trace")] +mod profiler; mod state; use iced_graphics::core::widget::operation::focusable::focus; @@ -147,6 +150,11 @@ where let mut debug = Debug::new(); debug.startup_started(); + let resize_border = settings.window.resize_border; + + #[cfg(feature = "trace")] + let _ = info_span!("Application", "RUN").entered(); + let event_loop = EventLoopBuilder::with_user_event() .build() .expect("Create event loop"); @@ -242,6 +250,7 @@ where window, should_be_visible, exit_on_close_request, + resize_border, )); let mut context = task::Context::from_waker(task::noop_waker_ref()); @@ -289,6 +298,7 @@ async fn run_instance( window: Arc, should_be_visible: bool, exit_on_close_request: bool, + resize_border: u32, ) where A: Application + 'static, E: Executor + 'static, @@ -345,6 +355,12 @@ async fn run_instance( &mut debug, )); + // Creates closure for handling the window drag resize state with winit. + let mut drag_resize_window_func = drag_resize::event_func( + &window, + resize_border as f64 * window.scale_factor(), + ); + let mut mouse_interaction = mouse::Interaction::default(); let mut events = Vec::new(); let mut messages = Vec::new(); @@ -640,6 +656,13 @@ async fn run_instance( event: window_event, .. } => { + // Initiates a drag resize window state when found. + if let Some(func) = drag_resize_window_func.as_mut() { + if func(&window, &window_event) { + continue; + } + } + if requests_exit(&window_event, state.modifiers()) && exit_on_close_request { diff --git a/winit/src/application/drag_resize.rs b/winit/src/application/drag_resize.rs new file mode 100644 index 0000000000..ac4e3d063e --- /dev/null +++ b/winit/src/application/drag_resize.rs @@ -0,0 +1,132 @@ +use winit::window::{CursorIcon, ResizeDirection}; + +/// If supported by winit, returns a closure that implements cursor resize support. +pub fn event_func( + window: &winit::window::Window, + border_size: f64, +) -> Option< + impl FnMut(&winit::window::Window, &winit::event::WindowEvent) -> bool, +> { + if window.drag_resize_window(ResizeDirection::East).is_ok() { + // Keep track of cursor when it is within a resizeable border. + let mut cursor_prev_resize_direction = None; + + Some( + move |window: &winit::window::Window, + window_event: &winit::event::WindowEvent| + -> bool { + // Keep track of border resize state and set cursor icon when in range + match window_event { + winit::event::WindowEvent::CursorMoved { + position, .. + } => { + if !window.is_decorated() { + let location = cursor_resize_direction( + window.inner_size(), + *position, + border_size, + ); + if location != cursor_prev_resize_direction { + window.set_cursor_icon( + resize_direction_cursor_icon(location), + ); + cursor_prev_resize_direction = location; + return true; + } + } + } + winit::event::WindowEvent::MouseInput { + state: winit::event::ElementState::Pressed, + button: winit::event::MouseButton::Left, + .. + } => { + if let Some(direction) = cursor_prev_resize_direction { + let _res = window.drag_resize_window(direction); + return true; + } + } + _ => (), + } + + false + }, + ) + } else { + None + } +} + +/// Get the cursor icon that corresponds to the resize direction. +fn resize_direction_cursor_icon( + resize_direction: Option, +) -> CursorIcon { + match resize_direction { + Some(resize_direction) => match resize_direction { + ResizeDirection::East => CursorIcon::EResize, + ResizeDirection::North => CursorIcon::NResize, + ResizeDirection::NorthEast => CursorIcon::NeResize, + ResizeDirection::NorthWest => CursorIcon::NwResize, + ResizeDirection::South => CursorIcon::SResize, + ResizeDirection::SouthEast => CursorIcon::SeResize, + ResizeDirection::SouthWest => CursorIcon::SwResize, + ResizeDirection::West => CursorIcon::WResize, + }, + None => CursorIcon::Default, + } +} + +/// Identifies resize direction based on cursor position and window dimensions. +#[allow(clippy::similar_names)] +fn cursor_resize_direction( + win_size: winit::dpi::PhysicalSize, + position: winit::dpi::PhysicalPosition, + border_size: f64, +) -> Option { + enum XDirection { + West, + East, + Default, + } + + enum YDirection { + North, + South, + Default, + } + + let xdir = if position.x < border_size { + XDirection::West + } else if position.x > (win_size.width as f64 - border_size) { + XDirection::East + } else { + XDirection::Default + }; + + let ydir = if position.y < border_size { + YDirection::North + } else if position.y > (win_size.height as f64 - border_size) { + YDirection::South + } else { + YDirection::Default + }; + + Some(match xdir { + XDirection::West => match ydir { + YDirection::North => ResizeDirection::NorthWest, + YDirection::South => ResizeDirection::SouthWest, + YDirection::Default => ResizeDirection::West, + }, + + XDirection::East => match ydir { + YDirection::North => ResizeDirection::NorthEast, + YDirection::South => ResizeDirection::SouthEast, + YDirection::Default => ResizeDirection::East, + }, + + XDirection::Default => match ydir { + YDirection::North => ResizeDirection::North, + YDirection::South => ResizeDirection::South, + YDirection::Default => return None, + }, + }) +} From 3e70ccdadc86a54f2ecb8d7216ba98380a29cb31 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 30 Aug 2023 16:28:51 +0200 Subject: [PATCH 16/93] feat(renderer): define default icon color By default, this is the same as the text color for best visibility. --- core/src/renderer.rs | 3 +++ sctk/src/application.rs | 7 +++++++ style/src/application.rs | 3 +++ style/src/button.rs | 14 +++++++++++++- style/src/container.rs | 4 ++++ style/src/theme.rs | 2 ++ style/src/theme/palette.rs | 10 ++++++++-- widget/src/button.rs | 1 + widget/src/container.rs | 3 +++ widget/src/pane_grid/title_bar.rs | 1 + widget/src/svg.rs | 12 ++++++++++++ widget/src/tooltip.rs | 1 + winit/src/application.rs | 1 + winit/src/application/state.rs | 5 +++++ winit/src/multi_window.rs | 3 +++ winit/src/multi_window/state.rs | 5 +++++ 16 files changed, 72 insertions(+), 3 deletions(-) diff --git a/core/src/renderer.rs b/core/src/renderer.rs index 33d8447612..529bbdcfe1 100644 --- a/core/src/renderer.rs +++ b/core/src/renderer.rs @@ -54,6 +54,8 @@ impl Default for Quad { /// The styling attributes of a [`Renderer`]. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { + /// The color to apply to symbolic icons. + pub icon_color: Color, /// The text color pub text_color: Color, /// The scale factor @@ -63,6 +65,7 @@ pub struct Style { impl Default for Style { fn default() -> Self { Style { + icon_color: Color::BLACK, text_color: Color::BLACK, scale_factor: 1.0, } diff --git a/sctk/src/application.rs b/sctk/src/application.rs index 4d535da3ac..c624e9eeb9 100644 --- a/sctk/src/application.rs +++ b/sctk/src/application.rs @@ -899,6 +899,7 @@ where &mut renderer, state.theme(), &Style { + icon_color: state.icon_color(), text_color: state.text_color(), scale_factor: state.scale_factor(), }, @@ -1355,6 +1356,7 @@ where &mut renderer, state.theme(), &Style { + icon_color: state.icon_color(), text_color: state.text_color(), scale_factor: state.scale_factor(), }, @@ -1786,6 +1788,11 @@ where self.appearance.text_color } + /// Returns the current icon [`Color`] of the [`State`]. + pub fn icon_color(&self) -> Color { + self.appearance.icon_color + } + pub fn set_cursor_position(&mut self, p: Option>) { self.cursor_position = p.map(|p| p.to_physical(self.application_scale_factor)); diff --git a/style/src/application.rs b/style/src/application.rs index e9a1f4ff45..673178b801 100644 --- a/style/src/application.rs +++ b/style/src/application.rs @@ -18,6 +18,9 @@ pub struct Appearance { /// The background [`Color`] of the application. pub background_color: Color, + /// The default icon [`Color`] of the application. + pub icon_color: Color, + /// The default text [`Color`] of the application. pub text_color: Color, } diff --git a/style/src/button.rs b/style/src/button.rs index 0d7a668aca..d508181621 100644 --- a/style/src/button.rs +++ b/style/src/button.rs @@ -1,5 +1,5 @@ //! Change the apperance of a button. -use iced_core::{Background, Border, Color, Shadow, Vector}; +use iced_core::{border::Radius, Background, Border, Color, Shadow, Vector}; /// The appearance of a button. #[derive(Debug, Clone, Copy)] @@ -8,6 +8,14 @@ pub struct Appearance { pub shadow_offset: Vector, /// The [`Background`] of the button. pub background: Option, + /// The border radius of the button. + pub border_radius: Radius, + /// The border width of the button. + pub border_width: f32, + /// The border [`Color`] of the button. + pub border_color: Color, + /// The icon [`Color`] of the button. + pub icon_color: Color, /// The text [`Color`] of the button. pub text_color: Color, /// The [`Border`] of the buton. @@ -21,6 +29,10 @@ impl std::default::Default for Appearance { Self { shadow_offset: Vector::default(), background: None, + border_radius: 0.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + icon_color: Color::BLACK, text_color: Color::BLACK, border: Border::default(), shadow: Shadow::default(), diff --git a/style/src/container.rs b/style/src/container.rs index 00649c25eb..95100cb450 100644 --- a/style/src/container.rs +++ b/style/src/container.rs @@ -4,6 +4,8 @@ use crate::core::{Background, Border, Color, Pixels, Shadow}; /// The appearance of a container. #[derive(Debug, Clone, Copy, Default)] pub struct Appearance { + /// The icon [`Color`] of the container. + pub icon_color: Option, /// The text [`Color`] of the container. pub text_color: Option, /// The [`Background`] of the container. @@ -36,6 +38,8 @@ impl Appearance { pub fn with_background(self, background: impl Into) -> Self { Self { background: Some(background.into()), + icon_color: None, + text_color: None, ..self } } diff --git a/style/src/theme.rs b/style/src/theme.rs index 8d1ff23779..3edee9a618 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -140,6 +140,7 @@ impl application::StyleSheet for Theme { match style { Application::Default => application::Appearance { background_color: palette.background.base.color, + icon_color: palette.background.base.icon, text_color: palette.background.base.text, }, Application::Custom(custom) => custom.appearance(self), @@ -431,6 +432,7 @@ impl container::StyleSheet for Theme { let palette = self.extended_palette(); container::Appearance { + icon_color: None, text_color: None, background: Some(palette.background.weak.color.into()), border: Border::with_radius(2), diff --git a/style/src/theme/palette.rs b/style/src/theme/palette.rs index 76977a2942..5500cf7b11 100644 --- a/style/src/theme/palette.rs +++ b/style/src/theme/palette.rs @@ -120,12 +120,15 @@ impl Extended { } } -/// A pair of background and text colors. +/// Recommended background, icon, and text [`Color`]. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Pair { /// The background color. pub color: Color, + /// The icon color, which defaults to the text color. + pub icon: Color, + /// The text color. /// /// It's guaranteed to be readable on top of the background [`color`]. @@ -137,9 +140,12 @@ pub struct Pair { impl Pair { /// Creates a new [`Pair`] from a background [`Color`] and some text [`Color`]. pub fn new(color: Color, text: Color) -> Self { + let text = readable(color, text); + Self { color, - text: readable(color, text), + icon: text, + text, } } } diff --git a/widget/src/button.rs b/widget/src/button.rs index 7a88c4ff19..2c94138b23 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -314,6 +314,7 @@ where renderer, theme, &renderer::Style { + icon_color: styling.icon_color, text_color: styling.text_color, scale_factor: renderer_style.scale_factor, }, diff --git a/widget/src/container.rs b/widget/src/container.rs index 589da31d02..0da82a35f2 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -264,6 +264,9 @@ where renderer, theme, &renderer::Style { + icon_color: style + .icon_color + .unwrap_or(renderer_style.text_color), text_color: style .text_color .unwrap_or(renderer_style.text_color), diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs index 6fc8bb2989..7bfb000600 100644 --- a/widget/src/pane_grid/title_bar.rs +++ b/widget/src/pane_grid/title_bar.rs @@ -132,6 +132,7 @@ where let bounds = layout.bounds(); let style = theme.appearance(&self.style); let inherited_style = renderer::Style { + icon_color: style.icon_color.unwrap_or(inherited_style.icon_color), text_color: style.text_color.unwrap_or(inherited_style.text_color), scale_factor: inherited_style.scale_factor, }; diff --git a/widget/src/svg.rs b/widget/src/svg.rs index 6ee0dfd9c6..f5cae3e61f 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -39,6 +39,7 @@ where height: Length, content_fit: ContentFit, style: ::Style, + symbolic: bool, } impl<'a, Theme> Svg<'a, Theme> @@ -59,6 +60,7 @@ where width: Length::Fill, height: Length::Shrink, content_fit: ContentFit::Contain, + symbolic: false, style: Default::default(), } } @@ -95,6 +97,13 @@ where } } + /// Symbolic icons inherit their color from the renderer if a color is not defined. + #[must_use] + pub fn symbolic(mut self, symbolic: bool) -> Self { + self.symbolic = symbolic; + self + } + /// Sets the style variant of this [`Svg`]. #[must_use] pub fn style(mut self, style: impl Into) -> Self { @@ -216,6 +225,9 @@ where } else { theme.appearance(&self.style) }; + if self.symbolic && appearance.color.is_none() { + appearance.color = Some(style.icon_color); + } renderer.draw( self.handle.clone(), diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs index 41bca6836b..f87925d6df 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -445,6 +445,7 @@ where container::draw_background(renderer, &style, layout.bounds()); let defaults = renderer::Style { + icon_color: inherited_style.icon_color, text_color: style.text_color.unwrap_or(inherited_style.text_color), scale_factor: inherited_style.scale_factor, }; diff --git a/winit/src/application.rs b/winit/src/application.rs index 9beafbcd5b..e515551603 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -526,6 +526,7 @@ async fn run_instance( &mut renderer, state.theme(), &renderer::Style { + icon_color: state.icon_color(), text_color: state.text_color(), scale_factor: state.scale_factor(), }, diff --git a/winit/src/application/state.rs b/winit/src/application/state.rs index 94f2af7032..59d631c1fe 100644 --- a/winit/src/application/state.rs +++ b/winit/src/application/state.rs @@ -120,6 +120,11 @@ where self.appearance.background_color } + /// Returns the current icon [`Color`] of the [`State`]. + pub fn icon_color(&self) -> Color { + self.appearance.icon_color + } + /// Returns the current text [`Color`] of the [`State`]. pub fn text_color(&self) -> Color { self.appearance.text_color diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 3564cfc09b..b7b7b961f3 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -545,6 +545,7 @@ async fn run_instance( &mut window.renderer, window.state.theme(), &renderer::Style { + icon_color: window.state.icon_color(), text_color: window.state.text_color(), scale_factor: window.state.scale_factor(), }, @@ -616,6 +617,7 @@ async fn run_instance( &mut window.renderer, window.state.theme(), &renderer::Style { + icon_color: window.state.icon_color(), text_color: window.state.text_color(), scale_factor: window .state @@ -865,6 +867,7 @@ async fn run_instance( &mut window.renderer, state.theme(), &renderer::Style { + icon_color: state.icon_color(), text_color: state.text_color(), scale_factor: state.scale_factor(), }, diff --git a/winit/src/multi_window/state.rs b/winit/src/multi_window/state.rs index 2e97a13d96..9f65c90a1c 100644 --- a/winit/src/multi_window/state.rs +++ b/winit/src/multi_window/state.rs @@ -137,6 +137,11 @@ where self.appearance.text_color } + /// Returns the current icon [`Color`] of the [`State`]. + pub fn icon_color(&self) -> Color { + self.appearance.icon_color + } + /// Processes the provided window event and updates the [`State`] accordingly. pub fn update( &mut self, From a3fcdc084687ff0852cb848ceb2348fdebdf7131 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Thu, 31 Aug 2023 02:08:12 +0200 Subject: [PATCH 17/93] fix(button): inherit icon color if set to none --- style/src/button.rs | 4 ++-- widget/src/button.rs | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/style/src/button.rs b/style/src/button.rs index d508181621..48df624570 100644 --- a/style/src/button.rs +++ b/style/src/button.rs @@ -15,7 +15,7 @@ pub struct Appearance { /// The border [`Color`] of the button. pub border_color: Color, /// The icon [`Color`] of the button. - pub icon_color: Color, + pub icon_color: Option, /// The text [`Color`] of the button. pub text_color: Color, /// The [`Border`] of the buton. @@ -32,7 +32,7 @@ impl std::default::Default for Appearance { border_radius: 0.0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, - icon_color: Color::BLACK, + icon_color: None, text_color: Color::BLACK, border: Border::default(), shadow: Shadow::default(), diff --git a/widget/src/button.rs b/widget/src/button.rs index 2c94138b23..77521921fa 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -314,7 +314,9 @@ where renderer, theme, &renderer::Style { - icon_color: styling.icon_color, + icon_color: styling + .icon_color + .unwrap_or(renderer_style.icon_color), text_color: styling.text_color, scale_factor: renderer_style.scale_factor, }, From d87f092c1d0b216669db4636b8b99747f08b689c Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Thu, 7 Sep 2023 16:52:53 +0200 Subject: [PATCH 18/93] fix(widget): container inherited wrong icon color from renderer --- widget/src/container.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widget/src/container.rs b/widget/src/container.rs index 0da82a35f2..eaae79243b 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -266,7 +266,7 @@ where &renderer::Style { icon_color: style .icon_color - .unwrap_or(renderer_style.text_color), + .unwrap_or(renderer_style.icon_color), text_color: style .text_color .unwrap_or(renderer_style.text_color), From 5db1da977b05e265108529c196abead4fe18bcc2 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 12 Sep 2023 17:53:44 +0200 Subject: [PATCH 19/93] feat(mouse-area): added on_drag method --- widget/src/mouse_area.rs | 42 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index 2729e2f0a0..5b93d88eb1 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -1,6 +1,7 @@ //! A container for capturing mouse events. use iced_renderer::core::widget::OperationOutputWrapper; +use iced_renderer::core::Point; use crate::core::event::{self, Event}; use crate::core::layout; @@ -22,6 +23,7 @@ pub struct MouseArea< Renderer = crate::Renderer, > { content: Element<'a, Message, Theme, Renderer>, + on_drag: Option, on_press: Option, on_release: Option, on_right_press: Option, @@ -31,6 +33,13 @@ pub struct MouseArea< } impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { + /// The message to emit when a drag is initiated. + #[must_use] + pub fn on_drag(mut self, message: Message) -> Self { + self.on_drag = Some(message); + self + } + /// The message to emit on a left button press. #[must_use] pub fn on_press(mut self, message: Message) -> Self { @@ -78,6 +87,7 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { #[derive(Default)] struct State { // TODO: Support on_mouse_enter and on_mouse_exit + drag_initiated: Option, } impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { @@ -87,6 +97,7 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { ) -> Self { MouseArea { content: content.into(), + on_drag: None, on_press: None, on_release: None, on_right_press: None, @@ -173,7 +184,14 @@ where return event::Status::Captured; } - update(self, &event, layout, cursor, shell) + update( + self, + &event, + layout, + cursor, + shell, + tree.state.downcast_mut::(), + ) } fn mouse_interaction( @@ -250,6 +268,7 @@ fn update( layout: Layout<'_>, cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, + state: &mut State, ) -> event::Status { if !cursor.is_over(layout.bounds()) { return event::Status::Ignored; @@ -259,6 +278,7 @@ fn update( if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) = event { + state.drag_initiated = cursor.position(); shell.publish(message.clone()); return event::Status::Captured; @@ -269,6 +289,7 @@ fn update( if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) | Event::Touch(touch::Event::FingerLifted { .. }) = event { + state.drag_initiated = None; shell.publish(message.clone()); return event::Status::Captured; @@ -318,5 +339,24 @@ fn update( } } + if state.drag_initiated.is_none() && widget.on_drag.is_some() { + if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) = event + { + state.drag_initiated = cursor.position(); + } + } else if let Some((message, drag_source)) = + widget.on_drag.as_ref().zip(state.drag_initiated) + { + if let Some(position) = cursor.position() { + if position.distance(drag_source) > 1.0 { + state.drag_initiated = None; + shell.publish(message.clone()); + + return event::Status::Captured; + } + } + } + event::Status::Ignored } From b66eeab49ed3d627cd24f7088d32047f5a5c78a2 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 26 Sep 2023 14:59:17 -0400 Subject: [PATCH 20/93] feat: gradient backgground for the slider rail --- examples/tour/Cargo.toml | 2 +- style/src/slider.rs | 20 +++++++-- style/src/theme.rs | 29 +++++++++--- widget/src/slider.rs | 81 ++++++++++++++++++++++------------ widget/src/vertical_slider.rs | 83 +++++++++++++++++++++++------------ 5 files changed, 151 insertions(+), 64 deletions(-) diff --git a/examples/tour/Cargo.toml b/examples/tour/Cargo.toml index 9e984ad124..266b4fcd9c 100644 --- a/examples/tour/Cargo.toml +++ b/examples/tour/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["image", "debug"] +iced.features = ["image", "debug", "winit"] [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tracing-subscriber = "0.3" diff --git a/style/src/slider.rs b/style/src/slider.rs index bf1c732961..4c2de9c9c8 100644 --- a/style/src/slider.rs +++ b/style/src/slider.rs @@ -1,6 +1,5 @@ //! Change the apperance of a slider. -use crate::core::border; -use crate::core::Color; +use iced_core::{border, gradient::Linear, Color}; /// The appearance of a slider. #[derive(Debug, Clone, Copy)] @@ -15,13 +14,28 @@ pub struct Appearance { #[derive(Debug, Clone, Copy)] pub struct Rail { /// The colors of the rail of the slider. - pub colors: (Color, Color), + pub colors: RailBackground, /// The width of the stroke of a slider rail. pub width: f32, /// The border radius of the corners of the rail. pub border_radius: border::Radius, } +/// The background color of the rail +#[derive(Debug, Clone, Copy)] +pub enum RailBackground { + /// Start and end colors of the rail + Pair(Color, Color), + /// Linear gradient for the background of the rail + /// includes an option for auto-selecting the angle + Gradient { + /// the linear gradient of the slider + gradient: Linear, + /// Let the widget determin the angle of the gradient + auto_angle: bool, + }, +} + /// The appearance of the handle of a slider. #[derive(Debug, Clone, Copy)] pub struct Handle { diff --git a/style/src/theme.rs b/style/src/theme.rs index 3edee9a618..746a265734 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -16,12 +16,17 @@ use crate::radio; use crate::rule; use crate::scrollable; use crate::slider; +use crate::slider::RailBackground; use crate::svg; use crate::text_editor; use crate::text_input; use crate::toggler; -use crate::core::{Background, Border, Color, Shadow, Vector}; +use iced_core::gradient::ColorStop; +use iced_core::gradient::Linear; +use iced_core::Degrees; +use iced_core::Radians; +use iced_core::{Background, Border, Color, Shadow, Vector}; use std::fmt; use std::rc::Rc; @@ -482,10 +487,24 @@ impl slider::StyleSheet for Theme { slider::Appearance { rail: slider::Rail { - colors: ( - palette.primary.base.color, - palette.secondary.base.color, - ), + colors: RailBackground::Gradient { + gradient: Linear::new(Radians::from(Degrees(0.0))) + .add_stops([ + ColorStop { + offset: 0.0, + color: Color::from_rgb(1.0, 0.0, 0.0), + }, + ColorStop { + offset: 0.5, + color: Color::from_rgb(0.0, 1.0, 0.0), + }, + ColorStop { + offset: 1.0, + color: Color::from_rgb(0.0, 0.0, 1.0), + }, + ]), + auto_angle: true, + }, width: 4.0, border_radius: 2.0.into(), }, diff --git a/widget/src/slider.rs b/widget/src/slider.rs index 5171b4acbb..f95dc76433 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -15,7 +15,7 @@ use crate::core::{ use std::ops::RangeInclusive; -use iced_renderer::core::border::Radius; +use iced_renderer::core::{border::Radius, Radians}; pub use iced_style::slider::{ Appearance, Handle, HandleShape, Rail, StyleSheet, }; @@ -539,35 +539,60 @@ pub fn draw( let rail_y = bounds.y + bounds.height / 2.0; - // rail - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x, - y: rail_y - style.rail.width / 2.0, - width: offset + handle_width / 2.0, - height: style.rail.width, + match style.rail.colors { + iced_style::slider::RailBackground::Pair(l, r) => { + // rail + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: rail_y - style.rail.width / 2.0, + width: offset + handle_width / 2.0, + height: style.rail.width, + }, + border: Border::with_radius(style.rail.border_radius), + ..renderer::Quad::default() + }, + l, + ); + + // right rail + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + offset + handle_width / 2.0, + y: rail_y - style.rail.width / 2.0, + width: bounds.width - offset - handle_width / 2.0, + height: style.rail.width, + }, + border: Border::with_radius(style.rail.border_radius), + ..renderer::Quad::default() + }, + r, + ); + } + iced_style::slider::RailBackground::Gradient { + mut gradient, + auto_angle, + } => renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: rail_y - style.rail.width / 2.0, + width: bounds.width, + height: style.rail.width, + }, + border: Border::with_radius(style.rail.border_radius), + ..renderer::Quad::default() }, - border: Border::with_radius(style.rail.border_radius), - ..renderer::Quad::default() - }, - style.rail.colors.0, - ); - - // right rail - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x + offset + handle_width / 2.0, - y: rail_y - style.rail.width / 2.0, - width: bounds.width - offset - handle_width / 2.0, - height: style.rail.width, + if auto_angle { + gradient.angle = Radians(0.0); + gradient + } else { + gradient }, - border: Border::with_radius(style.rail.border_radius), - ..renderer::Quad::default() - }, - style.rail.colors.1, - ); + ), + } // handle renderer.fill_quad( diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index d3086a81f5..38417e03f6 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -3,6 +3,8 @@ //! A [`VerticalSlider`] has some local [`State`]. use std::ops::RangeInclusive; +use iced_renderer::core::{Degrees, Radians}; + pub use crate::style::slider::{Appearance, Handle, HandleShape, StyleSheet}; use crate::core; @@ -385,33 +387,60 @@ pub fn draw( let rail_x = bounds.x + bounds.width / 2.0; - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: rail_x - style.rail.width / 2.0, - y: bounds.y, - width: style.rail.width, - height: offset + handle_width / 2.0, - }, - border: Border::with_radius(style.rail.border_radius), - ..renderer::Quad::default() - }, - style.rail.colors.1, - ); - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: rail_x - style.rail.width / 2.0, - y: bounds.y + offset + handle_width / 2.0, - width: style.rail.width, - height: bounds.height - offset - handle_width / 2.0, - }, - border: Border::with_radius(style.rail.border_radius), - ..renderer::Quad::default() - }, - style.rail.colors.0, - ); + match style.rail.colors { + iced_style::slider::RailBackground::Pair(start, end) => { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: rail_x - style.rail.width / 2.0, + y: bounds.y, + width: style.rail.width, + height: offset + handle_width / 2.0, + }, + border: Border::with_radius(style.rail.border_radius), + ..renderer::Quad::default() + }, + end, + ); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: rail_x - style.rail.width / 2.0, + y: bounds.y + offset + handle_width / 2.0, + width: style.rail.width, + height: bounds.height - offset - handle_width / 2.0, + }, + border: Border::with_radius(style.rail.border_radius), + ..renderer::Quad::default() + }, + start, + ); + } + iced_style::slider::RailBackground::Gradient { + mut gradient, + auto_angle, + } => { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: rail_x - style.rail.width / 2.0, + y: bounds.y, + width: style.rail.width, + height: bounds.height - handle_width / 2.0, + }, + border: Border::with_radius(style.rail.border_radius), + ..renderer::Quad::default() + }, + if auto_angle { + gradient.angle = Radians::from(Degrees(90.0)); + gradient + } else { + gradient + }, + ); + } + } renderer.fill_quad( renderer::Quad { From 31abe65732db970c9ea8a3e4836f7e0bb5db9c2f Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 29 Sep 2023 12:40:33 -0400 Subject: [PATCH 21/93] fix: slider gradient angle --- style/src/theme.rs | 30 ++++++++++++++++-------------- widget/src/slider.rs | 4 ++-- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/style/src/theme.rs b/style/src/theme.rs index 746a265734..cdf0c8ff0c 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -22,6 +22,8 @@ use crate::text_editor; use crate::text_input; use crate::toggler; +use ::palette::FromColor; +use ::palette::RgbHue; use iced_core::gradient::ColorStop; use iced_core::gradient::Linear; use iced_core::Degrees; @@ -489,20 +491,20 @@ impl slider::StyleSheet for Theme { rail: slider::Rail { colors: RailBackground::Gradient { gradient: Linear::new(Radians::from(Degrees(0.0))) - .add_stops([ - ColorStop { - offset: 0.0, - color: Color::from_rgb(1.0, 0.0, 0.0), - }, - ColorStop { - offset: 0.5, - color: Color::from_rgb(0.0, 1.0, 0.0), - }, - ColorStop { - offset: 1.0, - color: Color::from_rgb(0.0, 0.0, 1.0), - }, - ]), + .add_stops((0u16..8).map(|h| ColorStop { + color: Color::from( + ::palette::Srgba::from_color( + ::palette::Hsv::new_srgb_const( + RgbHue::new( + f32::from(h) * 45.0, + ), + 1.0, + 1.0, + ), + ), + ), + offset: f32::from(h) / 7.0, + })), auto_angle: true, }, width: 4.0, diff --git a/widget/src/slider.rs b/widget/src/slider.rs index f95dc76433..bfd175cf1f 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -15,7 +15,7 @@ use crate::core::{ use std::ops::RangeInclusive; -use iced_renderer::core::{border::Radius, Radians}; +use iced_renderer::core::{border::Radius, Degrees, Radians}; pub use iced_style::slider::{ Appearance, Handle, HandleShape, Rail, StyleSheet, }; @@ -586,7 +586,7 @@ pub fn draw( ..renderer::Quad::default() }, if auto_angle { - gradient.angle = Radians(0.0); + gradient.angle = Radians::from(Degrees(180.0)); gradient } else { gradient From 0db19f0c72d71f06c03dc526082dd085ab6c5b53 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 29 Sep 2023 12:41:04 -0400 Subject: [PATCH 22/93] feat: allow setting the width and height of a rule --- widget/src/rule.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/widget/src/rule.rs b/widget/src/rule.rs index bca345413c..347ccc19bc 100644 --- a/widget/src/rule.rs +++ b/widget/src/rule.rs @@ -45,6 +45,24 @@ where } } + /// Set the width of the rule + /// Will not be applied if it is vertical + pub fn width(mut self, width: impl Into) -> Self { + if self.is_horizontal { + self.width = width.into(); + } + self + } + + /// Set the height of the rule + /// Will not be applied if it is horizontal + pub fn height(mut self, height: impl Into) -> Self { + if !self.is_horizontal { + self.height = height.into(); + } + self + } + /// Sets the style of the [`Rule`]. pub fn style(mut self, style: impl Into) -> Self { self.style = style.into(); From f4d3ea9ccf86cbc2d8ec26ff38f488d4531775f4 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 29 Sep 2023 12:48:24 -0400 Subject: [PATCH 23/93] refactor: restore default style of slider --- examples/slider/Cargo.toml | 1 + style/src/theme.rs | 22 ++++------------------ 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/examples/slider/Cargo.toml b/examples/slider/Cargo.toml index fad8916e46..bf52dc6ca9 100644 --- a/examples/slider/Cargo.toml +++ b/examples/slider/Cargo.toml @@ -7,3 +7,4 @@ publish = false [dependencies] iced.workspace = true +iced.features = ["winit"] \ No newline at end of file diff --git a/style/src/theme.rs b/style/src/theme.rs index cdf0c8ff0c..9109970e8c 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -489,24 +489,10 @@ impl slider::StyleSheet for Theme { slider::Appearance { rail: slider::Rail { - colors: RailBackground::Gradient { - gradient: Linear::new(Radians::from(Degrees(0.0))) - .add_stops((0u16..8).map(|h| ColorStop { - color: Color::from( - ::palette::Srgba::from_color( - ::palette::Hsv::new_srgb_const( - RgbHue::new( - f32::from(h) * 45.0, - ), - 1.0, - 1.0, - ), - ), - ), - offset: f32::from(h) / 7.0, - })), - auto_angle: true, - }, + colors: RailBackground::Pair( + palette.primary.base.color, + palette.secondary.base.color, + ), width: 4.0, border_radius: 2.0.into(), }, From 136f1d90eff0df318504075f10bbc681f6e5c095 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 26 Sep 2023 15:41:28 -0400 Subject: [PATCH 24/93] cleanup: clippy fixes and formatting Part of this is a refactor of the ID cleanup: clippy and fmt fix: test workflow fix: add note in CHANGELOG fix: clippy --- .github/workflows/document.yml | 2 +- .github/workflows/lint.yml | 10 ++++- .github/workflows/test.yml | 36 ++++++++++++++++-- accessibility/src/a11y_tree.rs | 6 +-- accessibility/src/node.rs | 2 +- accessibility/src/traits.rs | 2 +- core/src/id.rs | 37 ++++++++++++++++--- core/src/widget/operation/focusable.rs | 3 +- core/src/widget/text.rs | 2 +- core/src/widget/tree.rs | 16 +++++--- .../wayland/layer_surface.rs | 2 +- .../platform_specific/wayland/popup.rs | 2 +- .../platform_specific/wayland/window.rs | 2 +- sctk/src/application.rs | 32 ++++++++-------- sctk/src/commands/layer_surface.rs | 2 +- sctk/src/commands/window.rs | 2 +- sctk/src/event_loop/state.rs | 13 ++----- sctk/src/handlers/compositor.rs | 13 +++---- sctk/src/handlers/data_device/data_device.rs | 5 +-- sctk/src/handlers/data_device/data_offer.rs | 1 - sctk/src/handlers/shell/xdg_window.rs | 14 +++---- sctk/src/util.rs | 17 ++------- src/wayland/sandbox.rs | 8 +++- widget/src/button.rs | 7 ++-- widget/src/checkbox.rs | 2 +- widget/src/container.rs | 2 +- widget/src/text_input/text_input.rs | 4 +- widget/src/toggler.rs | 2 +- 28 files changed, 147 insertions(+), 99 deletions(-) diff --git a/.github/workflows/document.yml b/.github/workflows/document.yml index 35bf10f428..d916e26538 100644 --- a/.github/workflows/document.yml +++ b/.github/workflows/document.yml @@ -13,7 +13,7 @@ jobs: - name: Generate documentation run: | RUSTDOCFLAGS="--cfg docsrs" \ - cargo doc --no-deps --all-features \ + cargo doc --no-deps --features "winit" \ -p iced_core \ -p iced_highlighter \ -p iced_style \ diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2ff86614ad..d2c99c5183 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,6 +7,14 @@ jobs: - uses: hecrj/setup-rust-action@v1 with: components: clippy + - uses: actions/checkout@master + - name: Install dependencies + run: | + export DEBIAN_FRONTED=noninteractive + sudo apt-get -qq update + sudo apt-get install -y libxkbcommon-dev libwayland-dev - name: Check lints - run: cargo lint + run: | + cargo clippy --no-default-features --features "winit" --all-targets + cargo clippy --no-default-features --features "wayland wgpu svg canvas qr_code lazy debug tokio palette web-colors system a11y" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9c5ee0d949..b1183e596a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,8 +19,38 @@ jobs: run: | export DEBIAN_FRONTED=noninteractive sudo apt-get -qq update - sudo apt-get install -y libxkbcommon-dev libgtk-3-dev + sudo apt-get install -y libxkbcommon-dev libwayland-dev - name: Run tests run: | - cargo test --verbose --workspace - cargo test --verbose --workspace --all-features + cargo test --verbose --features "winit wgpu svg canvas qr_code lazy debug tokio palette web-colors system a11y" + cargo test -p iced_accessibility + cargo test -p iced_core + cargo test -p iced_futures + cargo test -p iced_graphics + cargo test -p iced_renderer + cargo test -p iced_runtime + cargo test -p iced_tiny_skia + cargo test -p iced_widget + cargo test -p iced_wgpu + - name: test wayland + if: matrix.os == 'ubuntu-latest' + run: | + cargo test --verbose --features "wayland wgpu svg canvas qr_code lazy debug tokio palette web-colors system a11y" + cargo test -p iced_sctk + + web: + runs-on: ubuntu-latest + steps: + - uses: hecrj/setup-rust-action@v1 + with: + rust-version: stable + targets: wasm32-unknown-unknown + - uses: actions/checkout@master + - name: Run checks + run: cargo check --package iced --target wasm32-unknown-unknown --no-default-features --features "winit" + - name: Check compilation of `tour` example + run: cargo build --package tour --target wasm32-unknown-unknown + - name: Check compilation of `todos` example + run: cargo build --package todos --target wasm32-unknown-unknown + - name: Check compilation of `integration` example + run: cargo build --package integration --target wasm32-unknown-unknown diff --git a/accessibility/src/a11y_tree.rs b/accessibility/src/a11y_tree.rs index 964b7656b1..bb61981571 100644 --- a/accessibility/src/a11y_tree.rs +++ b/accessibility/src/a11y_tree.rs @@ -1,4 +1,4 @@ -use crate::{A11yId, A11yNode}; +use crate::{A11yId, A11yNode, IdEq}; #[derive(Debug, Clone, Default)] /// Accessible tree of nodes @@ -64,8 +64,8 @@ impl A11yTree { } pub fn contains(&self, id: &A11yId) -> bool { - self.root.iter().any(|n| n.id() == id) - || self.children.iter().any(|n| n.id() == id) + self.root.iter().any(|n| IdEq::eq(n.id(), id)) + || self.children.iter().any(|n| IdEq::eq(n.id(), id)) } } diff --git a/accessibility/src/node.rs b/accessibility/src/node.rs index e419903c3c..cc99d496d0 100644 --- a/accessibility/src/node.rs +++ b/accessibility/src/node.rs @@ -2,7 +2,7 @@ use accesskit::NodeClassSet; use crate::A11yId; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct A11yNode { node: accesskit::NodeBuilder, id: A11yId, diff --git a/accessibility/src/traits.rs b/accessibility/src/traits.rs index be5ebb825e..88e1d9208e 100644 --- a/accessibility/src/traits.rs +++ b/accessibility/src/traits.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use crate::A11yId; -#[derive(Debug, Clone, PartialEq, Hash)] +#[derive(Debug, Clone, PartialEq)] pub enum Description<'a> { Text(Cow<'a, str>), Id(Vec), diff --git a/core/src/id.rs b/core/src/id.rs index e2af48ee00..3fc74ce925 100644 --- a/core/src/id.rs +++ b/core/src/id.rs @@ -37,7 +37,6 @@ impl Id { } // Not meant to be used directly -#[cfg(feature = "a11y")] impl From for Id { fn from(value: u64) -> Self { Self(Internal::Unique(value)) @@ -45,10 +44,9 @@ impl From for Id { } // Not meant to be used directly -#[cfg(feature = "a11y")] -impl Into for Id { - fn into(self) -> NonZeroU128 { - match &self.0 { +impl From for NonZeroU128 { + fn from(id: Id) -> NonZeroU128 { + match &id.0 { Internal::Unique(id) => NonZeroU128::try_from(*id as u128).unwrap(), Internal::Custom(id, _) => { NonZeroU128::try_from(*id as u128).unwrap() @@ -83,7 +81,7 @@ pub fn window_node_id() -> NonZeroU128 { } // TODO refactor to make panic impossible? -#[derive(Debug, Clone, Eq, Hash)] +#[derive(Debug, Clone, Eq)] /// Internal representation of an [`Id`]. pub enum Internal { /// a unique id @@ -98,6 +96,23 @@ pub enum Internal { } 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 { + /// Compare two Ids for equality based on their number or name + 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, @@ -116,6 +131,16 @@ impl PartialEq for Internal { } } +impl std::hash::Hash for Internal { + fn hash(&self, state: &mut H) { + match self { + Self::Unique(id) => id.hash(state), + Self::Custom(name, _) => name.hash(state), + Self::Set(ids) => ids.hash(state), + } + } +} + #[cfg(test)] mod tests { use super::Id; diff --git a/core/src/widget/operation/focusable.rs b/core/src/widget/operation/focusable.rs index 68c22faa8c..7365cf108d 100644 --- a/core/src/widget/operation/focusable.rs +++ b/core/src/widget/operation/focusable.rs @@ -1,4 +1,5 @@ //! Operate on widgets that can be focused. +use crate::id::IdEq; use crate::widget::operation::{Operation, Outcome}; use crate::widget::Id; use crate::Rectangle; @@ -34,7 +35,7 @@ pub fn focus(target: Id) -> impl Operation { impl Operation for Focus { fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) { match id { - Some(id) if id == &self.target => { + Some(id) if IdEq::eq(&id.0, &self.target.0) => { state.focus(); } _ => { diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index 32f2a8d3f7..4c83053f7e 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -224,7 +224,7 @@ where } fn id(&self) -> Option { - Some(self.id.clone().into()) + Some(self.id.clone()) } fn set_id(&mut self, id: crate::widget::Id) { diff --git a/core/src/widget/tree.rs b/core/src/widget/tree.rs index 3578d45af7..1a1ed1b8df 100644 --- a/core/src/widget/tree.rs +++ b/core/src/widget/tree.rs @@ -130,14 +130,14 @@ impl Tree { let children_len = self.children.len(); let (mut id_map, mut id_list): ( - HashMap, + 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); + if let Internal::Custom(_, ref name) = id.0 { + let _ = id_map.insert(name.to_string(), c); } else { id_list.push(c); } @@ -150,9 +150,13 @@ impl Tree { 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)) - { + let child_state = if let Some(c) = new_id.as_ref().and_then(|id| { + if let Internal::Custom(_, ref name) = id.0 { + id_map.remove(name.as_ref()) + } else { + None + } + }) { c } else if child_state_i < id_list.len() { let c = &mut id_list[child_state_i]; diff --git a/runtime/src/command/platform_specific/wayland/layer_surface.rs b/runtime/src/command/platform_specific/wayland/layer_surface.rs index 56b0df7ebe..aff488ddbf 100644 --- a/runtime/src/command/platform_specific/wayland/layer_surface.rs +++ b/runtime/src/command/platform_specific/wayland/layer_surface.rs @@ -160,7 +160,7 @@ impl Action { match self { Action::LayerSurface { builder, .. } => Action::LayerSurface { builder, - _phantom: PhantomData::default(), + _phantom: PhantomData, }, Action::Size { id, width, height } => { Action::Size { id, width, height } diff --git a/runtime/src/command/platform_specific/wayland/popup.rs b/runtime/src/command/platform_specific/wayland/popup.rs index 87e95a31eb..261de781b1 100644 --- a/runtime/src/command/platform_specific/wayland/popup.rs +++ b/runtime/src/command/platform_specific/wayland/popup.rs @@ -139,7 +139,7 @@ impl Action { match self { Action::Popup { popup, .. } => Action::Popup { popup, - _phantom: PhantomData::default(), + _phantom: PhantomData, }, Action::Destroy { id } => Action::Destroy { id }, Action::Grab { id } => Action::Grab { id }, diff --git a/runtime/src/command/platform_specific/wayland/window.rs b/runtime/src/command/platform_specific/wayland/window.rs index 5f7764093d..81f24c30fb 100644 --- a/runtime/src/command/platform_specific/wayland/window.rs +++ b/runtime/src/command/platform_specific/wayland/window.rs @@ -184,7 +184,7 @@ impl Action { match self { Action::Window { builder, .. } => Action::Window { builder, - _phantom: PhantomData::default(), + _phantom: PhantomData, }, Action::Size { id, width, height } => { Action::Size { id, width, height } diff --git a/sctk/src/application.rs b/sctk/src/application.rs index c624e9eeb9..a24bcccf59 100644 --- a/sctk/src/application.rs +++ b/sctk/src/application.rs @@ -1155,20 +1155,20 @@ where for (object_id, surface_id) in &surface_ids { let state = match states.get_mut(&surface_id.inner()) { Some(s) => { - if !s.needs_redraw() { - continue; - } else if auto_size_surfaces - .get(surface_id) - .map(|(w, h, _, dirty)| { - // don't redraw yet if the autosize state is dirty - *dirty || { - let Size { width, height } = - s.logical_size(); - width.round() as u32 != *w - || height.round() as u32 != *h - } - }) - .unwrap_or_default() + if !s.needs_redraw() + || auto_size_surfaces + .get(surface_id) + .map(|(w, h, _, dirty)| { + // don't redraw yet if the autosize state is dirty + *dirty || { + let Size { width, height } = + s.logical_size(); + width.round() as u32 != *w + || height.round() as u32 + != *h + } + }) + .unwrap_or_default() { continue; } else { @@ -1228,7 +1228,7 @@ where } if !sent_control_flow { let mut wait_500_ms = Instant::now(); - wait_500_ms = wait_500_ms + Duration::from_millis(250); + wait_500_ms += Duration::from_millis(250); _ = control_sender .start_send(ControlFlow::WaitUntil(wait_500_ms)); } @@ -2010,7 +2010,7 @@ where state.logical_size(), &state.title, debug, - id.clone(), // TODO: run the operation on every widget tree ? + *id, // TODO: run the operation on every widget tree ? auto_size_surfaces, proxy ); diff --git a/sctk/src/commands/layer_surface.rs b/sctk/src/commands/layer_surface.rs index b8846adeae..9558821a3a 100644 --- a/sctk/src/commands/layer_surface.rs +++ b/sctk/src/commands/layer_surface.rs @@ -25,7 +25,7 @@ pub fn get_layer_surface( platform_specific::Action::Wayland(wayland::Action::LayerSurface( wayland::layer_surface::Action::LayerSurface { builder, - _phantom: PhantomData::default(), + _phantom: PhantomData, }, )), )) diff --git a/sctk/src/commands/window.rs b/sctk/src/commands/window.rs index bf9923b4fe..9f3bb0dd52 100644 --- a/sctk/src/commands/window.rs +++ b/sctk/src/commands/window.rs @@ -18,7 +18,7 @@ pub fn get_window(builder: SctkWindowSettings) -> Command { platform_specific::Action::Wayland(wayland::Action::Window( wayland::window::Action::Window { builder, - _phantom: PhantomData::default(), + _phantom: PhantomData, }, )), )) diff --git a/sctk/src/event_loop/state.rs b/sctk/src/event_loop/state.rs index be3fb9e7eb..e2b0e017c0 100644 --- a/sctk/src/event_loop/state.rs +++ b/sctk/src/event_loop/state.rs @@ -122,12 +122,8 @@ impl SctkWindow { &mut self, LogicalSize { width, height }: LogicalSize, ) { - self.window.set_window_geometry( - 0, - 0, - width.get() as u32, - height.get() as u32, - ); + self.window + .set_window_geometry(0, 0, width.get(), height.get()); self.current_size = Some((width, height)); // Update the target viewport, this is used if and only if fractional scaling is in use. if let Some(viewport) = self.wp_viewport.as_ref() { @@ -403,10 +399,7 @@ impl SctkState { } popup.scale_factor = Some(scale_factor); if legacy { - let _ = popup - .popup - .wl_surface() - .set_buffer_scale(scale_factor as _); + popup.popup.wl_surface().set_buffer_scale(scale_factor as _); } self.compositor_updates.push(SctkEvent::PopupEvent { variant: PopupEventVariant::ScaleFactorChanged( diff --git a/sctk/src/handlers/compositor.rs b/sctk/src/handlers/compositor.rs index 3e77b21dfc..797861258a 100644 --- a/sctk/src/handlers/compositor.rs +++ b/sctk/src/handlers/compositor.rs @@ -2,12 +2,11 @@ use sctk::{ compositor::CompositorHandler, delegate_compositor, - reexports::client::{protocol::wl_surface, Connection, Proxy, QueueHandle}, - shell::WaylandSurface, + reexports::client::{protocol::wl_surface, Connection, QueueHandle}, }; use std::fmt::Debug; -use crate::{event_loop::state::SctkState, sctk_event::SctkEvent}; +use crate::event_loop::state::SctkState; impl CompositorHandler for SctkState { fn scale_factor_changed( @@ -32,10 +31,10 @@ impl CompositorHandler for SctkState { fn transform_changed( &mut self, - conn: &Connection, - qh: &QueueHandle, - surface: &wl_surface::WlSurface, - new_transform: sctk::reexports::client::protocol::wl_output::Transform, + _conn: &Connection, + _qh: &QueueHandle, + _surface: &wl_surface::WlSurface, + _new_transform: sctk::reexports::client::protocol::wl_output::Transform, ) { // TODO // this is not required diff --git a/sctk/src/handlers/data_device/data_device.rs b/sctk/src/handlers/data_device/data_device.rs index 0adf03c354..5eb4b36747 100644 --- a/sctk/src/handlers/data_device/data_device.rs +++ b/sctk/src/handlers/data_device/data_device.rs @@ -1,9 +1,6 @@ -use std::fmt::Debug; - use sctk::{ data_device_manager::{ - data_device::{DataDevice, DataDeviceHandler}, - data_offer::DragOffer, + data_device::DataDeviceHandler, data_offer::DragOffer, }, reexports::client::{protocol::wl_data_device, Connection, QueueHandle}, }; diff --git a/sctk/src/handlers/data_device/data_offer.rs b/sctk/src/handlers/data_device/data_offer.rs index b56f5810bd..2901c07038 100644 --- a/sctk/src/handlers/data_device/data_offer.rs +++ b/sctk/src/handlers/data_device/data_offer.rs @@ -4,7 +4,6 @@ use sctk::{ protocol::wl_data_device_manager::DndAction, Connection, QueueHandle, }, }; -use std::fmt::Debug; use crate::event_loop::state::SctkState; diff --git a/sctk/src/handlers/shell/xdg_window.rs b/sctk/src/handlers/shell/xdg_window.rs index b03b69870f..bf0d6fb207 100644 --- a/sctk/src/handlers/shell/xdg_window.rs +++ b/sctk/src/handlers/shell/xdg_window.rs @@ -84,16 +84,12 @@ impl WindowHandler for SctkState { .unwrap_or_else(|| NonZeroU32::new(500).unwrap()), ); } - configure - .new_size - .0 - .zip(configure.new_size.1) - .map(|new_size| { - window.update_size(LogicalSize { - width: new_size.0, - height: new_size.1, - }); + if let Some(new_size) = configure.new_size.0.zip(configure.new_size.1) { + window.update_size(LogicalSize { + width: new_size.0, + height: new_size.1, }); + } let wl_surface = window.window.wl_surface(); let id = wl_surface.clone(); diff --git a/sctk/src/util.rs b/sctk/src/util.rs index 81329b263a..fd6fa19715 100644 --- a/sctk/src/util.rs +++ b/sctk/src/util.rs @@ -33,8 +33,10 @@ pub enum CursorGrabMode { /// Describes the appearance of the mouse cursor. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Default)] pub enum CursorIcon { /// The platform-dependent default cursor. + #[default] Default, /// A simple crosshair. Crosshair, @@ -89,12 +91,6 @@ pub enum CursorIcon { RowResize, } -impl Default for CursorIcon { - fn default() -> Self { - CursorIcon::Default - } -} - #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Theme { Light, @@ -107,7 +103,7 @@ pub enum Theme { /// /// [`Critical`]: Self::Critical /// [`Informational`]: Self::Informational -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum UserAttentionType { /// ## Platform-specific /// @@ -118,11 +114,6 @@ pub enum UserAttentionType { /// /// - **macOS:** Bounces the dock icon once. /// - **Windows:** Flashes the taskbar button until the application is in focus. + #[default] Informational, } - -impl Default for UserAttentionType { - fn default() -> Self { - UserAttentionType::Informational - } -} diff --git a/src/wayland/sandbox.rs b/src/wayland/sandbox.rs index ead4975350..c545236cca 100644 --- a/src/wayland/sandbox.rs +++ b/src/wayland/sandbox.rs @@ -61,7 +61,7 @@ use crate::{ /// says "Hello, world!": /// /// ```no_run -/// use iced::{Element, Sandbox, Settings}; +/// use iced::{Element, Sandbox, Settings, window::Id}; /// /// pub fn main() -> iced::Result { /// Hello::run(Settings::default()) @@ -84,9 +84,13 @@ use crate::{ /// // This application has no interactions /// } /// -/// fn view(&self) -> Element { +/// fn view(&self, _: Id) -> Element { /// "Hello, world!".into() /// } +/// +/// fn close_requested(&self, _: Id) -> Self::Message { +/// unimplemented!() +/// } /// } /// ``` pub trait Sandbox { diff --git a/widget/src/button.rs b/widget/src/button.rs index 77521921fa..43ea77e1a3 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -3,6 +3,7 @@ //! A [`Button`] has some local [`State`]. use iced_runtime::core::widget::Id; use iced_runtime::{keyboard, Command}; +#[cfg(feature = "a11y")] use std::borrow::Cow; use crate::core::event::{self, Event}; @@ -370,7 +371,7 @@ where let child_tree = self.content .as_widget() - .a11y_nodes(child_layout, &child_tree, p); + .a11y_nodes(child_layout, child_tree, p); let Rectangle { x, @@ -485,7 +486,7 @@ impl State { /// Processes the given [`Event`] and updates the [`State`] of a [`Button`] /// accordingly. pub fn update<'a, Message: Clone>( - id: Id, + _id: Id, event: Event, layout: Layout<'_>, cursor: mouse::Cursor, @@ -532,7 +533,7 @@ pub fn update<'a, Message: Clone>( iced_accessibility::accesskit::ActionRequest { action, .. }, ) => { let state = state(); - if let Some(Some(on_press)) = (id == event_id + if let Some(Some(on_press)) = (_id == event_id && matches!( action, iced_accessibility::accesskit::Action::Default diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index af90585e6d..35361a9339 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -13,7 +13,7 @@ use crate::core::touch; use crate::core::widget; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - id::Internal, Alignment, Clipboard, Element, Layout, Length, Pixels, Point, + id::Internal, Alignment, Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, Widget, }; diff --git a/widget/src/container.rs b/widget/src/container.rs index eaae79243b..fdd0389567 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -460,7 +460,7 @@ pub fn visible_bounds(id: Id) -> Command> { } Command::widget(VisibleBounds { - target: id.into(), + target: id, depth: 0, scrollables: Vec::new(), bounds: None, diff --git a/widget/src/text_input/text_input.rs b/widget/src/text_input/text_input.rs index 3c65e100c9..c34d658e1d 100644 --- a/widget/src/text_input/text_input.rs +++ b/widget/src/text_input/text_input.rs @@ -5,9 +5,9 @@ pub use super::cursor::Cursor; pub use super::value::Value; use super::cursor; -use super::editor; + use super::editor::Editor; -use super::value; + use iced_renderer::core::widget::OperationOutputWrapper; use crate::core::alignment; diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index 56e5587fec..c0359f4db8 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -102,7 +102,7 @@ where labeled_by_widget: None, is_toggled, on_toggle: Box::new(f), - label: label, + label, width: Length::Fill, size: Self::DEFAULT_SIZE, text_size: None, From f0d510f5799c070b91b207348a13712cb37e6dab Mon Sep 17 00:00:00 2001 From: Eduardo Flores Date: Sat, 21 Oct 2023 18:09:00 -0700 Subject: [PATCH 25/93] feat: Add side mouse button events --- CHANGELOG.md | 1 + sctk/src/conversion.rs | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3226c1ba30..65e305d0bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Many thanks to... - @dtzxporter Many thanks to... +- @edfloreshz - @jackpot51 - @wash2 diff --git a/sctk/src/conversion.rs b/sctk/src/conversion.rs index 10564e7fed..f1f46d99f9 100644 --- a/sctk/src/conversion.rs +++ b/sctk/src/conversion.rs @@ -7,7 +7,10 @@ use sctk::{ reexports::client::protocol::wl_pointer::AxisSource, seat::{ keyboard::Modifiers, - pointer::{AxisScroll, CursorIcon, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT}, + pointer::{ + AxisScroll, CursorIcon, BTN_EXTRA, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT, + BTN_SIDE, + }, }, }; @@ -23,6 +26,10 @@ pub fn pointer_button_to_native(button: u32) -> Option { Some(mouse::Button::Right) } else if button == BTN_MIDDLE { Some(mouse::Button::Middle) + } else if button == BTN_SIDE { + Some(mouse::Button::Back) + } else if button == BTN_EXTRA { + Some(mouse::Button::Forward) } else { button.try_into().ok().map(mouse::Button::Other) } From cfe214e7747ef8d57939f9c0bae24250ef03f13b Mon Sep 17 00:00:00 2001 From: Michael Murphy Date: Fri, 3 Nov 2023 14:56:41 +0100 Subject: [PATCH 26/93] feat: add border radius to image rendering --- core/src/image.rs | 1 + graphics/src/primitive.rs | 2 + graphics/src/renderer.rs | 2 + renderer/src/lib.rs | 7 +- tiny_skia/src/backend.rs | 2 + tiny_skia/src/raster.rs | 149 ++++++++++++++++++++++++++++++++++++- widget/src/image.rs | 23 +++++- widget/src/image/viewer.rs | 1 + 8 files changed, 183 insertions(+), 4 deletions(-) diff --git a/core/src/image.rs b/core/src/image.rs index e9675316f5..a4eca37d44 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -193,5 +193,6 @@ pub trait Renderer: crate::Renderer { handle: Self::Handle, filter_method: FilterMethod, bounds: Rectangle, + border_radius: [f32; 4], ); } diff --git a/graphics/src/primitive.rs b/graphics/src/primitive.rs index aed59e1a91..0d7e231e22 100644 --- a/graphics/src/primitive.rs +++ b/graphics/src/primitive.rs @@ -80,6 +80,8 @@ pub enum Primitive { filter_method: image::FilterMethod, /// The bounds of the image bounds: Rectangle, + /// The border radii of the image + border_radius: [f32; 4], }, /// An SVG primitive Svg { diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index cb07c23bc5..f500261dec 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -220,11 +220,13 @@ where handle: image::Handle, filter_method: image::FilterMethod, bounds: Rectangle, + border_radius: [f32; 4], ) { self.primitives.push(Primitive::Image { handle, filter_method, bounds, + border_radius, }); } } diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index a7df414b9c..2c2cb8db4c 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -226,8 +226,13 @@ impl crate::core::image::Renderer for Renderer { handle: crate::core::image::Handle, filter_method: crate::core::image::FilterMethod, bounds: Rectangle, + border_radius: [f32; 4], ) { - delegate!(self, renderer, renderer.draw(handle, filter_method, bounds)); + delegate!( + self, + renderer, + renderer.draw(handle, filter_method, bounds, border_radius) + ); } } diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index f5bc75a211..a3581dd677 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -564,6 +564,7 @@ impl Backend { handle, filter_method, bounds, + border_radius, } => { let physical_bounds = (*bounds + translation) * scale_factor; @@ -587,6 +588,7 @@ impl Backend { pixels, transform, clip_mask, + *border_radius, ); } #[cfg(not(feature = "image"))] diff --git a/tiny_skia/src/raster.rs b/tiny_skia/src/raster.rs index 5f17ae60e0..d04fc9a19d 100644 --- a/tiny_skia/src/raster.rs +++ b/tiny_skia/src/raster.rs @@ -33,8 +33,9 @@ impl Pipeline { pixels: &mut tiny_skia::PixmapMut<'_>, transform: tiny_skia::Transform, clip_mask: Option<&tiny_skia::Mask>, + border_radius: [f32; 4], ) { - if let Some(image) = self.cache.borrow_mut().allocate(handle) { + if let Some(mut image) = self.cache.borrow_mut().allocate(handle) { let width_scale = bounds.width / image.width() as f32; let height_scale = bounds.height / image.height() as f32; @@ -48,6 +49,24 @@ impl Pipeline { tiny_skia::FilterQuality::Nearest } }; + let mut scratch; + + // Round the borders if a border radius is defined + if border_radius.iter().any(|&corner| corner != 0.0) { + scratch = image.to_owned(); + round(&mut scratch.as_mut(), { + let [a, b, c, d] = border_radius; + let scale_by = width_scale.min(height_scale); + let max_radius = image.width().min(image.height()) / 2; + [ + ((a / scale_by) as u32).max(1).min(max_radius), + ((b / scale_by) as u32).max(1).min(max_radius), + ((c / scale_by) as u32).max(1).min(max_radius), + ((d / scale_by) as u32).max(1).min(max_radius), + ] + }); + image = scratch.as_ref(); + } pixels.draw_pixmap( (bounds.x / width_scale) as i32, @@ -124,3 +143,131 @@ struct Entry { height: u32, pixels: Vec, } + +// https://users.rust-lang.org/t/how-to-trim-image-to-circle-image-without-jaggy/70374/2 +fn round(img: &mut tiny_skia::PixmapMut<'_>, radius: [u32; 4]) { + let (width, height) = (img.width(), img.height()); + assert!(radius[0] + radius[1] <= width); + assert!(radius[3] + radius[2] <= width); + assert!(radius[0] + radius[3] <= height); + assert!(radius[1] + radius[2] <= height); + + // top left + border_radius(img, radius[0], |x, y| (x - 1, y - 1)); + // top right + border_radius(img, radius[1], |x, y| (width - x, y - 1)); + // bottom right + border_radius(img, radius[2], |x, y| (width - x, height - y)); + // bottom left + border_radius(img, radius[3], |x, y| (x - 1, height - y)); +} + +fn border_radius( + img: &mut tiny_skia::PixmapMut<'_>, + r: u32, + coordinates: impl Fn(u32, u32) -> (u32, u32), +) { + if r == 0 { + return; + } + let r0 = r; + + // 16x antialiasing: 16x16 grid creates 256 possible shades, great for u8! + let r = 16 * r; + + let mut x = 0; + let mut y = r - 1; + let mut p: i32 = 2 - r as i32; + + // ... + + let mut alpha: u16 = 0; + let mut skip_draw = true; + + fn pixel_id(width: u32, (x, y): (u32, u32)) -> usize { + ((width as usize * y as usize) + x as usize) * 4 + } + + let clear_pixel = |img: &mut tiny_skia::PixmapMut<'_>, + (x, y): (u32, u32)| { + let pixel = pixel_id(img.width(), (x, y)); + img.data_mut()[pixel..pixel + 4].copy_from_slice(&[0; 4]); + }; + + let draw = |img: &mut tiny_skia::PixmapMut<'_>, alpha, x, y| { + debug_assert!((1..=256).contains(&alpha)); + let pixel = pixel_id(img.width(), coordinates(r0 - x, r0 - y)); + let pixel_alpha = &mut img.data_mut()[pixel + 3]; + *pixel_alpha = ((alpha * *pixel_alpha as u16 + 128) / 256) as u8; + }; + + 'l: loop { + // (comments for bottom_right case:) + // remove contents below current position + { + let i = x / 16; + for j in y / 16 + 1..r0 { + clear_pixel(img, coordinates(r0 - i, r0 - j)); + } + } + // remove contents right of current position mirrored + { + let j = x / 16; + for i in y / 16 + 1..r0 { + clear_pixel(img, coordinates(r0 - i, r0 - j)); + } + } + + // draw when moving to next pixel in x-direction + if !skip_draw { + draw(img, alpha, x / 16 - 1, y / 16); + draw(img, alpha, y / 16, x / 16 - 1); + alpha = 0; + } + + for _ in 0..16 { + skip_draw = false; + + if x >= y { + break 'l; + } + + alpha += y as u16 % 16 + 1; + if p < 0 { + x += 1; + p += (2 * x + 2) as i32; + } else { + // draw when moving to next pixel in y-direction + if y % 16 == 0 { + draw(img, alpha, x / 16, y / 16); + draw(img, alpha, y / 16, x / 16); + skip_draw = true; + alpha = (x + 1) as u16 % 16 * 16; + } + + x += 1; + p -= (2 * (y - x) + 2) as i32; + y -= 1; + } + } + } + + // one corner pixel left + if x / 16 == y / 16 { + // column under current position possibly not yet accounted + if x == y { + alpha += y as u16 % 16 + 1; + } + let s = y as u16 % 16 + 1; + let alpha = 2 * alpha - s * s; + draw(img, alpha, x / 16, y / 16); + } + + // remove remaining square of content in the corner + let range = y / 16 + 1..r0; + for i in range.clone() { + for j in range.clone() { + clear_pixel(img, coordinates(r0 - i, r0 - j)); + } + } +} diff --git a/widget/src/image.rs b/widget/src/image.rs index 4442186b4b..01eca9d917 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -12,11 +12,13 @@ use crate::core::{ ContentFit, Element, Layout, Length, Rectangle, Size, Vector, Widget, }; -use std::borrow::Cow; use std::hash::Hash; pub use image::{FilterMethod, Handle}; +#[cfg(feature = "a11y")] +use std::borrow::Cow; + /// Creates a new [`Viewer`] with the given image `Handle`. pub fn viewer(handle: Handle) -> Viewer { Viewer::new(handle) @@ -47,6 +49,7 @@ pub struct Image<'a, Handle> { height: Length, content_fit: ContentFit, filter_method: FilterMethod, + border_radius: [f32; 4], phantom_data: std::marker::PhantomData<&'a ()>, } @@ -66,10 +69,17 @@ impl<'a, Handle> Image<'a, Handle> { height: Length::Shrink, content_fit: ContentFit::Contain, filter_method: FilterMethod::default(), + border_radius: [0.0; 4], phantom_data: std::marker::PhantomData, } } + /// Sets the border radius of the image. + pub fn border_radius(mut self, border_radius: [f32; 4]) -> Self { + self.border_radius = border_radius; + self + } + /// Sets the width of the [`Image`] boundaries. pub fn width(mut self, width: impl Into) -> Self { self.width = width.into(); @@ -140,6 +150,7 @@ pub fn layout( width: Length, height: Length, content_fit: ContentFit, + border_radius: [f32; 4], ) -> layout::Node where Renderer: image::Renderer, @@ -179,6 +190,7 @@ pub fn draw( handle: &Handle, content_fit: ContentFit, filter_method: FilterMethod, + border_radius: [f32; 4], ) where Renderer: image::Renderer, Handle: Clone + Hash, @@ -201,7 +213,12 @@ pub fn draw( ..bounds }; - renderer.draw(handle.clone(), filter_method, drawing_bounds + offset); + renderer.draw( + handle.clone(), + filter_method, + drawing_bounds + offset, + border_radius, + ); }; if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height @@ -238,6 +255,7 @@ where self.width, self.height, self.content_fit, + self.border_radius, ) } @@ -257,6 +275,7 @@ where &self.handle, self.content_fit, self.filter_method, + self.border_radius, ); } diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 9666ff9f0e..fb4dc73f28 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -338,6 +338,7 @@ where y: bounds.y, ..Rectangle::with_size(image_size) }, + [0.0; 4], ); }); }); From e2a3b831514c6ce7472b040dbe33d842a447a6f0 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 3 Nov 2023 16:11:05 +0100 Subject: [PATCH 27/93] fix(wgpu): handle border_radius property with image raster --- wgpu/src/layer.rs | 2 ++ wgpu/src/layer/image.rs | 3 +++ 2 files changed, 5 insertions(+) diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index 1d30bbfeb1..f5e7eac44a 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -220,6 +220,7 @@ impl<'a> Layer<'a> { handle, filter_method, bounds, + border_radius, } => { let layer = &mut layers[current_layer]; @@ -227,6 +228,7 @@ impl<'a> Layer<'a> { handle: handle.clone(), filter_method: *filter_method, bounds: *bounds + translation, + border_radius: *border_radius, }); } Primitive::Svg { diff --git a/wgpu/src/layer/image.rs b/wgpu/src/layer/image.rs index facbe1922c..0ea17bbb34 100644 --- a/wgpu/src/layer/image.rs +++ b/wgpu/src/layer/image.rs @@ -15,6 +15,9 @@ pub enum Image { /// The bounds of the image. bounds: Rectangle, + + /// Border radius to apply + border_radius: [f32; 4], }, /// A vector image. Vector { From 3db690465656fa97f3409932db64b6dbff50f95b Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 3 Nov 2023 12:24:17 -0600 Subject: [PATCH 28/93] iced_core: feature for serde serialization of KeyCode --- core/Cargo.toml | 5 +++++ core/src/keyboard/key.rs | 1 + widget/src/svg.rs | 7 +++++-- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/core/Cargo.toml b/core/Cargo.toml index bb49c232d7..8608dd11f0 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -28,6 +28,11 @@ sctk.optional = true palette.workspace = true palette.optional = true +[dependencies.serde] +version = "1" +optional = true +features = ["serde_derive"] + [target.'cfg(windows)'.dependencies] raw-window-handle.workspace = true diff --git a/core/src/keyboard/key.rs b/core/src/keyboard/key.rs index dbde51965c..bf9a93c4f7 100644 --- a/core/src/keyboard/key.rs +++ b/core/src/keyboard/key.rs @@ -7,6 +7,7 @@ use crate::SmolStr; /// /// [`winit`]: https://docs.rs/winit/0.29.10/winit/keyboard/enum.Key.html #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Key { /// A key with an established name. Named(Named), diff --git a/widget/src/svg.rs b/widget/src/svg.rs index f5cae3e61f..bc5b3407b3 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -11,6 +11,7 @@ use crate::core::{ }; use std::borrow::Cow; +use std::marker::PhantomData; use std::path::PathBuf; pub use crate::style::svg::{Appearance, StyleSheet}; @@ -40,6 +41,7 @@ where content_fit: ContentFit, style: ::Style, symbolic: bool, + _phantom_data: PhantomData<&'a ()>, } impl<'a, Theme> Svg<'a, Theme> @@ -62,6 +64,7 @@ where content_fit: ContentFit::Contain, symbolic: false, style: Default::default(), + _phantom_data: PhantomData::default(), } } @@ -196,7 +199,7 @@ where _state: &Tree, renderer: &mut Renderer, theme: &Theme, - _style: &renderer::Style, + style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, _viewport: &Rectangle, @@ -220,7 +223,7 @@ where ..bounds }; - let appearance = if is_mouse_over { + let mut appearance = if is_mouse_over { theme.hovered(&self.style) } else { theme.appearance(&self.style) From fa679b38c0ad70d357bdfc626e3b2d6717aed65c Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Mon, 13 Nov 2023 15:54:13 -0700 Subject: [PATCH 29/93] Hack to remove image blur --- wgpu/src/image.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs index 1e5d3ee0d9..13d06b2e59 100644 --- a/wgpu/src/image.rs +++ b/wgpu/src/image.rs @@ -607,12 +607,12 @@ fn add_instance( _position: position, _size: size, _position_in_atlas: [ - (x as f32 + 0.5) / atlas::SIZE as f32, - (y as f32 + 0.5) / atlas::SIZE as f32, + x as f32 / atlas::SIZE as f32, + y as f32 / atlas::SIZE as f32, ], _size_in_atlas: [ - (width as f32 - 1.0) / atlas::SIZE as f32, - (height as f32 - 1.0) / atlas::SIZE as f32, + width as f32 / atlas::SIZE as f32, + height as f32 / atlas::SIZE as f32, ], _layer: layer as u32, }; From 5109b619abf00aa5a659fab888cb985478b7da99 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 15 Nov 2023 18:58:14 -0500 Subject: [PATCH 30/93] chore: update softbuffer --- graphics/src/compositor.rs | 9 +++++++++ wgpu/src/image.rs | 1 + 2 files changed, 10 insertions(+) diff --git a/graphics/src/compositor.rs b/graphics/src/compositor.rs index 0188f4d8b2..030ff27f2a 100644 --- a/graphics/src/compositor.rs +++ b/graphics/src/compositor.rs @@ -111,6 +111,15 @@ pub enum SurfaceError { /// There is no more memory left to allocate a new frame. #[error("There is no more memory left to allocate a new frame")] OutOfMemory, + /// Resize Error + #[error("Resize Error")] + Resize, + /// Invalid dimensions + #[error("Invalid dimensions")] + InvalidDimensions, + /// Present Error + #[error("Present Error")] + Present(String), } /// Contains information about the graphics (e.g. graphics adapter, graphics backend). diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs index 13d06b2e59..c7e91bb1b2 100644 --- a/wgpu/src/image.rs +++ b/wgpu/src/image.rs @@ -400,6 +400,7 @@ impl Pipeline { handle, filter_method, bounds, + .. } => { if let Some(atlas_entry) = raster_cache.upload( device, From 3c95946f70536a0a56d834d69f0f3f49cd646f31 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Fri, 17 Nov 2023 23:44:49 +0100 Subject: [PATCH 31/93] Update mod.rs --- src/wayland/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wayland/mod.rs b/src/wayland/mod.rs index 0265f16482..225b583ae3 100644 --- a/src/wayland/mod.rs +++ b/src/wayland/mod.rs @@ -31,7 +31,7 @@ pub trait Application: Sized { type Theme: Default + StyleSheet; /// The data needed to initialize your [`Application`]. - type Flags: Clone; + type Flags; /// Initializes the [`Application`] with the flags provided to /// [`run`] as part of the [`Settings`]. From fa62d43c0997a255400bb89ba2672744b483720b Mon Sep 17 00:00:00 2001 From: Daniel Eades Date: Sun, 19 Nov 2023 15:13:55 +0000 Subject: [PATCH 32/93] refactor --- renderer/src/lib.rs | 4 ++-- widget/src/checkbox.rs | 1 + widget/src/scrollable.rs | 1 + widget/src/svg.rs | 1 + widget/src/toggler.rs | 1 + winit/src/application.rs | 2 ++ 6 files changed, 8 insertions(+), 2 deletions(-) diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index 2c2cb8db4c..7f8bb6a4d9 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -264,8 +264,8 @@ impl crate::graphics::geometry::Renderer for Renderer { crate::Geometry::TinySkia(primitive) => { renderer.draw_primitive(primitive); } - #[cfg(feature = "wgpu")] - crate::Geometry::Wgpu(_) => unreachable!(), + #[allow(unreachable_patterns)] + _ => unreachable!(), } } } diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index 35361a9339..852e374445 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -1,6 +1,7 @@ //! Show toggle controls using checkboxes. use iced_renderer::core::Size; use iced_runtime::core::widget::Id; +#[cfg(feature = "a11y")] use std::borrow::Cow; use crate::core::alignment; diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index cd654ccfed..138fdb7aa8 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -1,5 +1,6 @@ //! Navigate an endless amount of content with a scrollbar. use iced_runtime::core::widget::Id; +#[cfg(feature = "a11y")] use std::borrow::Cow; use crate::core::event::{self, Event}; diff --git a/widget/src/svg.rs b/widget/src/svg.rs index bc5b3407b3..ba51ee5551 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -10,6 +10,7 @@ use crate::core::{ ContentFit, Element, Layout, Length, Rectangle, Size, Vector, Widget, }; +#[cfg(feature = "a11y")] use std::borrow::Cow; use std::marker::PhantomData; use std::path::PathBuf; diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index c0359f4db8..fba9d63b96 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -1,4 +1,5 @@ //! Show toggle controls using togglers. +#[cfg(feature = "a11y")] use std::borrow::Cow; use crate::core::alignment; diff --git a/winit/src/application.rs b/winit/src/application.rs index e515551603..37510bf055 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -4,6 +4,7 @@ mod drag_resize; mod profiler; mod state; +#[cfg(feature = "a11y")] use iced_graphics::core::widget::operation::focusable::focus; use iced_graphics::core::widget::operation::OperationWrapper; use iced_graphics::core::widget::Operation; @@ -365,6 +366,7 @@ async fn run_instance( let mut events = Vec::new(); let mut messages = Vec::new(); let mut redraw_pending = false; + #[cfg(feature = "a11y")] let mut commands: Vec> = Vec::new(); #[cfg(feature = "a11y")] From 6c34f07350edc02aab147bf7373ff5a8333887cb Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 28 Nov 2023 09:59:43 -0500 Subject: [PATCH 33/93] chore: cleanup iced_widget --- widget/src/button.rs | 1 - widget/src/checkbox.rs | 4 ++-- widget/src/image.rs | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/widget/src/button.rs b/widget/src/button.rs index 43ea77e1a3..ddfd7ba87a 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -209,7 +209,6 @@ where } 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)) } diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index 852e374445..410f2ee629 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -14,8 +14,8 @@ use crate::core::touch; use crate::core::widget; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - id::Internal, Alignment, Clipboard, Element, Layout, Length, Pixels, - Rectangle, Shell, Widget, + id::Internal, Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, + Widget, }; pub use crate::style::checkbox::{Appearance, StyleSheet}; diff --git a/widget/src/image.rs b/widget/src/image.rs index 01eca9d917..673169f0a1 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -150,7 +150,7 @@ pub fn layout( width: Length, height: Length, content_fit: ContentFit, - border_radius: [f32; 4], + _border_radius: [f32; 4], ) -> layout::Node where Renderer: image::Renderer, From 566840800824ec35f62171c2a74129dae19b9686 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 28 Nov 2023 10:34:41 -0500 Subject: [PATCH 34/93] cleanup git workflows --- .github/workflows/lint.yml | 4 ++-- .github/workflows/test.yml | 4 ++-- core/src/overlay.rs | 2 +- src/application.rs | 5 ----- src/lib.rs | 6 ++---- src/sandbox.rs | 2 -- src/settings.rs | 13 +++---------- src/wayland/mod.rs | 8 +------- src/wayland/sandbox.rs | 5 ----- 9 files changed, 11 insertions(+), 38 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d2c99c5183..fa42f9a047 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,7 +2,7 @@ name: Lint on: [push, pull_request] jobs: all: - runs-on: macOS-latest + runs-on: ubuntu-latest steps: - uses: hecrj/setup-rust-action@v1 with: @@ -17,4 +17,4 @@ jobs: - name: Check lints run: | cargo clippy --no-default-features --features "winit" --all-targets - cargo clippy --no-default-features --features "wayland wgpu svg canvas qr_code lazy debug tokio palette web-colors system a11y" + cargo clippy --no-default-features --features "wayland wgpu svg canvas qr_code lazy debug tokio palette web-colors a11y" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b1183e596a..db548fa31d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,7 +22,7 @@ jobs: sudo apt-get install -y libxkbcommon-dev libwayland-dev - name: Run tests run: | - cargo test --verbose --features "winit wgpu svg canvas qr_code lazy debug tokio palette web-colors system a11y" + cargo test --verbose --features "winit wgpu svg canvas qr_code lazy debug tokio palette web-colors a11y" cargo test -p iced_accessibility cargo test -p iced_core cargo test -p iced_futures @@ -35,7 +35,7 @@ jobs: - name: test wayland if: matrix.os == 'ubuntu-latest' run: | - cargo test --verbose --features "wayland wgpu svg canvas qr_code lazy debug tokio palette web-colors system a11y" + cargo test --verbose --features "wayland wgpu svg canvas qr_code lazy debug tokio palette web-colors a11y" cargo test -p iced_sctk web: diff --git a/core/src/overlay.rs b/core/src/overlay.rs index cc32abc2b6..e0a9cf2009 100644 --- a/core/src/overlay.rs +++ b/core/src/overlay.rs @@ -42,7 +42,7 @@ where cursor: mouse::Cursor, ); - /// Applies a [`widget::Operation`] to the [`Overlay`]. + /// Applies an [`Operation`] to the [`Overlay`]. fn operate( &mut self, _layout: Layout<'_>, diff --git a/src/application.rs b/src/application.rs index 67175ad5f4..ccde2efba2 100644 --- a/src/application.rs +++ b/src/application.rs @@ -25,8 +25,6 @@ pub use crate::style::application::{Appearance, StyleSheet}; /// # Examples /// [The repository has a bunch of examples] that use the [`Application`] trait: /// -/// - [`clock`], an application that uses the [`Canvas`] widget to draw a clock -/// and its hands to display the current time. /// - [`download_progress`], a basic application that asynchronously downloads /// a dummy file of 100 MB and tracks the download progress. /// - [`events`], a log of native events displayed using a conditional @@ -35,8 +33,6 @@ pub use crate::style::application::{Appearance, StyleSheet}; /// by [John Horton Conway]. /// - [`pokedex`], an application that displays a random Pokédex entry (sprite /// included!) by using the [PokéAPI]. -/// - [`solar_system`], an animated solar system drawn using the [`Canvas`] widget -/// and showcasing how to compose different transforms. /// - [`stopwatch`], a watch with start/stop and reset buttons showcasing how /// to listen to time. /// - [`todos`], a todos tracker inspired by [TodoMVC]. @@ -51,7 +47,6 @@ pub use crate::style::application::{Appearance, StyleSheet}; /// [`stopwatch`]: https://github.com/iced-rs/iced/tree/0.10/examples/stopwatch /// [`todos`]: https://github.com/iced-rs/iced/tree/0.10/examples/todos /// [`Sandbox`]: crate::Sandbox -/// [`Canvas`]: crate::widget::Canvas /// [PokéAPI]: https://pokeapi.co/ /// [TodoMVC]: http://todomvc.com/ /// diff --git a/src/lib.rs b/src/lib.rs index 470f63044d..6124a1ee3b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -143,7 +143,7 @@ //! 1. Draw the resulting user interface. //! //! # Usage -//! The [`Application`] and [`Sandbox`] traits should get you started quickly, +//! The [`Application`] trait should get you started quickly, //! streamlining all the process described above! //! //! [Elm]: https://elm-lang.org/ @@ -351,7 +351,5 @@ pub type Element< Renderer = crate::Renderer, > = crate::core::Element<'a, Message, Theme, Renderer>; -/// The result of running an [`Application`]. -/// -/// [`Application`]: crate::Application +/// The result of running an application. pub type Result = std::result::Result<(), Error>; diff --git a/src/sandbox.rs b/src/sandbox.rs index 825a0b60e2..65a48519e2 100644 --- a/src/sandbox.rs +++ b/src/sandbox.rs @@ -44,10 +44,8 @@ use crate::{Application, Command, Element, Error, Settings, Subscription}; /// [`styling`]: https://github.com/iced-rs/iced/tree/0.10/examples/styling /// [`svg`]: https://github.com/iced-rs/iced/tree/0.10/examples/svg /// [`tour`]: https://github.com/iced-rs/iced/tree/0.10/examples/tour -/// [`Canvas widget`]: crate::widget::Canvas /// [the overview]: index.html#overview /// [`iced_wgpu`]: https://github.com/iced-rs/iced/tree/0.10/wgpu -/// [`Svg` widget]: crate::widget::Svg /// [Ghostscript Tiger]: https://commons.wikimedia.org/wiki/File:Ghostscript_Tiger.svg /// /// ## A simple "Hello, world!" diff --git a/src/settings.rs b/src/settings.rs index 1cb56290ee..c45922f5f4 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -27,9 +27,7 @@ pub struct Settings { #[cfg(feature = "wayland")] pub initial_surface: InitialSurface, - /// The data needed to initialize the [`Application`]. - /// - /// [`Application`]: crate::Application + /// The data needed to initialize the Application. pub flags: Flags, /// The fonts to load on boot. @@ -48,12 +46,9 @@ pub struct Settings { /// If set to true, the renderer will try to perform antialiasing for some /// primitives. /// - /// Enabling it can produce a smoother result in some widgets, like the - /// [`Canvas`], at a performance cost. + /// Enabling it can produce a smoother result in some widgets /// /// By default, it is disabled. - /// - /// [`Canvas`]: crate::widget::Canvas pub antialiasing: bool, /// If set to true the application will exit when the main window is closed. @@ -62,9 +57,7 @@ pub struct Settings { #[cfg(not(any(feature = "winit", feature = "wayland")))] impl Settings { - /// Initialize [`Application`] settings using the given data. - /// - /// [`Application`]: crate::Application + /// Initialize Application settings using the given data. pub fn with_flags(flags: Flags) -> Self { let default_settings = Settings::<()>::default(); Self { diff --git a/src/wayland/mod.rs b/src/wayland/mod.rs index 225b583ae3..3b0f50404b 100644 --- a/src/wayland/mod.rs +++ b/src/wayland/mod.rs @@ -12,9 +12,6 @@ pub use iced_sctk::{application::SurfaceIdWrapper, commands::*, settings::*}; /// /// Unlike the impure version, the `view` method of this trait takes an /// immutable reference to `self` and returns a pure [`Element`]. -/// -/// [`Application`]: crate::Application -/// [`Element`]: pure::Element pub trait Application: Sized { /// The [`Executor`] that will run commands and subscriptions. /// @@ -67,10 +64,7 @@ pub trait Application: Sized { Self::Theme::default() } - /// Returns the current [`Style`] of the [`Theme`]. - /// - /// [`Style`]: ::Style - /// [`Theme`]: Self::Theme + /// Returns the current Style of the Theme. fn style(&self) -> ::Style { Default::default() } diff --git a/src/wayland/sandbox.rs b/src/wayland/sandbox.rs index c545236cca..e66e4418de 100644 --- a/src/wayland/sandbox.rs +++ b/src/wayland/sandbox.rs @@ -34,8 +34,6 @@ use crate::{ /// slider. /// - [`styling`], an example showcasing custom styling with a light and dark /// theme. -/// - [`svg`], an application that renders the [Ghostscript Tiger] by leveraging -/// the [`Svg` widget]. /// - [`tour`], a simple UI tour that can run both on native platforms and the /// web! /// @@ -47,12 +45,9 @@ use crate::{ /// [`pane_grid`]: https://github.com/iced-rs/iced/tree/0.4/examples/pane_grid /// [`progress_bar`]: https://github.com/iced-rs/iced/tree/0.4/examples/progress_bar /// [`styling`]: https://github.com/iced-rs/iced/tree/0.4/examples/styling -/// [`svg`]: https://github.com/iced-rs/iced/tree/0.4/examples/svg /// [`tour`]: https://github.com/iced-rs/iced/tree/0.4/examples/tour -/// [`Canvas widget`]: crate::widget::Canvas /// [the overview]: index.html#overview /// [`iced_wgpu`]: https://github.com/iced-rs/iced/tree/0.4/wgpu -/// [`Svg` widget]: crate::widget::Svg /// [Ghostscript Tiger]: https://commons.wikimedia.org/wiki/File:Ghostscript_Tiger.svg /// /// ## A simple "Hello, world!" From b004b7544f40b7d0fdcbdc7053147ec8c9658401 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 28 Nov 2023 11:22:54 -0500 Subject: [PATCH 35/93] cleanup: clippy --- core/src/event/wayland/data_device.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/event/wayland/data_device.rs b/core/src/event/wayland/data_device.rs index efd4ed1c12..342352ef37 100644 --- a/core/src/event/wayland/data_device.rs +++ b/core/src/event/wayland/data_device.rs @@ -1,9 +1,9 @@ use sctk::{ - data_device_manager::{data_offer::DragOffer, ReadPipe}, + data_device_manager::ReadPipe, reexports::client::protocol::wl_data_device_manager::DndAction, }; use std::{ - os::fd::{AsRawFd, OwnedFd, RawFd}, + os::fd::{AsRawFd, OwnedFd}, sync::{Arc, Mutex}, }; From df3452ce62f176d7728e79dcd6487f539a473e13 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 28 Nov 2023 14:17:01 -0500 Subject: [PATCH 36/93] fix: editor and sctk_todos examples --- examples/editor/Cargo.toml | 2 +- examples/sctk_todos/Cargo.toml | 13 +------------ 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/examples/editor/Cargo.toml b/examples/editor/Cargo.toml index dc885728ca..37378d61f6 100644 --- a/examples/editor/Cargo.toml +++ b/examples/editor/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["highlighter", "tokio", "debug"] +iced.features = ["highlighter", "tokio", "debug", "winit"] tokio.workspace = true tokio.features = ["fs"] diff --git a/examples/sctk_todos/Cargo.toml b/examples/sctk_todos/Cargo.toml index 9e0e1f8e98..fc1b9d0b8f 100644 --- a/examples/sctk_todos/Cargo.toml +++ b/examples/sctk_todos/Cargo.toml @@ -15,19 +15,8 @@ iced_style = { path = "../../style" } sctk.workspace = true log = "0.4.17" env_logger = "0.10.0" - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] async-std = "1.0" -directories-next = "2.0" - -[target.'cfg(target_arch = "wasm32")'.dependencies] -web-sys = { version = "0.3", features = ["Window", "Storage"] } -wasm-timer = "0.2" +directories-next = "2.0.0" -[package.metadata.deb] -assets = [ - ["target/release-opt/todos", "usr/bin/iced-todos", "755"], - ["iced-todos.desktop", "usr/share/applications/", "644"], -] [profile.release-opt] debug = true From 7d710e9fa07f00a1009ee3e15b8868ec3205514b Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 28 Nov 2023 14:17:22 -0500 Subject: [PATCH 37/93] reexport limits --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6124a1ee3b..7e7c03fab7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -217,8 +217,8 @@ pub use crate::core::border::{self, Border, Radius}; pub use crate::core::color; pub use crate::core::gradient; pub use crate::core::{ - id, Alignment, Background, Color, ContentFit, Degrees, Gradient, Length, - Padding, Pixels, Point, Radians, Rectangle, Size, Vector, + id, layout::Limits, Alignment, Background, Color, ContentFit, Degrees, + Gradient, Length, Padding, Pixels, Point, Radians, Rectangle, Size, Vector, }; pub mod clipboard { From da1c08d223234469196b517774d2661c2788289e Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 29 Nov 2023 22:20:19 -0500 Subject: [PATCH 38/93] refactor: udpate gradient angles for slider --- examples/gradient/Cargo.toml | 3 ++- widget/src/slider.rs | 2 +- widget/src/vertical_slider.rs | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/gradient/Cargo.toml b/examples/gradient/Cargo.toml index 2dea2c4f59..aef35ad7a2 100644 --- a/examples/gradient/Cargo.toml +++ b/examples/gradient/Cargo.toml @@ -5,4 +5,5 @@ edition = "2021" publish = false [dependencies] -iced = { path = "../.." } +iced = { path = "../..", features = ["winit", "wgpu"]} + diff --git a/widget/src/slider.rs b/widget/src/slider.rs index bfd175cf1f..7640c908af 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -586,7 +586,7 @@ pub fn draw( ..renderer::Quad::default() }, if auto_angle { - gradient.angle = Radians::from(Degrees(180.0)); + gradient.angle = Radians::from(Degrees(90.0)); gradient } else { gradient diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index 38417e03f6..7b5f44edae 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -433,7 +433,7 @@ pub fn draw( ..renderer::Quad::default() }, if auto_angle { - gradient.angle = Radians::from(Degrees(90.0)); + gradient.angle = Radians::from(Degrees(180.0)); gradient } else { gradient From f08ff472ef614ee20aba147033969348f6c8aedd Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 30 Nov 2023 11:27:37 -0500 Subject: [PATCH 39/93] fix: tooltip children and diff --- widget/src/tooltip.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs index f87925d6df..c34cf23b9b 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -136,7 +136,10 @@ where } fn diff(&mut self, tree: &mut crate::core::widget::Tree) { - tree.diff_children(std::slice::from_mut(&mut self.content)) + tree.diff_children(&mut [ + self.content.as_widget_mut(), + &mut self.tooltip, + ]) } fn layout( From 9bbb559fa86b8208628c9fe41ceb423a1be0695a Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 30 Nov 2023 13:38:54 -0500 Subject: [PATCH 40/93] fix: add back the window id to the frames subscription --- runtime/src/window.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/runtime/src/window.rs b/runtime/src/window.rs index 4622b100e6..217b6a00c0 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -23,9 +23,11 @@ use crate::futures::Subscription; /// /// In any case, this [`Subscription`] is useful to smoothly draw application-driven /// animations without missing any frames. -pub fn frames() -> Subscription { +pub fn frames() -> Subscription<(Id, Instant)> { event::listen_raw(|event, _status| match event { - iced_core::Event::Window(_, Event::RedrawRequested(at)) => Some(at), + iced_core::Event::Window(id, Event::RedrawRequested(at)) => { + Some((id, at)) + } _ => None, }) } From 66daa6156efeacb4d94f40b62f74116460323f3b Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 30 Nov 2023 15:02:20 -0500 Subject: [PATCH 41/93] fix: CI tests --- examples/svg/Cargo.toml | 2 +- sctk/src/application.rs | 24 +++++++++++------------- sctk/src/event_loop/mod.rs | 4 ++-- src/wayland/mod.rs | 4 ++-- widget/src/lib.rs | 2 -- 5 files changed, 16 insertions(+), 20 deletions(-) diff --git a/examples/svg/Cargo.toml b/examples/svg/Cargo.toml index 78208fb0b0..d5b95d0178 100644 --- a/examples/svg/Cargo.toml +++ b/examples/svg/Cargo.toml @@ -7,4 +7,4 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["svg"] +iced.features = ["svg", "winit"] diff --git a/sctk/src/application.rs b/sctk/src/application.rs index a24bcccf59..34e2d9764b 100644 --- a/sctk/src/application.rs +++ b/sctk/src/application.rs @@ -29,7 +29,7 @@ use iced_futures::{ renderer::Style, time::Instant, widget::{ - operation::{self, focusable::focus, OperationWrapper}, + operation::{self, OperationWrapper}, tree, Operation, Tree, }, Widget, @@ -55,7 +55,7 @@ use std::{ use wayland_backend::client::ObjectId; use wayland_protocols::wp::viewporter::client::wp_viewport::WpViewport; -use iced_graphics::{compositor, renderer, Compositor, Viewport}; +use iced_graphics::{compositor, Compositor, Viewport}; use iced_runtime::{ clipboard, command::{ @@ -809,7 +809,7 @@ where // Dnd Surfaces are only drawn once let id = wl_surface.id(); - let (native_id, e, node) = match dnd_icon { + let (native_id, _e, node) = match dnd_icon { DndIcon::Custom(id) => { let mut e = application.view(id); let state = e.as_widget().state(); @@ -1406,12 +1406,14 @@ where }); } Action::Focus => { - commands.push(Command::widget(focus( - iced_runtime::core::id::Id::from(u128::from( - request.target.0, - ) - as u64), - ))); + commands.push(Command::widget( + operation::focusable::focus( + iced_runtime::core::id::Id::from(u128::from( + request.target.0, + ) + as u64), + ), + )); } Action::Blur => todo!(), Action::Collapse => todo!(), @@ -1646,10 +1648,6 @@ where self.frame = frame; } - pub(crate) fn frame(&self) -> Option<&WlSurface> { - self.frame.as_ref() - } - pub(crate) fn first(&self) -> bool { self.first } diff --git a/sctk/src/event_loop/mod.rs b/sctk/src/event_loop/mod.rs index 46f33cd702..8e2acd1dd3 100644 --- a/sctk/src/event_loop/mod.rs +++ b/sctk/src/event_loop/mod.rs @@ -244,11 +244,11 @@ where surface: &WlSurface, app_id: Option, surface_title: Option, - role: iced_accessibility::accesskit::Role, + _role: iced_accessibility::accesskit::Role, ) -> adapter::IcedSctkAdapter { use iced_accessibility::{ accesskit::{ - Node, NodeBuilder, NodeClassSet, NodeId, Role, Tree, TreeUpdate, + NodeBuilder, NodeClassSet, NodeId, Role, Tree, TreeUpdate, }, accesskit_unix::Adapter, window_node_id, diff --git a/src/wayland/mod.rs b/src/wayland/mod.rs index 3b0f50404b..4ca48d0ac4 100644 --- a/src/wayland/mod.rs +++ b/src/wayland/mod.rs @@ -1,5 +1,5 @@ use crate::runtime::window::Id; -use crate::{Command, Element, Executor, Settings, Subscription}; +use crate::{Command, Element, Executor, Settings as Settings_, Subscription}; /// wayland sandbox pub mod sandbox; @@ -111,7 +111,7 @@ pub trait Application: Sized { /// [`Error`] during startup. /// /// [`Error`]: crate::Error - fn run(settings: Settings) -> crate::Result + fn run(settings: Settings_) -> crate::Result where Self: 'static, { diff --git a/widget/src/lib.rs b/widget/src/lib.rs index d89a01058c..4901b103d6 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -138,8 +138,6 @@ pub use qr_code::QRCode; pub use renderer::Renderer; pub use style::theme::{self, Theme}; #[cfg(feature = "wayland")] -#[doc(no_inline)] pub mod dnd_listener; #[cfg(feature = "wayland")] -#[doc(no_inline)] pub mod dnd_source; From 024cb16b9aaa9fc3c8fef357ecb6ddba4876ea20 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 30 Nov 2023 15:15:08 -0500 Subject: [PATCH 42/93] chore: remove artifacts job --- .github/workflows/audit.yml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 5716979605..14cd20d1cf 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -16,15 +16,3 @@ jobs: run: cargo update - name: Audit vulnerabilities run: cargo audit - - artifacts: - runs-on: ubuntu-latest - steps: - - uses: hecrj/setup-rust-action@v1 - - name: Install cargo-outdated - run: cargo install cargo-outdated - - uses: actions/checkout@master - - name: Delete `web-sys` dependency from `integration` example - run: sed -i '$d' examples/integration/Cargo.toml - - name: Find outdated dependencies - run: cargo outdated --workspace --exit-code 1 --ignore raw-window-handle From e4a0590a5fb61261b3a246f6bd41d73eec448675 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 30 Nov 2023 16:16:00 -0500 Subject: [PATCH 43/93] fix: ambiguous palette import --- style/src/theme.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/style/src/theme.rs b/style/src/theme.rs index 9109970e8c..179ba37252 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -1,7 +1,7 @@ //! Use the built-in theme and styles. pub mod palette; -pub use palette::Palette; +pub use self::palette::Palette; use crate::application; use crate::button; From f64f6a01471b4c0c184773b7525e0a9cbc1fb29b Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 1 Dec 2023 10:52:15 -0500 Subject: [PATCH 44/93] fix: dnd widget layout --- widget/src/dnd_listener.rs | 6 +++++- widget/src/dnd_source.rs | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/widget/src/dnd_listener.rs b/widget/src/dnd_listener.rs index 1529af6a37..5a0894fac6 100644 --- a/widget/src/dnd_listener.rs +++ b/widget/src/dnd_listener.rs @@ -187,7 +187,11 @@ where u32::MAX, u32::MAX, |renderer, limits| { - self.content.as_widget().layout(tree, renderer, limits) + self.content.as_widget().layout( + &mut tree.children[0], + renderer, + limits, + ) }, ) } diff --git a/widget/src/dnd_source.rs b/widget/src/dnd_source.rs index bf306c64f1..f25f97a8ec 100644 --- a/widget/src/dnd_source.rs +++ b/widget/src/dnd_source.rs @@ -166,7 +166,11 @@ where u32::MAX, u32::MAX, |renderer, limits| { - self.content.as_widget().layout(tree, renderer, limits) + self.content.as_widget().layout( + &mut tree.children[0], + renderer, + limits, + ) }, ) } From 3d80378ebeac64665d99144dc5fd74026d1aa24a Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 4 Dec 2023 11:22:56 -0500 Subject: [PATCH 45/93] chore: use advanced text shaping for pick list --- widget/src/pick_list.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index ec8cf37cda..af2c3bebac 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -81,7 +81,7 @@ where padding: Self::DEFAULT_PADDING, text_size: None, text_line_height: text::LineHeight::default(), - text_shaping: text::Shaping::Basic, + text_shaping: text::Shaping::Advanced, font: None, handle: Handle::default(), style: Default::default(), From 941a5a93ea9d4039bd7329d12d0d9b9ebedf0ff6 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 4 Dec 2023 12:03:46 -0500 Subject: [PATCH 46/93] fix: clip mask checks --- core/src/rectangle.rs | 20 ++++++++++++++++++++ src/lib.rs | 7 ++++--- tiny_skia/src/backend.rs | 40 ++++++++++++++++++++++++---------------- 3 files changed, 48 insertions(+), 19 deletions(-) diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index c1c2eeac3c..6a66692680 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -79,6 +79,16 @@ impl Rectangle { && point.y < self.y + self.height } + /// Returns true if the given [`Point`] is contained in the [`Rectangle`]. + /// The [`Point`] must be strictly contained, i.e. it must not be on the + /// border. + pub fn contains_strict(&self, point: Point) -> bool { + self.x < point.x + && point.x < self.x + self.width + && self.y < point.y + && point.y < self.y + self.height + } + /// Returns true if the current [`Rectangle`] is completely within the given /// `container`. pub fn is_within(&self, container: &Rectangle) -> bool { @@ -88,6 +98,16 @@ impl Rectangle { ) } + /// Returns true if the current [`Rectangle`] is completely within the given + /// `container`. The [`Rectangle`] must be strictly contained, i.e. it must + /// not be on the border. + pub fn is_within_strict(&self, container: &Rectangle) -> bool { + container.contains_strict(self.position()) + && container.contains_strict( + self.position() + Vector::new(self.width, self.height), + ) + } + /// Computes the intersection with the given [`Rectangle`]. pub fn intersection( &self, diff --git a/src/lib.rs b/src/lib.rs index 7e7c03fab7..1b5bfe7b43 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -151,12 +151,13 @@ #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] -#![forbid(rust_2018_idioms, unsafe_code)] +#![forbid(unsafe_code)] #![deny( missing_debug_implementations, missing_docs, - unused_results, - rustdoc::broken_intra_doc_links + rust_2018_idioms, + rustdoc::broken_intra_doc_links, + unused_results )] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))] diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index a3581dd677..57f7e7ee24 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -161,8 +161,9 @@ impl Backend { return; } - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) - .then_some(clip_mask as &_); + let clip_mask = (!physical_bounds + .is_within_strict(&clip_bounds)) + .then_some(clip_mask as &_); let transform = tiny_skia::Transform::from_translate( translation.x, @@ -456,8 +457,9 @@ impl Backend { return; } - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) - .then_some(clip_mask as &_); + let clip_mask = (!physical_bounds + .is_within_strict(&clip_bounds)) + .then_some(clip_mask as &_); self.text_pipeline.draw_paragraph( paragraph, @@ -481,8 +483,9 @@ impl Backend { return; } - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) - .then_some(clip_mask as &_); + let clip_mask = (!physical_bounds + .is_within_strict(&clip_bounds)) + .then_some(clip_mask as &_); self.text_pipeline.draw_editor( editor, @@ -512,8 +515,9 @@ impl Backend { return; } - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) - .then_some(clip_mask as &_); + let clip_mask = (!physical_bounds + .is_within_strict(&clip_bounds)) + .then_some(clip_mask as &_); self.text_pipeline.draw_cached( content, @@ -572,8 +576,9 @@ impl Backend { return; } - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) - .then_some(clip_mask as &_); + let clip_mask = (!physical_bounds + .is_within_strict(&clip_bounds)) + .then_some(clip_mask as &_); let transform = tiny_skia::Transform::from_translate( translation.x, @@ -609,8 +614,9 @@ impl Backend { return; } - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) - .then_some(clip_mask as &_); + let clip_mask = (!physical_bounds + .is_within_strict(&clip_bounds)) + .then_some(clip_mask as &_); self.vector_pipeline.draw( handle, @@ -645,8 +651,9 @@ impl Backend { return; } - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) - .then_some(clip_mask as &_); + let clip_mask = (!physical_bounds + .is_within_strict(&clip_bounds)) + .then_some(clip_mask as &_); pixels.fill_path( path, @@ -679,8 +686,9 @@ impl Backend { return; } - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) - .then_some(clip_mask as &_); + let clip_mask = (!physical_bounds + .is_within_strict(&clip_bounds)) + .then_some(clip_mask as &_); pixels.stroke_path( path, From 50646bae625dc7a88f9c822f60e6eb698ff2aee5 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 4 Dec 2023 13:00:07 -0500 Subject: [PATCH 47/93] fix: set web-sys to =0.3.64 --- Cargo.toml | 2 +- examples/integration/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f7098325b1..f533c6177a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -169,7 +169,7 @@ unicode-segmentation = "1.0" wasm-bindgen-futures = "0.4" wasm-timer = "0.2" wayland-protocols = { version = "0.31.0", features = [ "staging"]} -web-sys = "0.3" +web-sys = "0.3.64" web-time = "0.2" wgpu = "0.19" winapi = "0.3" diff --git a/examples/integration/Cargo.toml b/examples/integration/Cargo.toml index a4a961f885..77aae20aa9 100644 --- a/examples/integration/Cargo.toml +++ b/examples/integration/Cargo.toml @@ -20,4 +20,4 @@ iced_wgpu.features = ["webgl"] console_error_panic_hook = "0.1" console_log = "1.0" wasm-bindgen = "0.2" -web-sys = { version = "0.3", features = ["Element", "HtmlCanvasElement", "Window", "Document"] } +web-sys = { version = "=0.3.64", features = ["Element", "HtmlCanvasElement", "Window", "Document"] } From 73238f1fd6debd3b954edc358bbd661f1d5130bd Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 4 Dec 2023 13:32:34 -0500 Subject: [PATCH 48/93] fix: translate the wayland event position for content inside a scrollable --- core/src/event/wayland/mod.rs | 17 +++++++++++++++++ widget/src/scrollable.rs | 10 +++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/core/src/event/wayland/mod.rs b/core/src/event/wayland/mod.rs index bf3ddcb1b0..f7bfd5bac7 100644 --- a/core/src/event/wayland/mod.rs +++ b/core/src/event/wayland/mod.rs @@ -43,3 +43,20 @@ pub enum Event { /// Frame events Frame(Instant, WlSurface, Id), } + +impl Event { + /// Translate the event by some vector + pub fn translate(&mut self, vector: crate::vector::Vector) { + match self { + Event::DndOffer(DndOfferEvent::Enter { x, y, .. }) => { + *x += vector.x as f64; + *y += vector.y as f64; + } + Event::DndOffer(DndOfferEvent::Motion { x, y }) => { + *x += vector.x as f64; + *y += vector.y as f64; + } + _ => {} + } + } +} diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 138fdb7aa8..5916e40c75 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -657,7 +657,7 @@ pub fn layout( /// accordingly. pub fn update( state: &mut State, - event: Event, + #[allow(unused_mut)] mut event: Event, layout: Layout<'_>, cursor: mouse::Cursor, clipboard: &mut dyn Clipboard, @@ -699,6 +699,14 @@ pub fn update( let translation = state.translation(direction, bounds, content_bounds); + #[cfg(feature = "wayland")] + if let Event::PlatformSpecific( + iced_runtime::core::event::PlatformSpecific::Wayland(e), + ) = &mut event + { + e.translate(translation); + } + update_content( event.clone(), content, From 0d673be8d3a05b11b080302f0e02ea74ca0938ce Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 6 Dec 2023 18:36:11 -0500 Subject: [PATCH 49/93] fix(example): sctk_drag id --- examples/sctk_drag/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/sctk_drag/src/main.rs b/examples/sctk_drag/src/main.rs index c5dcf2ad41..59c97187e5 100644 --- a/examples/sctk_drag/src/main.rs +++ b/examples/sctk_drag/src/main.rs @@ -160,7 +160,7 @@ impl Application for DndTest { .map(|t| t.to_string()) .collect(), DndAction::Move, - window::Id::unique(), + window::Id::MAIN, Some(DndIcon::Custom(iced::window::Id::unique())), Box::new(MyDndString( self.current_text.chars().rev().collect::(), From 9c962a38028ce7a26ad19b5c42a5b6b16f0a6091 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 7 Dec 2023 18:44:18 -0500 Subject: [PATCH 50/93] cleanup: formatting and clippy --- Cargo.toml | 2 +- examples/integration/Cargo.toml | 2 +- examples/integration/src/main.rs | 2 +- examples/sctk_todos/src/main.rs | 4 ---- runtime/src/program/state.rs | 2 +- src/application.rs | 1 - src/wayland/mod.rs | 4 ++-- src/wayland/sandbox.rs | 4 ---- winit/src/application.rs | 10 ++-------- winit/src/conversion.rs | 3 --- winit/src/multi_window.rs | 8 ++------ 11 files changed, 10 insertions(+), 32 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f533c6177a..f7098325b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -169,7 +169,7 @@ unicode-segmentation = "1.0" wasm-bindgen-futures = "0.4" wasm-timer = "0.2" wayland-protocols = { version = "0.31.0", features = [ "staging"]} -web-sys = "0.3.64" +web-sys = "0.3" web-time = "0.2" wgpu = "0.19" winapi = "0.3" diff --git a/examples/integration/Cargo.toml b/examples/integration/Cargo.toml index 77aae20aa9..2fe826db45 100644 --- a/examples/integration/Cargo.toml +++ b/examples/integration/Cargo.toml @@ -20,4 +20,4 @@ iced_wgpu.features = ["webgl"] console_error_panic_hook = "0.1" console_log = "1.0" wasm-bindgen = "0.2" -web-sys = { version = "=0.3.64", features = ["Element", "HtmlCanvasElement", "Window", "Document"] } +web-sys = { version = "=0.3", features = ["Element", "HtmlCanvasElement", "Window", "Document"] } diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index ad6717b19c..5a3fd25672 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -160,7 +160,7 @@ pub fn main() -> Result<(), Box> { ); let mut state = program::State::new( - Id(0), + Id::MAIN, controls, viewport.logical_size(), &mut renderer, diff --git a/examples/sctk_todos/src/main.rs b/examples/sctk_todos/src/main.rs index 356e383806..f6d95134e6 100644 --- a/examples/sctk_todos/src/main.rs +++ b/examples/sctk_todos/src/main.rs @@ -1,11 +1,8 @@ use env_logger::Env; use iced::alignment::{self, Alignment}; use iced::event::{self, listen_raw, Event}; -use iced::subscription; use iced::theme::{self, Theme}; -use iced::wayland::actions::data_device::ActionInner; use iced::wayland::actions::window::SctkWindowSettings; -use iced::wayland::data_device::action as data_device_action; use iced::wayland::InitialSurface; use iced::widget::{ self, button, checkbox, column, container, row, scrollable, text, @@ -21,7 +18,6 @@ use iced_core::{id, keyboard}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; use std::fmt::Debug; -use std::sync::Arc; static INPUT_ID: Lazy = Lazy::new(Id::unique); diff --git a/runtime/src/program/state.rs b/runtime/src/program/state.rs index f084370f47..9df4f4cea3 100644 --- a/runtime/src/program/state.rs +++ b/runtime/src/program/state.rs @@ -275,7 +275,7 @@ where } fn build_user_interface<'a, P: Program>( - id: crate::window::Id, + _id: crate::window::Id, program: &'a mut P, cache: user_interface::Cache, renderer: &mut P::Renderer, diff --git a/src/application.rs b/src/application.rs index ccde2efba2..e3988e793a 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,5 +1,4 @@ //! Build interactive cross-platform applications. -use iced_core::window::Id; use crate::{Command, Element, Executor, Settings, Subscription}; diff --git a/src/wayland/mod.rs b/src/wayland/mod.rs index 4ca48d0ac4..5926699080 100644 --- a/src/wayland/mod.rs +++ b/src/wayland/mod.rs @@ -60,7 +60,7 @@ pub trait Application: Sized { /// Returns the current [`Theme`] of the [`Application`]. /// /// [`Theme`]: Self::Theme - fn theme(&self, id: Id) -> Self::Theme { + fn theme(&self, _id: Id) -> Self::Theme { Self::Theme::default() } @@ -98,7 +98,7 @@ pub trait Application: Sized { /// while a scale factor of `0.5` will shrink them to half their size. /// /// By default, it returns `1.0`. - fn scale_factor(&self, id: Id) -> f64 { + fn scale_factor(&self, _id: Id) -> f64 { 1.0 } diff --git a/src/wayland/sandbox.rs b/src/wayland/sandbox.rs index e66e4418de..74c6f85879 100644 --- a/src/wayland/sandbox.rs +++ b/src/wayland/sandbox.rs @@ -82,10 +82,6 @@ use crate::{ /// fn view(&self, _: Id) -> Element { /// "Hello, world!".into() /// } -/// -/// fn close_requested(&self, _: Id) -> Self::Message { -/// unimplemented!() -/// } /// } /// ``` pub trait Sandbox { diff --git a/winit/src/application.rs b/winit/src/application.rs index 37510bf055..36a39d5cc6 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -1,7 +1,5 @@ //! Create interactive, native cross-platform applications. mod drag_resize; -#[cfg(feature = "trace")] -mod profiler; mod state; #[cfg(feature = "a11y")] @@ -905,14 +903,10 @@ pub fn run_command( for action in command.actions() { match action { command::Action::Future(future) => { - runtime.spawn(Box::pin( - future.map(|e| UserEventWrapper::Message(e)), - )); + runtime.spawn(Box::pin(future.map(UserEventWrapper::Message))); } command::Action::Stream(stream) => { - runtime.run(Box::pin( - stream.map(|e| UserEventWrapper::Message(e)), - )); + runtime.run(Box::pin(stream.map(UserEventWrapper::Message))); } command::Action::Clipboard(action) => match action { clipboard::Action::Read(tag) => { diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index 5b4652b359..489c9e8902 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -152,9 +152,6 @@ pub fn window_event( WindowEvent::CloseRequested => { Some(Event::Window(id, window::Event::CloseRequested)) } - WindowEvent::CloseRequested => { - Some(Event::Window(id, window::Event::CloseRequested)) - } WindowEvent::CursorMoved { position, .. } => { let position = position.to_logical::(scale_factor); diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index b7b7b961f3..88992bc137 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -1108,14 +1108,10 @@ fn run_command( for action in command.actions() { match action { command::Action::Future(future) => { - runtime.spawn(Box::pin( - future.map(|e| UserEventWrapper::Message(e)), - )); + runtime.spawn(Box::pin(future.map(UserEventWrapper::Message))); } command::Action::Stream(stream) => { - runtime.run(Box::pin( - stream.map(|e| UserEventWrapper::Message(e)), - )); + runtime.run(Box::pin(stream.map(UserEventWrapper::Message))); } command::Action::Clipboard(action) => match action { clipboard::Action::Read(tag) => { From 37978aa019cc50a417eba2600cd90a921ef77105 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 11 Dec 2023 12:21:17 -0500 Subject: [PATCH 51/93] chore: fix sctk multi-window dependency --- Cargo.toml | 2 +- sctk/Cargo.toml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f7098325b1..90911d1418 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,7 +56,7 @@ a11y = ["iced_accessibility", "iced_core/a11y", "iced_widget/a11y", "iced_winit? # Enables the winit shell. Conflicts with `wayland` and `glutin`. winit = ["iced_winit", "iced_accessibility?/accesskit_winit"] # Enables the sctk shell. COnflicts with `winit` and `glutin`. -wayland = ["iced_sctk", "iced_widget/wayland", "iced_accessibility?/accesskit_unix", "iced_core/wayland", "multi-window"] +wayland = ["iced_sctk", "iced_widget/wayland", "iced_accessibility?/accesskit_unix", "iced_core/wayland"] [dependencies] iced_core.workspace = true diff --git a/sctk/Cargo.toml b/sctk/Cargo.toml index 9592eb4566..733865d47a 100644 --- a/sctk/Cargo.toml +++ b/sctk/Cargo.toml @@ -9,7 +9,6 @@ edition = "2021" debug = ["iced_runtime/debug"] system = ["sysinfo"] application = [] -multi_window = [] a11y = ["iced_accessibility", "iced_runtime/a11y"] [dependencies] From 200cc090b39dcdb22024cc4aecad5762195e520a Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Mon, 11 Dec 2023 14:38:24 -0800 Subject: [PATCH 52/93] dnd_listener: Fix behavior when there are multiple listeners (#87) A `dnd_listener` widget shouldn't handle a DnD event when the dnd drag isn't within the widget's bounds. So add a few more checks for this. Enter/leave events generated by `DndOfferEvent::Motion` also don't behave as one might expect, since the enter may occur before the leave depending on the order it calls `on_event` on the widget. Not sure how to address that, but cosmic-workspaces can just ignore the leave events for now. Otherwise, this seems to be working fine, after these changes. --- widget/src/dnd_listener.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/widget/src/dnd_listener.rs b/widget/src/dnd_listener.rs index 5a0894fac6..db33e9b055 100644 --- a/widget/src/dnd_listener.rs +++ b/widget/src/dnd_listener.rs @@ -416,6 +416,10 @@ fn update( Event::PlatformSpecific(PlatformSpecific::Wayland( event::wayland::Event::DndOffer(DndOfferEvent::Leave), )) => { + if matches!(state.dnd, DndState::None | DndState::External(..)) { + return event::Status::Ignored; + } + if !matches!(state.dnd, DndState::Dropped) { state.dnd = DndState::None; } @@ -430,10 +434,10 @@ fn update( )) => { if matches!(state.dnd, DndState::Hovered(..)) { state.dnd = DndState::Dropped; - } - if let Some(message) = widget.on_drop.clone() { - shell.publish(message); - return event::Status::Captured; + if let Some(message) = widget.on_drop.clone() { + shell.publish(message); + return event::Status::Captured; + } } } Event::PlatformSpecific(PlatformSpecific::Wayland( @@ -481,6 +485,10 @@ fn update( action, )), )) => { + if matches!(state.dnd, DndState::None | DndState::External(..)) { + return event::Status::Ignored; + } + if let Some(message) = widget.on_selected_action.as_ref() { shell.publish(message(*action)); return event::Status::Captured; From 2611d7b782d9896aee7b22ddf7cadd970d896682 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 13 Dec 2023 14:40:29 -0500 Subject: [PATCH 53/93] fix: broadcast surface events --- sctk/src/application.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sctk/src/application.rs b/sctk/src/application.rs index 34e2d9764b..d3660a2e5b 100644 --- a/sctk/src/application.rs +++ b/sctk/src/application.rs @@ -937,6 +937,9 @@ where | SctkEvent::SessionLocked | SctkEvent::SessionLockFinished | SctkEvent::SessionUnlocked + | SctkEvent::PopupEvent { .. } + | SctkEvent::LayerSurfaceEvent { .. } + | SctkEvent::WindowEvent { .. } ); if remove { let event = sctk_events.remove(i); From c155a267743451bab6be8fbef2bec08984f0726a Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Tue, 19 Dec 2023 14:13:08 -0700 Subject: [PATCH 54/93] Fixes for last commit --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 90911d1418..85a3852d2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -152,7 +152,7 @@ ouroboros = "0.17" palette = "0.7" qrcode = { version = "0.12", default-features = false } raw-window-handle = "0.6" -resvg = "0.36" +resvg = "0.38" rustc-hash = "1.0" sctk = { package = "smithay-client-toolkit", git = "https://github.com/smithay/client-toolkit", rev = "2e9bf9f" } smol = "1.0" From 1a75a1b47b7de09eb0b1937cc9b76c027e412be9 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Tue, 19 Dec 2023 15:00:32 -0700 Subject: [PATCH 55/93] Add function to fill a Raw --- core/src/renderer/null.rs | 3 +++ core/src/text.rs | 6 ++++++ graphics/src/renderer.rs | 5 +++++ renderer/src/lib.rs | 8 ++++++-- 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index 55eccf6f76..2582863fbb 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -42,6 +42,7 @@ impl text::Renderer for Null { type Font = Font; type Paragraph = (); type Editor = (); + type Raw = (); const ICON_FONT: Font = Font::DEFAULT; const CHECKMARK_ICON: char = '0'; @@ -75,6 +76,8 @@ impl text::Renderer for Null { ) { } + fn fill_raw(&mut self, _raw: Self::Raw) {} + fn fill_text( &mut self, _paragraph: Text<'_, Self::Font>, diff --git a/core/src/text.rs b/core/src/text.rs index 3ff76be479..8d6ba6f916 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -173,6 +173,9 @@ pub trait Renderer: crate::Renderer { /// The [`Editor`] of this [`Renderer`]. type Editor: Editor + 'static; + /// The [`Raw`] of this [`Renderer`]. + type Raw; + /// The icon font of the backend. const ICON_FONT: Self::Font; @@ -215,6 +218,9 @@ pub trait Renderer: crate::Renderer { clip_bounds: Rectangle, ); + /// Draws the given [`Raw`] + fn fill_raw(&mut self, raw: Self::Raw); + /// Draws the given [`Text`] at the given position and with the given /// [`Color`]. fn fill_text( diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index f500261dec..147120133f 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -136,6 +136,7 @@ where type Font = Font; type Paragraph = text::Paragraph; type Editor = text::Editor; + type Raw = text::Raw; const ICON_FONT: Font = Font::with_name("Iced-Icons"); const CHECKMARK_ICON: char = '\u{f00c}'; @@ -183,6 +184,10 @@ where }); } + fn fill_raw(&mut self, raw: Self::Raw) { + self.primitives.push(Primitive::RawText(raw)); + } + fn fill_text( &mut self, text: Text<'_, Self::Font>, diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index 7f8bb6a4d9..b94b88494c 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -23,8 +23,7 @@ pub use geometry::Geometry; use crate::core::renderer; use crate::core::text::{self, Text}; use crate::core::{Background, Color, Font, Pixels, Point, Rectangle, Vector}; -use crate::graphics::text::Editor; -use crate::graphics::text::Paragraph; +use crate::graphics::text::{Editor, Paragraph, Raw}; use crate::graphics::Mesh; use std::borrow::Cow; @@ -150,6 +149,7 @@ impl text::Renderer for Renderer { type Font = Font; type Paragraph = Paragraph; type Editor = Editor; + type Raw = Raw; const ICON_FONT: Font = iced_tiny_skia::Renderer::ICON_FONT; const CHECKMARK_ICON: char = iced_tiny_skia::Renderer::CHECKMARK_ICON; @@ -195,6 +195,10 @@ impl text::Renderer for Renderer { ); } + fn fill_raw(&mut self, raw: Self::Raw) { + delegate!(self, renderer, renderer.fill_raw(raw)); + } + fn fill_text( &mut self, text: Text<'_, Self::Font>, From d3d89a8708f06291ea31b052dc2994c45c40fd1a Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 12 Jan 2024 10:17:14 -0700 Subject: [PATCH 56/93] Fix docs error --- core/src/text.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/text.rs b/core/src/text.rs index 8d6ba6f916..1598ef67b1 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -173,7 +173,7 @@ pub trait Renderer: crate::Renderer { /// The [`Editor`] of this [`Renderer`]. type Editor: Editor + 'static; - /// The [`Raw`] of this [`Renderer`]. + /// The Raw of this [`Renderer`]. type Raw; /// The icon font of the backend. @@ -218,7 +218,7 @@ pub trait Renderer: crate::Renderer { clip_bounds: Rectangle, ); - /// Draws the given [`Raw`] + /// Draws the given Raw fn fill_raw(&mut self, raw: Self::Raw); /// Draws the given [`Text`] at the given position and with the given From 590b8edd94c7009f1108f85eb0bfb19e7b9b2d22 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Tue, 19 Dec 2023 14:02:28 -0700 Subject: [PATCH 57/93] Update to cosmic-text refactor --- Cargo.toml | 6 +- graphics/src/geometry/text.rs | 1 + graphics/src/text/editor.rs | 541 ++++++++++++++++++---------------- tiny_skia/src/text.rs | 26 +- 4 files changed, 313 insertions(+), 261 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 85a3852d2a..9e7d76ce21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,6 +94,8 @@ members = [ "accessibility", "sctk" ] +exclude = ["examples/integration"] + [profile.release-opt] inherits = "release" @@ -134,10 +136,10 @@ iced_accessibility = { version = "0.1", path = "accessibility" } async-std = "1.0" bitflags = "1.0" bytemuck = { version = "1.0", features = ["derive"] } -cosmic-text = "0.10" +cosmic-text = { git = "https://github.com/pop-os/cosmic-text.git", rev = "e0ae465" } futures = "0.3" glam = "0.24" -glyphon = "0.5" +glyphon = { git = "https://github.com/wash2/glyphon.git", rev = "3598375"} guillotiere = "0.6" half = "2.2" image = "0.24" diff --git a/graphics/src/geometry/text.rs b/graphics/src/geometry/text.rs index 7a2cd7b45e..6c02b909b7 100644 --- a/graphics/src/geometry/text.rs +++ b/graphics/src/geometry/text.rs @@ -52,6 +52,7 @@ impl Text { self.size.0, f32::MAX, cosmic_text::Wrap::None, + None, ); let translation_x = match self.horizontal_alignment { diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index d5262ae820..e1be3ff6b4 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -7,7 +7,7 @@ use crate::core::text::LineHeight; use crate::core::{Font, Pixels, Point, Rectangle, Size}; use crate::text; -use cosmic_text::Edit as _; +use cosmic_text::{BufferRef, Edit as _}; use std::fmt; use std::sync::{self, Arc}; @@ -17,7 +17,7 @@ use std::sync::{self, Arc}; pub struct Editor(Option>); struct Internal { - editor: cosmic_text::Editor, + editor: cosmic_text::Editor<'static>, font: Font, bounds: Size, topmost_line_changed: Option, @@ -30,9 +30,21 @@ impl Editor { Self::default() } - /// Returns the buffer of the [`Editor`]. + /// Runs a closure with the buffer of the [`Editor`]. + pub fn with_buffer T, T>( + &self, + f: F, + ) -> T { + self.internal().editor.with_buffer(f) + } + + /// Returns the buffer of the [`Paragraph`]. pub fn buffer(&self) -> &cosmic_text::Buffer { - self.internal().editor.buffer() + match self.internal().editor.buffer_ref() { + cosmic_text::BufferRef::Owned(buffer) => buffer, + cosmic_text::BufferRef::Borrowed(buffer) => buffer, + cosmic_text::BufferRef::Arc(buffer) => buffer, + } } /// Creates a [`Weak`] reference to the [`Editor`]. @@ -83,14 +95,16 @@ impl editor::Editor for Editor { } fn line(&self, index: usize) -> Option<&str> { - self.buffer() - .lines - .get(index) - .map(cosmic_text::BufferLine::text) + let buffer = match self.internal().editor.buffer_ref() { + cosmic_text::BufferRef::Owned(buffer) => buffer, + cosmic_text::BufferRef::Borrowed(buffer) => buffer, + cosmic_text::BufferRef::Arc(buffer) => buffer, + }; + buffer.lines.get(index).map(cosmic_text::BufferLine::text) } fn line_count(&self) -> usize { - self.buffer().lines.len() + self.with_buffer(|buffer| buffer.lines.len()) } fn selection(&self) -> Option { @@ -101,133 +115,132 @@ impl editor::Editor for Editor { let internal = self.internal(); let cursor = internal.editor.cursor(); - let buffer = internal.editor.buffer(); + internal.editor.with_buffer(|buffer| { + match internal.editor.selection_bounds() { + Some((start, end)) => { + let line_height = buffer.metrics().line_height; + let selected_lines = end.line - start.line + 1; + + let visual_lines_offset = + visual_lines_offset(start.line, buffer); + + let regions = buffer + .lines + .iter() + .skip(start.line) + .take(selected_lines) + .enumerate() + .flat_map(|(i, line)| { + highlight_line( + line, + if i == 0 { start.index } else { 0 }, + if i == selected_lines - 1 { + end.index + } else { + line.text().len() + }, + ) + }) + .enumerate() + .filter_map(|(visual_line, (x, width))| { + if width > 0.0 { + Some(Rectangle { + x, + width, + y: (visual_line as i32 + + visual_lines_offset) + as f32 + * line_height, + height: line_height, + }) + } else { + None + } + }) + .collect(); - match internal.editor.select_opt() { - Some(selection) => { - let (start, end) = if cursor < selection { - (cursor, selection) - } else { - (selection, cursor) - }; + Cursor::Selection(regions) + } + _ => { + let line_height = buffer.metrics().line_height; - let line_height = buffer.metrics().line_height; - let selected_lines = end.line - start.line + 1; - - let visual_lines_offset = - visual_lines_offset(start.line, buffer); - - let regions = buffer - .lines - .iter() - .skip(start.line) - .take(selected_lines) - .enumerate() - .flat_map(|(i, line)| { - highlight_line( - line, - if i == 0 { start.index } else { 0 }, - if i == selected_lines - 1 { - end.index - } else { - line.text().len() - }, - ) - }) - .enumerate() - .filter_map(|(visual_line, (x, width))| { - if width > 0.0 { - Some(Rectangle { - x, - width, - y: (visual_line as i32 + visual_lines_offset) - as f32 - * line_height, - height: line_height, - }) - } else { - None - } - }) - .collect(); + let visual_lines_offset = + visual_lines_offset(cursor.line, buffer); - Cursor::Selection(regions) - } - _ => { - let line_height = buffer.metrics().line_height; - - let visual_lines_offset = - visual_lines_offset(cursor.line, buffer); - - let line = buffer - .lines - .get(cursor.line) - .expect("Cursor line should be present"); - - let layout = line - .layout_opt() - .as_ref() - .expect("Line layout should be cached"); - - let mut lines = layout.iter().enumerate(); - - let (visual_line, offset) = lines - .find_map(|(i, line)| { - let start = line - .glyphs - .first() - .map(|glyph| glyph.start) - .unwrap_or(0); - let end = line - .glyphs - .last() - .map(|glyph| glyph.end) - .unwrap_or(0); - - let is_cursor_before_start = start > cursor.index; - - let is_cursor_before_end = match cursor.affinity { - cosmic_text::Affinity::Before => { - cursor.index <= end - } - cosmic_text::Affinity::After => cursor.index < end, - }; - - if is_cursor_before_start { - // Sometimes, the glyph we are looking for is right - // between lines. This can happen when a line wraps - // on a space. - // In that case, we can assume the cursor is at the - // end of the previous line. - // i is guaranteed to be > 0 because `start` is always - // 0 for the first line, so there is no way for the - // cursor to be before it. - Some((i - 1, layout[i - 1].w)) - } else if is_cursor_before_end { - let offset = line + let line = buffer + .lines + .get(cursor.line) + .expect("Cursor line should be present"); + + let layout = line + .layout_opt() + .as_ref() + .expect("Line layout should be cached"); + + let mut lines = layout.iter().enumerate(); + + let (visual_line, offset) = lines + .find_map(|(i, line)| { + let start = line .glyphs - .iter() - .take_while(|glyph| cursor.index > glyph.start) - .map(|glyph| glyph.w) - .sum(); - - Some((i, offset)) - } else { - None - } - }) - .unwrap_or(( - layout.len().saturating_sub(1), - layout.last().map(|line| line.w).unwrap_or(0.0), - )); + .first() + .map(|glyph| glyph.start) + .unwrap_or(0); + let end = line + .glyphs + .last() + .map(|glyph| glyph.end) + .unwrap_or(0); + + let is_cursor_before_start = start > cursor.index; + + let is_cursor_before_end = match cursor.affinity { + cosmic_text::Affinity::Before => { + cursor.index <= end + } + cosmic_text::Affinity::After => { + cursor.index < end + } + }; + + if is_cursor_before_start { + // Sometimes, the glyph we are looking for is right + // between lines. This can happen when a line wraps + // on a space. + // In that case, we can assume the cursor is at the + // end of the previous line. + // i is guaranteed to be > 0 because `start` is always + // 0 for the first line, so there is no way for the + // cursor to be before it. + Some((i - 1, layout[i - 1].w)) + } else if is_cursor_before_end { + let offset = line + .glyphs + .iter() + .take_while(|glyph| { + cursor.index > glyph.start + }) + .map(|glyph| glyph.w) + .sum(); - Cursor::Caret(Point::new( - offset, - (visual_lines_offset + visual_line as i32) as f32 - * line_height, - )) + Some((i, offset)) + } else { + None + } + }) + .unwrap_or(( + layout.len().saturating_sub(1), + layout.last().map(|line| line.w).unwrap_or(0.0), + )); + + Cursor::Caret(Point::new( + offset, + (visual_lines_offset + visual_line as i32) as f32 + * line_height, + )) + } } - } + }) } fn cursor_position(&self) -> (usize, usize) { @@ -252,16 +265,8 @@ impl editor::Editor for Editor { match action { // Motion events Action::Move(motion) => { - if let Some(selection) = editor.select_opt() { - let cursor = editor.cursor(); - - let (left, right) = if cursor < selection { - (cursor, selection) - } else { - (selection, cursor) - }; - - editor.set_select_opt(None); + if let Some((left, right)) = editor.selection_bounds() { + editor.set_selection(cosmic_text::Selection::None); match motion { // These motions are performed as-is even when a selection @@ -290,20 +295,23 @@ impl editor::Editor for Editor { Action::Select(motion) => { let cursor = editor.cursor(); - if editor.select_opt().is_none() { - editor.set_select_opt(Some(cursor)); + if editor.selection() == cosmic_text::Selection::None { + editor + .set_selection(cosmic_text::Selection::Normal(cursor)); } editor.action(font_system.raw(), motion_to_action(motion)); // Deselect if selection matches cursor position - if let Some(selection) = editor.select_opt() { + if let cosmic_text::Selection::Normal(selection) = + editor.selection() + { let cursor = editor.cursor(); if cursor.line == selection.line && cursor.index == selection.index { - editor.set_select_opt(None); + editor.set_selection(cosmic_text::Selection::None); } } } @@ -311,10 +319,12 @@ impl editor::Editor for Editor { use unicode_segmentation::UnicodeSegmentation; let cursor = editor.cursor(); - - if let Some(line) = editor.buffer().lines.get(cursor.line) { - let (start, end) = - UnicodeSegmentation::unicode_word_indices(line.text()) + let start_end_opt = editor.with_buffer(|buffer| { + if let Some(line) = buffer.lines.get(cursor.line) { + let (start, end) = + UnicodeSegmentation::unicode_word_indices( + line.text(), + ) // Split words with dots .flat_map(|(i, word)| { word.split('.').scan(i, |current, word| { @@ -354,35 +364,43 @@ impl editor::Editor for Editor { (start, end) }); + Some((start, end)) + } else { + None + } + }); + + if let Some((start, end)) = start_end_opt { if start != end { editor.set_cursor(cosmic_text::Cursor { index: start, ..cursor }); - editor.set_select_opt(Some(cosmic_text::Cursor { - index: end, - ..cursor - })); + editor.set_selection(cosmic_text::Selection::Normal( + cosmic_text::Cursor { + index: end, + ..cursor + }, + )); } } } Action::SelectLine => { let cursor = editor.cursor(); - if let Some(line_length) = editor - .buffer() - .lines - .get(cursor.line) - .map(|line| line.text().len()) - { + if let Some(line_length) = editor.with_buffer(|buffer| { + buffer.lines.get(cursor.line).map(|line| line.text().len()) + }) { editor .set_cursor(cosmic_text::Cursor { index: 0, ..cursor }); - editor.set_select_opt(Some(cosmic_text::Cursor { - index: line_length, - ..cursor - })); + editor.set_selection(cosmic_text::Selection::Normal( + cosmic_text::Cursor { + index: line_length, + ..cursor + }, + )); } } @@ -419,7 +437,12 @@ impl editor::Editor for Editor { } let cursor = editor.cursor(); - let selection = editor.select_opt().unwrap_or(cursor); + let selection = match editor.selection() { + cosmic_text::Selection::Normal(selection) => selection, + cosmic_text::Selection::Line(selection) => selection, + cosmic_text::Selection::Word(selection) => selection, + cosmic_text::Selection::None => cursor, + }; internal.topmost_line_changed = Some(cursor.min(selection).line); @@ -445,13 +468,15 @@ impl editor::Editor for Editor { ); // Deselect if selection matches cursor position - if let Some(selection) = editor.select_opt() { + if let cosmic_text::Selection::Normal(selection) = + editor.selection() + { let cursor = editor.cursor(); if cursor.line == selection.line && cursor.index == selection.index { - editor.set_select_opt(None); + editor.set_selection(cosmic_text::Selection::None); } } } @@ -490,9 +515,11 @@ impl editor::Editor for Editor { if font_system.version() != internal.version { log::trace!("Updating `FontSystem` of `Editor`..."); - for line in internal.editor.buffer_mut().lines.iter_mut() { - line.reset(); - } + internal.editor.with_buffer_mut(|buffer| { + for line in buffer.lines.iter_mut() { + line.reset(); + } + }); internal.version = font_system.version(); internal.topmost_line_changed = Some(0); @@ -501,17 +528,19 @@ impl editor::Editor for Editor { if new_font != internal.font { log::trace!("Updating font of `Editor`..."); - for line in internal.editor.buffer_mut().lines.iter_mut() { - let _ = line.set_attrs_list(cosmic_text::AttrsList::new( - text::to_attributes(new_font), - )); - } + internal.editor.with_buffer_mut(|buffer| { + for line in buffer.lines.iter_mut() { + let _ = line.set_attrs_list(cosmic_text::AttrsList::new( + text::to_attributes(new_font), + )); + } + }); internal.font = new_font; internal.topmost_line_changed = Some(0); } - let metrics = internal.editor.buffer().metrics(); + let metrics = internal.editor.with_buffer(|buffer| buffer.metrics()); let new_line_height = new_line_height.to_absolute(new_size); if new_size.0 != metrics.font_size @@ -519,20 +548,24 @@ impl editor::Editor for Editor { { log::trace!("Updating `Metrics` of `Editor`..."); - internal.editor.buffer_mut().set_metrics( - font_system.raw(), - cosmic_text::Metrics::new(new_size.0, new_line_height.0), - ); + internal.editor.with_buffer_mut(|buffer| { + buffer.set_metrics( + font_system.raw(), + cosmic_text::Metrics::new(new_size.0, new_line_height.0), + ) + }); } if new_bounds != internal.bounds { log::trace!("Updating size of `Editor`..."); - internal.editor.buffer_mut().set_size( - font_system.raw(), - new_bounds.width, - new_bounds.height, - ); + internal.editor.with_buffer_mut(|buffer| { + buffer.set_size( + font_system.raw(), + new_bounds.width, + new_bounds.height, + ) + }); internal.bounds = new_bounds; } @@ -546,7 +579,10 @@ impl editor::Editor for Editor { new_highlighter.change_line(topmost_line_changed); } - internal.editor.shape_as_needed(font_system.raw()); + internal.editor.shape_as_needed( + font_system.raw(), + false, /*TODO: support trimming caches*/ + ); self.0 = Some(Arc::new(internal)); } @@ -558,29 +594,32 @@ impl editor::Editor for Editor { format_highlight: impl Fn(&H::Highlight) -> highlighter::Format, ) { let internal = self.internal(); - let buffer = internal.editor.buffer(); - - let mut window = buffer.scroll() + buffer.visible_lines(); - - let last_visible_line = buffer - .lines - .iter() - .enumerate() - .find_map(|(i, line)| { - let visible_lines = line - .layout_opt() - .as_ref() - .expect("Line layout should be cached") - .len() as i32; - - if window > visible_lines { - window -= visible_lines; - None - } else { - Some(i) - } - }) - .unwrap_or(buffer.lines.len().saturating_sub(1)); + + let last_visible_line = internal.editor.with_buffer(|buffer| { + let scroll = buffer.scroll(); + let mut window = scroll.layout + buffer.visible_lines(); + + buffer + .lines + .iter() + .enumerate() + .skip(scroll.line) + .find_map(|(i, line)| { + let visible_lines = line + .layout_opt() + .as_ref() + .expect("Line layout should be cached") + .len() as i32; + + if window > visible_lines { + window -= visible_lines; + None + } else { + Some(i) + } + }) + .unwrap_or(buffer.lines.len().saturating_sub(1)) + }); let current_line = highlighter.current_line(); @@ -599,33 +638,38 @@ impl editor::Editor for Editor { let attributes = text::to_attributes(font); - for line in &mut internal.editor.buffer_mut().lines - [current_line..=last_visible_line] - { - let mut list = cosmic_text::AttrsList::new(attributes); - - for (range, highlight) in highlighter.highlight_line(line.text()) { - let format = format_highlight(&highlight); - - if format.color.is_some() || format.font.is_some() { - list.add_span( - range, - cosmic_text::Attrs { - color_opt: format.color.map(text::to_color), - ..if let Some(font) = format.font { - text::to_attributes(font) - } else { - attributes - } - }, - ); + internal.editor.with_buffer_mut(|buffer| { + for line in &mut buffer.lines[current_line..=last_visible_line] { + let mut list = cosmic_text::AttrsList::new(attributes); + + for (range, highlight) in + highlighter.highlight_line(line.text()) + { + let format = format_highlight(&highlight); + + if format.color.is_some() || format.font.is_some() { + list.add_span( + range, + cosmic_text::Attrs { + color_opt: format.color.map(text::to_color), + ..if let Some(font) = format.font { + text::to_attributes(font) + } else { + attributes + } + }, + ); + } } - } - let _ = line.set_attrs_list(list); - } + let _ = line.set_attrs_list(list); + } + }); - internal.editor.shape_as_needed(font_system.raw()); + internal.editor.shape_as_needed( + font_system.raw(), + false, /*TODO: support trimming caches*/ + ); self.0 = Some(Arc::new(internal)); } @@ -641,7 +685,8 @@ impl PartialEq for Internal { fn eq(&self, other: &Self) -> bool { self.font == other.font && self.bounds == other.bounds - && self.editor.buffer().metrics() == other.editor.buffer().metrics() + && self.editor.with_buffer(|buffer| buffer.metrics()) + == other.editor.with_buffer(|buffer| buffer.metrics()) } } @@ -746,9 +791,11 @@ fn highlight_line( } fn visual_lines_offset(line: usize, buffer: &cosmic_text::Buffer) -> i32 { + let scroll = buffer.scroll(); let visual_lines_before_start: usize = buffer .lines .iter() + .skip(scroll.line) .take(line) .map(|line| { line.layout_opt() @@ -758,22 +805,22 @@ fn visual_lines_offset(line: usize, buffer: &cosmic_text::Buffer) -> i32 { }) .sum(); - visual_lines_before_start as i32 - buffer.scroll() + visual_lines_before_start as i32 - scroll.layout } fn motion_to_action(motion: Motion) -> cosmic_text::Action { - match motion { - Motion::Left => cosmic_text::Action::Left, - Motion::Right => cosmic_text::Action::Right, - Motion::Up => cosmic_text::Action::Up, - Motion::Down => cosmic_text::Action::Down, - Motion::WordLeft => cosmic_text::Action::LeftWord, - Motion::WordRight => cosmic_text::Action::RightWord, - Motion::Home => cosmic_text::Action::Home, - Motion::End => cosmic_text::Action::End, - Motion::PageUp => cosmic_text::Action::PageUp, - Motion::PageDown => cosmic_text::Action::PageDown, - Motion::DocumentStart => cosmic_text::Action::BufferStart, - Motion::DocumentEnd => cosmic_text::Action::BufferEnd, - } + cosmic_text::Action::Motion(match motion { + Motion::Left => cosmic_text::Motion::Left, + Motion::Right => cosmic_text::Motion::Right, + Motion::Up => cosmic_text::Motion::Up, + Motion::Down => cosmic_text::Motion::Down, + Motion::WordLeft => cosmic_text::Motion::LeftWord, + Motion::WordRight => cosmic_text::Motion::RightWord, + Motion::Home => cosmic_text::Motion::Home, + Motion::End => cosmic_text::Motion::End, + Motion::PageUp => cosmic_text::Motion::PageUp, + Motion::PageDown => cosmic_text::Motion::PageDown, + Motion::DocumentStart => cosmic_text::Motion::BufferStart, + Motion::DocumentEnd => cosmic_text::Motion::BufferEnd, + }) } diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index 9413e31170..d87715261f 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -82,18 +82,20 @@ impl Pipeline { let mut font_system = font_system().write().expect("Write font system"); - draw( - font_system.raw(), - &mut self.glyph_cache, - editor.buffer(), - Rectangle::new(position, editor.bounds()), - color, - alignment::Horizontal::Left, - alignment::Vertical::Top, - scale_factor, - pixels, - clip_mask, - ); + editor.with_buffer(|buffer| { + draw( + font_system.raw(), + &mut self.glyph_cache, + buffer, + Rectangle::new(position, editor.bounds()), + color, + alignment::Horizontal::Left, + alignment::Vertical::Top, + scale_factor, + pixels, + clip_mask, + ); + }); } pub fn draw_cached( From 645a2a96874ed99aa900fbde816b0d32993e04db Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 29 Jan 2024 17:24:36 -0500 Subject: [PATCH 58/93] typo: add rev to glyphon --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 9e7d76ce21..116139bbd1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -139,7 +139,7 @@ bytemuck = { version = "1.0", features = ["derive"] } cosmic-text = { git = "https://github.com/pop-os/cosmic-text.git", rev = "e0ae465" } futures = "0.3" glam = "0.24" -glyphon = { git = "https://github.com/wash2/glyphon.git", rev = "3598375"} +glyphon = { git = "https://github.com/wash2/glyphon.git", rev = "b1610f3" } guillotiere = "0.6" half = "2.2" image = "0.24" From 259883080464af46584f39416c066b117fec7710 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 29 Jan 2024 17:27:02 -0500 Subject: [PATCH 59/93] chore: remove default features --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 116139bbd1..7872a077c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,6 @@ all-features = true maintenance = { status = "actively-developed" } [features] -default = ["wgpu", "winit", "multi-window"] # Enable the `wgpu` GPU-accelerated renderer backend wgpu = ["iced_renderer/wgpu", "iced_widget/wgpu"] # Enables the `Image` widget From 7d2564e3597ad2f532c4cbabacfcc9ecf9b597d2 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 29 Jan 2024 17:35:45 -0500 Subject: [PATCH 60/93] fix: core/serde --- core/src/keyboard/key.rs | 1 + core/src/lib.rs | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/core/src/keyboard/key.rs b/core/src/keyboard/key.rs index bf9a93c4f7..5655a16ed4 100644 --- a/core/src/keyboard/key.rs +++ b/core/src/keyboard/key.rs @@ -39,6 +39,7 @@ impl Key { /// /// [`winit`]: https://docs.rs/winit/0.29.10/winit/keyboard/enum.Key.html #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[allow(missing_docs)] pub enum Named { /// The `Alt` (Alternative) key. diff --git a/core/src/lib.rs b/core/src/lib.rs index 8f4c1c0519..bab7a3c259 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -9,12 +9,13 @@ #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] -#![forbid(unsafe_code, rust_2018_idioms)] +#![forbid(unsafe_code)] #![deny( missing_debug_implementations, missing_docs, unused_results, - rustdoc::broken_intra_doc_links + rustdoc::broken_intra_doc_links, + rust_2018_idioms )] pub mod alignment; pub mod border; From 4ffbc7996fafe147b29e48be85b344da436e5b97 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 29 Jan 2024 17:42:12 -0500 Subject: [PATCH 61/93] fix: downgrade resvg --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 7872a077c2..164c009fc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -153,7 +153,7 @@ ouroboros = "0.17" palette = "0.7" qrcode = { version = "0.12", default-features = false } raw-window-handle = "0.6" -resvg = "0.38" +resvg = "0.37" rustc-hash = "1.0" sctk = { package = "smithay-client-toolkit", git = "https://github.com/smithay/client-toolkit", rev = "2e9bf9f" } smol = "1.0" From aa6ef2b71fd74dba10a4a707afeff9ca4e01b729 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 1 Feb 2024 20:02:24 -0500 Subject: [PATCH 62/93] fix: typo --- sctk/src/sctk_event.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/sctk/src/sctk_event.rs b/sctk/src/sctk_event.rs index af81d84a92..832ba181a0 100755 --- a/sctk/src/sctk_event.rs +++ b/sctk/src/sctk_event.rs @@ -580,12 +580,10 @@ impl SctkEvent { .collect() } KeyboardEventVariant::Repeat(KeyEvent { - raw_code, - utf8, - .. + keysym, utf8, .. }) => { let (key, location) = - keysym_to_vkey_location(raw_code, utf8.as_deref()); + keysym_to_vkey_location(keysym.raw(), utf8.as_deref()); Some(iced_runtime::core::Event::Keyboard( keyboard::Event::KeyPressed { key: key, From 9597f09d608e1537e814cad6601bc6e4c1ce33f5 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 1 Feb 2024 21:44:29 -0500 Subject: [PATCH 63/93] chore: use updated softbuffer --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 164c009fc8..2f22cfb42a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -158,7 +158,7 @@ rustc-hash = "1.0" sctk = { package = "smithay-client-toolkit", git = "https://github.com/smithay/client-toolkit", rev = "2e9bf9f" } smol = "1.0" smol_str = "0.2" -softbuffer = "0.4" +softbuffer = { git = "https://github.com/pop-os/softbuffer", tag = "cosmic-4.0" } syntect = "5.1" sysinfo = "0.28" thiserror = "1.0" From 0c5cf6496c85721b3b653ede0ff8e35d21fab792 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 1 Feb 2024 22:10:38 -0500 Subject: [PATCH 64/93] feat(wgpu): use alpha modes for compositing if available --- wgpu/src/window/compositor.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 58f3f65410..efe5e4b724 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -241,6 +241,21 @@ impl graphics::Compositor for Compositor { width: u32, height: u32, ) { + let caps = surface.get_capabilities(&self.adapter); + let alpha_mode = if caps + .alpha_modes + .contains(&wgpu::CompositeAlphaMode::PostMultiplied) + { + wgpu::CompositeAlphaMode::PostMultiplied + } else if caps + .alpha_modes + .contains(&wgpu::CompositeAlphaMode::PreMultiplied) + { + wgpu::CompositeAlphaMode::PreMultiplied + } else { + wgpu::CompositeAlphaMode::Auto + }; + surface.configure( &self.device, &wgpu::SurfaceConfiguration { @@ -249,7 +264,7 @@ impl graphics::Compositor for Compositor { present_mode: self.settings.present_mode, width, height, - alpha_mode: wgpu::CompositeAlphaMode::Auto, + alpha_mode, view_formats: vec![], desired_maximum_frame_latency: 2, }, From 7ad4d85827daf47dd4b5426a0f91cd8277746dfa Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 1 Feb 2024 23:18:32 -0500 Subject: [PATCH 65/93] fix: distinguish between the key character and the utf8 of a key event --- sctk/Cargo.toml | 2 +- sctk/src/sctk_event.rs | 29 +++++++++++++++-------------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/sctk/Cargo.toml b/sctk/Cargo.toml index 733865d47a..4be53dd5c5 100644 --- a/sctk/Cargo.toml +++ b/sctk/Cargo.toml @@ -24,7 +24,7 @@ wayland-backend = {version = "0.3.1", features = ["client_system"]} float-cmp = "0.9" smithay-clipboard = "0.6" xkbcommon-dl = "0.4.1" - +xkbcommon = { version = "0.7", features = ["wayland"] } itertools = "0.12" xkeysym = "0.2.0" lazy_static = "1.4.0" diff --git a/sctk/src/sctk_event.rs b/sctk/src/sctk_event.rs index 832ba181a0..f2ddaa51d6 100755 --- a/sctk/src/sctk_event.rs +++ b/sctk/src/sctk_event.rs @@ -42,6 +42,7 @@ use sctk::{ }; use std::{collections::HashMap, time::Instant}; use wayland_protocols::wp::viewporter::client::wp_viewport::WpViewport; +use xkeysym::Keysym; pub enum IcedSctkEvent { /// Emitted when new events arrive from the OS to be processed. @@ -564,10 +565,7 @@ impl SctkEvent { )]) .collect(), KeyboardEventVariant::Press(ke) => { - let (key, location) = keysym_to_vkey_location( - ke.keysym.raw(), - ke.utf8.as_deref(), - ); + let (key, location) = keysym_to_vkey_location(ke.keysym); Some(iced_runtime::core::Event::Keyboard( keyboard::Event::KeyPressed { key: key, @@ -582,8 +580,7 @@ impl SctkEvent { KeyboardEventVariant::Repeat(KeyEvent { keysym, utf8, .. }) => { - let (key, location) = - keysym_to_vkey_location(keysym.raw(), utf8.as_deref()); + let (key, location) = keysym_to_vkey_location(keysym); Some(iced_runtime::core::Event::Keyboard( keyboard::Event::KeyPressed { key: key, @@ -596,10 +593,7 @@ impl SctkEvent { .collect() } KeyboardEventVariant::Release(ke) => { - let (k, location) = keysym_to_vkey_location( - ke.keysym.raw(), - ke.utf8.as_deref(), - ); + let (k, location) = keysym_to_vkey_location(ke.keysym); Some(iced_runtime::core::Event::Keyboard( keyboard::Event::KeyReleased { key: k, @@ -945,14 +939,21 @@ impl SctkEvent { } } -fn keysym_to_vkey_location(keysym: u32, utf8: Option<&str>) -> (Key, Location) { - let mut key = keysym_to_key(keysym); +fn keysym_to_vkey_location(keysym: Keysym) -> (Key, Location) { + let raw = keysym.raw(); + let mut key = keysym_to_key(raw); if matches!(key, key::Key::Unidentified) { - if let Some(utf8) = utf8 { + // XXX is there a better way to do this? + // we need to be able to determine the actual character for the key + // not the combination, so this seems to be correct + let mut utf8 = xkbcommon::xkb::keysym_to_utf8(keysym); + // remove null terminator + utf8.pop(); + if utf8.len() > 0 { key = Key::Character(utf8.into()); } } - let location = keymap::keysym_location(keysym); + let location = keymap::keysym_location(raw); (key, location) } From 9499727254042a98c191c4fa26ae4b6c71597a9a Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 2 Feb 2024 12:33:54 -0500 Subject: [PATCH 66/93] chore: update cosmic-text and glyphon --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2f22cfb42a..1a7a8cec8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -135,10 +135,10 @@ iced_accessibility = { version = "0.1", path = "accessibility" } async-std = "1.0" bitflags = "1.0" bytemuck = { version = "1.0", features = ["derive"] } -cosmic-text = { git = "https://github.com/pop-os/cosmic-text.git", rev = "e0ae465" } +cosmic-text = { git = "https://github.com/pop-os/cosmic-text.git", rev = "1b025ae" } futures = "0.3" glam = "0.24" -glyphon = { git = "https://github.com/wash2/glyphon.git", rev = "b1610f3" } +glyphon = { git = "https://github.com/wash2/glyphon.git", tag = "cosmic-0.5" } guillotiere = "0.6" half = "2.2" image = "0.24" From a83d381eb74116166df3583e650c72c481d8df0c Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 5 Feb 2024 13:05:50 -0500 Subject: [PATCH 67/93] fix(winit): pass text with modifiers in event --- winit/src/conversion.rs | 90 ++++++++++++++++++++++------------------- 1 file changed, 48 insertions(+), 42 deletions(-) diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index 489c9e8902..7aa7186701 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -2,6 +2,9 @@ //! //! [`winit`]: https://github.com/rust-windowing/winit //! [`iced_runtime`]: https://github.com/iced-rs/iced/tree/0.10/runtime +use winit::keyboard::SmolStr; +use winit::platform::modifier_supplement::KeyEventExtModifierSupplement; + use crate::core::keyboard; use crate::core::mouse; use crate::core::touch; @@ -195,51 +198,54 @@ pub fn window_event( })) } }, - WindowEvent::KeyboardInput { - event: - winit::event::KeyEvent { - logical_key, - state, - text, - location, - .. - }, - .. - } => Some(Event::Keyboard({ - let key = key(logical_key); - let modifiers = self::modifiers(modifiers); - - let location = match location { - winit::keyboard::KeyLocation::Standard => { - keyboard::Location::Standard - } - winit::keyboard::KeyLocation::Left => keyboard::Location::Left, - winit::keyboard::KeyLocation::Right => { - keyboard::Location::Right - } - winit::keyboard::KeyLocation::Numpad => { - keyboard::Location::Numpad - } - }; - - match state { - winit::event::ElementState::Pressed => { - keyboard::Event::KeyPressed { - key, - modifiers, - location, - text, + WindowEvent::KeyboardInput { event, .. } => { + let text_with_modifiers = + event.text_with_all_modifiers().map(|t| SmolStr::new(t)); + let winit::event::KeyEvent { + logical_key, + state, + text: _text, + location, + .. + } = event; + Some(Event::Keyboard({ + let key = key(logical_key); + let modifiers = self::modifiers(modifiers); + + let location = match location { + winit::keyboard::KeyLocation::Standard => { + keyboard::Location::Standard } - } - winit::event::ElementState::Released => { - keyboard::Event::KeyReleased { - key, - modifiers, - location, + winit::keyboard::KeyLocation::Left => { + keyboard::Location::Left + } + winit::keyboard::KeyLocation::Right => { + keyboard::Location::Right + } + winit::keyboard::KeyLocation::Numpad => { + keyboard::Location::Numpad + } + }; + + match state { + winit::event::ElementState::Pressed => { + keyboard::Event::KeyPressed { + key, + modifiers, + location, + text: text_with_modifiers, + } + } + winit::event::ElementState::Released => { + keyboard::Event::KeyReleased { + key, + modifiers, + location, + } } } - } - })), + })) + } WindowEvent::ModifiersChanged(new_modifiers) => { Some(Event::Keyboard(keyboard::Event::ModifiersChanged( self::modifiers(new_modifiers.state()), From 575de0024453168f328ac9228f6a3e0187d2e2bc Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Tue, 6 Feb 2024 09:14:52 -0800 Subject: [PATCH 68/93] Update wgpu to a commit that fixes use on Nvidia drivers This can be tested with something like `VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/nvidia_icd.json cargo run -p tour --features iced/wgpu`. On Nvidia I'm seeing a flood of `Suboptimal present of frame` warnings. So some improvement may still be needed here. But if it doesn't regress behavior on other hardware, that seems like an improvement over freezing. --- Cargo.toml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1a7a8cec8f..eb35749e9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -138,7 +138,7 @@ bytemuck = { version = "1.0", features = ["derive"] } cosmic-text = { git = "https://github.com/pop-os/cosmic-text.git", rev = "1b025ae" } futures = "0.3" glam = "0.24" -glyphon = { git = "https://github.com/wash2/glyphon.git", tag = "cosmic-0.5" } +glyphon = { git = "https://github.com/pop-os/glyphon.git", tag = "cosmic-0.5-wgpu" } guillotiere = "0.6" half = "2.2" image = "0.24" @@ -172,7 +172,9 @@ wasm-timer = "0.2" wayland-protocols = { version = "0.31.0", features = [ "staging"]} web-sys = "0.3" web-time = "0.2" -wgpu = "0.19" +# wgpu = "0.19" +# Newer wgpu commit that fixes Vulkan backend on Nvidia +wgpu = { git = "https://github.com/gfx-rs/wgpu", rev = "20fda69" } winapi = "0.3" window_clipboard = "0.4" winit = { git = "https://github.com/iced-rs/winit.git", rev = "b91e39ece2c0d378c3b80da7f3ab50e17bb798a5" } From dab797f490f2038a68112914da74eefecfb2fce3 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 7 Feb 2024 11:21:45 -0500 Subject: [PATCH 69/93] chore: unpin cosmic-text --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index eb35749e9b..bbb193f947 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -135,10 +135,10 @@ iced_accessibility = { version = "0.1", path = "accessibility" } async-std = "1.0" bitflags = "1.0" bytemuck = { version = "1.0", features = ["derive"] } -cosmic-text = { git = "https://github.com/pop-os/cosmic-text.git", rev = "1b025ae" } +cosmic-text = { git = "https://github.com/pop-os/cosmic-text.git" } futures = "0.3" glam = "0.24" -glyphon = { git = "https://github.com/pop-os/glyphon.git", tag = "cosmic-0.5-wgpu" } +glyphon = { git = "https://github.com/pop-os/glyphon.git", tag = "v0.5.0" } guillotiere = "0.6" half = "2.2" image = "0.24" From ef2f139ea30c3747f0d61d36a509e7234e4a8ac4 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 7 Feb 2024 12:47:42 -0500 Subject: [PATCH 70/93] chore: use pop-os fork of winit --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index bbb193f947..0abc67abb6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -177,4 +177,4 @@ web-time = "0.2" wgpu = { git = "https://github.com/gfx-rs/wgpu", rev = "20fda69" } winapi = "0.3" window_clipboard = "0.4" -winit = { git = "https://github.com/iced-rs/winit.git", rev = "b91e39ece2c0d378c3b80da7f3ab50e17bb798a5" } +winit = { git = "https://github.com/pop-os/winit.git", branch = "winit-0.29" } From 273f23439782228d5992ddceacd0d60e12f66528 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Wed, 7 Feb 2024 19:56:07 -0800 Subject: [PATCH 71/93] sctk: Add `Subsurface` widget (#79) This adds a widget that attaches an shm or dma buffer to a subsurface, scaled with `wp_viewporter`. By exposing this as a widget, rather than as a type of window, it can be positioned and scaled like any other iced widget. It provides an API that's similar to an iced image. The initial version of this just took a `wl_buffer`. But this makes buffer re-use problematic. In particular, the docs for `wl_surface::attach` note that `wl_buffer::release` events become unreliable if a buffer is attached to multiple surfaces. And indicates that a client should create multiple `wl_buffer` instances, or use `wp_linux_buffer_release`. So we store information about the buffer, and create `wl_buffer`s as needed. `SubsurfaceBuffer::new` also returns a future that's signaled when all references are destroyed, both `wl_buffer`s and any instance of the `SubsurfaceBuffer` that might still be used in the `view`. So this seems like the best solution for now, within the model-view-update architecture. This has two examples: `sctk_subsurface`, showing a single-color shm buffer, and `sctk_subsurface_gst`, which plays an h264 video to a subsurface with vaapi decoding. --- examples/sctk_subsurface/Cargo.toml | 14 + examples/sctk_subsurface/src/main.rs | 100 ++++ examples/sctk_subsurface/src/wayland.rs | 125 +++++ examples/sctk_subsurface_gst/Cargo.toml | 18 + examples/sctk_subsurface_gst/src/main.rs | 84 +++ examples/sctk_subsurface_gst/src/pipewire.rs | 111 ++++ sctk/src/application.rs | 38 ++ sctk/src/event_loop/mod.rs | 51 ++ sctk/src/event_loop/state.rs | 4 + sctk/src/handlers/mod.rs | 1 + sctk/src/handlers/subcompositor.rs | 5 + sctk/src/lib.rs | 1 + sctk/src/sctk_event.rs | 3 + sctk/src/subsurface_widget.rs | 519 +++++++++++++++++++ 14 files changed, 1074 insertions(+) create mode 100644 examples/sctk_subsurface/Cargo.toml create mode 100644 examples/sctk_subsurface/src/main.rs create mode 100644 examples/sctk_subsurface/src/wayland.rs create mode 100644 examples/sctk_subsurface_gst/Cargo.toml create mode 100644 examples/sctk_subsurface_gst/src/main.rs create mode 100644 examples/sctk_subsurface_gst/src/pipewire.rs create mode 100644 sctk/src/handlers/subcompositor.rs create mode 100644 sctk/src/subsurface_widget.rs diff --git a/examples/sctk_subsurface/Cargo.toml b/examples/sctk_subsurface/Cargo.toml new file mode 100644 index 0000000000..a896a237d5 --- /dev/null +++ b/examples/sctk_subsurface/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "sctk_subsurface" +version = "0.1.0" +edition = "2021" + +[dependencies] +sctk = { package = "smithay-client-toolkit", git = "https://github.com/smithay/client-toolkit", rev = "828b1eb" } +iced = { path = "../..", default-features = false, features = ["wayland", "debug", "a11y"] } +iced_runtime = { path = "../../runtime" } +iced_sctk = { path = "../../sctk" } +env_logger = "0.10" +futures-channel = "0.3.29" +calloop = "0.12.3" +rustix = { version = "0.38.30", features = ["fs", "shm"] } diff --git a/examples/sctk_subsurface/src/main.rs b/examples/sctk_subsurface/src/main.rs new file mode 100644 index 0000000000..c26034134a --- /dev/null +++ b/examples/sctk_subsurface/src/main.rs @@ -0,0 +1,100 @@ +// Shows a subsurface with a 1x1 px red buffer, stretch to window size + +use iced::{ + event::wayland::Event as WaylandEvent, wayland::InitialSurface, + widget::text, window, Application, Command, Element, Length, Subscription, + Theme, +}; +use iced_sctk::subsurface_widget::SubsurfaceBuffer; +use sctk::reexports::client::{Connection, Proxy}; + +mod wayland; + +fn main() { + let mut settings = iced::Settings::default(); + settings.initial_surface = InitialSurface::XdgWindow(Default::default()); + SubsurfaceApp::run(settings).unwrap(); +} + +#[derive(Debug, Clone, Default)] +struct SubsurfaceApp { + connection: Option, + red_buffer: Option, +} + +#[derive(Debug, Clone)] +pub enum Message { + WaylandEvent(WaylandEvent), + Wayland(wayland::Event), +} + +impl Application for SubsurfaceApp { + type Executor = iced::executor::Default; + type Message = Message; + type Flags = (); + type Theme = Theme; + + fn new(_flags: ()) -> (SubsurfaceApp, Command) { + ( + SubsurfaceApp { + ..SubsurfaceApp::default() + }, + Command::none(), + ) + } + + fn title(&self, _id: window::Id) -> String { + String::from("SubsurfaceApp") + } + + fn update(&mut self, message: Self::Message) -> Command { + match message { + Message::WaylandEvent(evt) => match evt { + WaylandEvent::Output(_evt, output) => { + if self.connection.is_none() { + if let Some(backend) = output.backend().upgrade() { + self.connection = + Some(Connection::from_backend(backend)); + } + } + } + _ => {} + }, + Message::Wayland(evt) => match evt { + wayland::Event::RedBuffer(buffer) => { + self.red_buffer = Some(buffer); + } + }, + } + Command::none() + } + + fn view(&self, _id: window::Id) -> Element { + if let Some(buffer) = &self.red_buffer { + iced_sctk::subsurface_widget::Subsurface::new(1, 1, buffer) + .width(Length::Fill) + .height(Length::Fill) + .into() + } else { + text("No subsurface").into() + } + } + + fn subscription(&self) -> Subscription { + let mut subscriptions = vec![iced::event::listen_with(|evt, _| { + if let iced::Event::PlatformSpecific( + iced::event::PlatformSpecific::Wayland(evt), + ) = evt + { + Some(Message::WaylandEvent(evt)) + } else { + None + } + })]; + if let Some(connection) = &self.connection { + subscriptions + .push(wayland::subscription(connection).map(Message::Wayland)); + } + Subscription::batch(subscriptions) + } +} diff --git a/examples/sctk_subsurface/src/wayland.rs b/examples/sctk_subsurface/src/wayland.rs new file mode 100644 index 0000000000..c3ba0b46e1 --- /dev/null +++ b/examples/sctk_subsurface/src/wayland.rs @@ -0,0 +1,125 @@ +use futures_channel::mpsc; +use iced::futures::{FutureExt, SinkExt}; +use iced_sctk::subsurface_widget::{Shmbuf, SubsurfaceBuffer}; +use rustix::{io::Errno, shm::ShmOFlags}; +use sctk::{ + reexports::{ + calloop_wayland_source::WaylandSource, + client::{ + delegate_noop, + globals::registry_queue_init, + protocol::{wl_buffer::WlBuffer, wl_shm}, + Connection, + }, + }, + registry::{ProvidesRegistryState, RegistryState}, + shm::{Shm, ShmHandler}, +}; +use std::{ + os::fd::OwnedFd, + sync::Arc, + thread, + time::{SystemTime, UNIX_EPOCH}, +}; + +#[derive(Debug, Clone)] +pub enum Event { + RedBuffer(SubsurfaceBuffer), +} + +struct AppData { + registry_state: RegistryState, + shm_state: Shm, +} + +impl ProvidesRegistryState for AppData { + fn registry(&mut self) -> &mut RegistryState { + &mut self.registry_state + } + + sctk::registry_handlers!(); +} + +impl ShmHandler for AppData { + fn shm_state(&mut self) -> &mut Shm { + &mut self.shm_state + } +} + +pub fn subscription(connection: &Connection) -> iced::Subscription { + let connection = connection.clone(); + iced::subscription::run_with_id( + "wayland-sub", + async { start(connection).await }.flatten_stream(), + ) +} + +async fn start(conn: Connection) -> mpsc::Receiver { + let (mut sender, receiver) = mpsc::channel(20); + + let (globals, event_queue) = registry_queue_init(&conn).unwrap(); + let qh = event_queue.handle(); + + let mut app_data = AppData { + registry_state: RegistryState::new(&globals), + shm_state: Shm::bind(&globals, &qh).unwrap(), + }; + + let fd = create_memfile().unwrap(); + rustix::io::write(&fd, &[0, 0, 255, 255]).unwrap(); + + let shmbuf = Shmbuf { + fd, + offset: 0, + width: 1, + height: 1, + stride: 4, + format: wl_shm::Format::Xrgb8888, + }; + + let buffer = SubsurfaceBuffer::new(Arc::new(shmbuf.into())).0; + let _ = sender.send(Event::RedBuffer(buffer)).await; + + thread::spawn(move || { + let mut event_loop = calloop::EventLoop::try_new().unwrap(); + WaylandSource::new(conn, event_queue) + .insert(event_loop.handle()) + .unwrap(); + loop { + event_loop.dispatch(None, &mut app_data).unwrap(); + } + }); + + receiver +} + +fn create_memfile() -> rustix::io::Result { + loop { + let flags = ShmOFlags::CREATE | ShmOFlags::EXCL | ShmOFlags::RDWR; + + let time = SystemTime::now(); + let name = format!( + "/iced-sctk-{}", + time.duration_since(UNIX_EPOCH).unwrap().subsec_nanos() + ); + + match rustix::io::retry_on_intr(|| { + rustix::shm::shm_open(&name, flags, 0600.into()) + }) { + Ok(fd) => match rustix::shm::shm_unlink(&name) { + Ok(_) => return Ok(fd), + Err(errno) => { + return Err(errno.into()); + } + }, + Err(Errno::EXIST) => { + continue; + } + Err(err) => return Err(err.into()), + } + } +} + +delegate_noop!(AppData: ignore WlBuffer); +sctk::delegate_registry!(AppData); +sctk::delegate_shm!(AppData); diff --git a/examples/sctk_subsurface_gst/Cargo.toml b/examples/sctk_subsurface_gst/Cargo.toml new file mode 100644 index 0000000000..f83edd45a0 --- /dev/null +++ b/examples/sctk_subsurface_gst/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "sctk_subsurface_gst" +version = "0.1.0" +edition = "2021" + +[dependencies] +sctk = { package = "smithay-client-toolkit", git = "https://github.com/smithay/client-toolkit", rev = "828b1eb" } +iced = { path = "../..", default-features = false, features = ["wayland", "debug", "a11y"] } +iced_runtime = { path = "../../runtime" } +iced_sctk = { path = "../../sctk" } +env_logger = "0.10" +futures-channel = "0.3.29" +calloop = "0.12.3" +gst = { package = "gstreamer", version = "0.21.3" } +gst-app = { package = "gstreamer-app", version = "0.21.2" } +gst-video = { package = "gstreamer-video", version = "0.21.2" } +gst-allocators = { package = "gstreamer-allocators", version = "0.21.2" } +drm-fourcc = "2.2.0" diff --git a/examples/sctk_subsurface_gst/src/main.rs b/examples/sctk_subsurface_gst/src/main.rs new file mode 100644 index 0000000000..09119117bb --- /dev/null +++ b/examples/sctk_subsurface_gst/src/main.rs @@ -0,0 +1,84 @@ +// Shows a subsurface with a 1x1 px red buffer, stretch to window size + +use iced::{ + wayland::InitialSurface, widget::text, window, Application, Command, + Element, Length, Subscription, Theme, +}; +use iced_sctk::subsurface_widget::SubsurfaceBuffer; +use std::{env, path::Path}; + +mod pipewire; + +fn main() { + let args = env::args(); + if args.len() != 2 { + eprintln!("usage: sctk_subsurface_gst [h264 mp4 path]"); + return; + } + let path = args.skip(1).next().unwrap(); + if !Path::new(&path).exists() { + eprintln!("File `{path}` not found."); + return; + } + let mut settings = iced::Settings::with_flags(path); + settings.initial_surface = InitialSurface::XdgWindow(Default::default()); + SubsurfaceApp::run(settings).unwrap(); +} + +#[derive(Debug, Clone, Default)] +struct SubsurfaceApp { + path: String, + buffer: Option, +} + +#[derive(Debug, Clone)] +pub enum Message { + Pipewire(pipewire::Event), +} + +impl Application for SubsurfaceApp { + type Executor = iced::executor::Default; + type Message = Message; + type Flags = String; + type Theme = Theme; + + fn new(flags: String) -> (SubsurfaceApp, Command) { + ( + SubsurfaceApp { + path: flags, + ..SubsurfaceApp::default() + }, + Command::none(), + ) + } + + fn title(&self, _id: window::Id) -> String { + String::from("SubsurfaceApp") + } + + fn update(&mut self, message: Self::Message) -> Command { + match message { + Message::Pipewire(evt) => match evt { + pipewire::Event::Frame(subsurface_buffer) => { + self.buffer = Some(subsurface_buffer); + } + }, + } + Command::none() + } + + fn view(&self, _id: window::Id) -> Element { + if let Some(buffer) = &self.buffer { + iced_sctk::subsurface_widget::Subsurface::new(1, 1, buffer) + .width(Length::Fill) + .height(Length::Fill) + .into() + } else { + text("No subsurface").into() + } + } + + fn subscription(&self) -> Subscription { + pipewire::subscription(&self.path).map(Message::Pipewire) + } +} diff --git a/examples/sctk_subsurface_gst/src/pipewire.rs b/examples/sctk_subsurface_gst/src/pipewire.rs new file mode 100644 index 0000000000..302f19c4b9 --- /dev/null +++ b/examples/sctk_subsurface_gst/src/pipewire.rs @@ -0,0 +1,111 @@ +use drm_fourcc::{DrmFourcc, DrmModifier}; +use gst::prelude::*; +use iced::futures::{executor::block_on, SinkExt}; +use iced_sctk::subsurface_widget::{Dmabuf, Plane, SubsurfaceBuffer}; +use std::{os::unix::io::BorrowedFd, sync::Arc, thread}; + +#[derive(Debug, Clone)] +pub enum Event { + Frame(SubsurfaceBuffer), +} + +pub fn subscription(path: &str) -> iced::Subscription { + let path = path.to_string(); + iced::subscription::channel("pw", 16, |sender| async { + thread::spawn(move || pipewire_thread(&path, sender)); + std::future::pending().await + }) +} + +fn pipewire_thread( + path: &str, + mut sender: futures_channel::mpsc::Sender, +) { + gst::init().unwrap(); + + // `vapostproc` can be added to convert color format + // TODO had issue on smithay using NV12? + let pipeline = gst::parse_launch(&format!( + "filesrc location={path} ! + qtdemux ! + h264parse ! + vah264dec ! + vapostproc ! + video/x-raw(memory:DMABuf),format=BGRA ! + appsink name=sink", + )) + .unwrap() + .dynamic_cast::() + .unwrap(); + + let appsink = pipeline + .by_name("sink") + .unwrap() + .dynamic_cast::() + .unwrap(); + + let mut subsurface_release = None; + + appsink.set_callbacks( + gst_app::AppSinkCallbacks::builder() + .new_sample(move |appsink| { + let sample = + appsink.pull_sample().map_err(|_| gst::FlowError::Eos)?; + + let buffer = sample.buffer().unwrap(); + let meta = buffer.meta::().unwrap(); + + let planes = (0..meta.n_planes()) + .map(|plane_idx| { + let memory = buffer + .memory(plane_idx) + .unwrap() + .downcast_memory::() + .unwrap(); + + // TODO avoid dup? + let fd = unsafe { BorrowedFd::borrow_raw(memory.fd()) } + .try_clone_to_owned() + .unwrap(); + + Plane { + fd, + plane_idx, + offset: meta.offset()[plane_idx as usize] as u32, + stride: meta.stride()[plane_idx as usize] as u32, + } + }) + .collect(); + + let dmabuf = Dmabuf { + width: meta.width() as i32, + height: meta.height() as i32, + planes, + // TODO should use dmabuf protocol to get supported formats, + // convert if needed. + format: DrmFourcc::Argb8888 as u32, + //format: DrmFourcc::Nv12 as u32, + // TODO modifier negotiation + modifier: DrmModifier::Linear.into(), + }; + + let (buffer, new_subsurface_release) = + SubsurfaceBuffer::new(Arc::new(dmabuf.into())); + block_on(sender.send(Event::Frame(buffer))).unwrap(); + + // Wait for server to release other buffer + // TODO is gstreamer using triple buffering? + if let Some(release) = subsurface_release.take() { + block_on(release); + } + subsurface_release = Some(new_subsurface_release); + + Ok(gst::FlowSuccess::Ok) + }) + .build(), + ); + + pipeline.set_state(gst::State::Playing).unwrap(); + let bus = pipeline.bus().unwrap(); + for _msg in bus.iter_timed(gst::ClockTime::NONE) {} +} diff --git a/sctk/src/application.rs b/sctk/src/application.rs index d3660a2e5b..065f386d94 100644 --- a/sctk/src/application.rs +++ b/sctk/src/application.rs @@ -80,6 +80,8 @@ use raw_window_handle::{ }; use std::mem::ManuallyDrop; +use crate::subsurface_widget::{SubsurfaceInstance, SubsurfaceState}; + pub enum Event { /// A normal sctk event SctkEvent(IcedSctkEvent), @@ -363,6 +365,8 @@ where let mut interfaces = ManuallyDrop::new(HashMap::new()); let mut simple_clipboard = Clipboard::unconnected(); + let mut subsurface_state = None::>; + { run_command( &application, @@ -893,6 +897,9 @@ where ); state.synchronize(&application); + // Subsurface list should always be empty before `view` + assert!(crate::subsurface_widget::take_subsurfaces().is_empty()); + // just draw here immediately and never again for dnd icons // TODO handle scale factor? let _new_mouse_interaction = user_interface.draw( @@ -906,6 +913,15 @@ where state.cursor(), ); + let subsurfaces = crate::subsurface_widget::take_subsurfaces(); + if let Some(subsurface_state) = subsurface_state.as_ref() { + subsurface_state.update_subsurfaces( + &state.wrapper.wl_surface, + &mut state.subsurfaces, + &subsurfaces, + ); + } + let _ = compositor.present( &mut renderer, state.surface.as_mut().unwrap(), @@ -1354,6 +1370,12 @@ where debug.layout_finished(); state.viewport_changed = false; } + + // Subsurface list should always be empty before `view` + assert!( + crate::subsurface_widget::take_subsurfaces().is_empty() + ); + debug.draw_started(); let new_mouse_interaction = user_interface.draw( &mut renderer, @@ -1366,6 +1388,17 @@ where state.cursor(), ); + // Update subsurfaces based on what view requested. + let subsurfaces = + crate::subsurface_widget::take_subsurfaces(); + if let Some(subsurface_state) = subsurface_state.as_ref() { + subsurface_state.update_subsurfaces( + &state.wrapper.wl_surface, + &mut state.subsurfaces, + &subsurfaces, + ); + } + debug.draw_finished(); if new_mouse_interaction != mouse_interaction { mouse_interaction = new_mouse_interaction; @@ -1462,6 +1495,9 @@ where } } } + IcedSctkEvent::Subcompositor(state) => { + subsurface_state = Some(state); + } } } @@ -1598,6 +1634,7 @@ where interface_state: user_interface::State, surface: Option, wrapper: SurfaceDisplayWrapper, + subsurfaces: Vec, } impl State @@ -1636,6 +1673,7 @@ where interface_state: user_interface::State::Outdated, surface: None, wrapper, + subsurfaces: Vec::new(), } } diff --git a/sctk/src/event_loop/mod.rs b/sctk/src/event_loop/mod.rs index 8e2acd1dd3..e728dd7b95 100644 --- a/sctk/src/event_loop/mod.rs +++ b/sctk/src/event_loop/mod.rs @@ -20,6 +20,7 @@ use crate::{ PopupEventVariant, SctkEvent, StartCause, WindowEventVariant, }, settings, + subsurface_widget::SubsurfaceState, }; use iced_futures::core::window::Mode; use iced_runtime::command::platform_specific::{ @@ -33,6 +34,7 @@ use sctk::{ activation::{ActivationState, RequestData}, compositor::CompositorState, data_device_manager::DataDeviceManagerState, + globals::GlobalData, output::OutputState, reexports::{ calloop::{self, EventLoop, PostAction}, @@ -298,6 +300,55 @@ where &mut control_flow, ); + // XXX don't re-bind? + let wl_compositor = self + .state + .registry_state + .bind_one(&self.state.queue_handle, 1..=6, GlobalData) + .unwrap(); + let wl_subcompositor = self.state.registry_state.bind_one( + &self.state.queue_handle, + 1..=1, + GlobalData, + ); + let wp_viewporter = self.state.registry_state.bind_one( + &self.state.queue_handle, + 1..=1, + GlobalData, + ); + let wl_shm = self + .state + .registry_state + .bind_one(&self.state.queue_handle, 1..=1, GlobalData) + .unwrap(); + let wp_dmabuf = self + .state + .registry_state + .bind_one(&self.state.queue_handle, 2..=4, GlobalData) + .ok(); + if let Ok(wl_subcompositor) = wl_subcompositor { + if let Ok(wp_viewporter) = wp_viewporter { + callback( + IcedSctkEvent::Subcompositor(SubsurfaceState { + wl_compositor, + wl_subcompositor, + wp_viewporter, + wl_shm, + wp_dmabuf, + qh: self.state.queue_handle.clone(), + }), + &self.state, + &mut control_flow, + ); + } else { + tracing::warn!( + "No `wp_viewporter`. Subsurfaces not supported." + ); + } + } else { + tracing::warn!("No `wl_subcompositor`. Subsurfaces not supported."); + } + let mut sctk_event_sink_back_buffer = Vec::new(); let mut compositor_event_back_buffer = Vec::new(); let mut frame_event_back_buffer = Vec::new(); diff --git a/sctk/src/event_loop/state.rs b/sctk/src/event_loop/state.rs index e2b0e017c0..9f3fafaf0d 100644 --- a/sctk/src/event_loop/state.rs +++ b/sctk/src/event_loop/state.rs @@ -41,10 +41,12 @@ use sctk::{ reexports::{ calloop::{LoopHandle, RegistrationToken}, client::{ + delegate_noop, protocol::{ wl_keyboard::WlKeyboard, wl_output::WlOutput, wl_seat::WlSeat, + wl_subsurface::WlSubsurface, wl_surface::{self, WlSurface}, wl_touch::WlTouch, }, @@ -842,3 +844,5 @@ where } } } + +delegate_noop!(@ SctkState: ignore WlSubsurface); diff --git a/sctk/src/handlers/mod.rs b/sctk/src/handlers/mod.rs index 332d296682..be989e6bf0 100644 --- a/sctk/src/handlers/mod.rs +++ b/sctk/src/handlers/mod.rs @@ -6,6 +6,7 @@ pub mod output; pub mod seat; pub mod session_lock; pub mod shell; +pub mod subcompositor; pub mod wp_fractional_scaling; pub mod wp_viewporter; diff --git a/sctk/src/handlers/subcompositor.rs b/sctk/src/handlers/subcompositor.rs new file mode 100644 index 0000000000..a5c9fdab3a --- /dev/null +++ b/sctk/src/handlers/subcompositor.rs @@ -0,0 +1,5 @@ +use crate::handlers::SctkState; +use sctk::delegate_subcompositor; +use std::fmt::Debug; + +delegate_subcompositor!(@ SctkState); diff --git a/sctk/src/lib.rs b/sctk/src/lib.rs index 48520f8889..beed0700d6 100644 --- a/sctk/src/lib.rs +++ b/sctk/src/lib.rs @@ -10,6 +10,7 @@ pub mod keymap; pub mod result; pub mod sctk_event; pub mod settings; +pub mod subsurface_widget; #[cfg(feature = "system")] pub mod system; pub mod util; diff --git a/sctk/src/sctk_event.rs b/sctk/src/sctk_event.rs index f2ddaa51d6..f67cd8e241 100755 --- a/sctk/src/sctk_event.rs +++ b/sctk/src/sctk_event.rs @@ -5,6 +5,7 @@ use crate::{ }, dpi::PhysicalSize, keymap::{self, keysym_to_key}, + subsurface_widget::SubsurfaceState, }; use iced_futures::core::event::{ @@ -122,6 +123,8 @@ pub enum IcedSctkEvent { /// Frame callback event Frame(WlSurface), + + Subcompositor(SubsurfaceState), } #[derive(Debug, Clone)] diff --git a/sctk/src/subsurface_widget.rs b/sctk/src/subsurface_widget.rs new file mode 100644 index 0000000000..699ae9e5a6 --- /dev/null +++ b/sctk/src/subsurface_widget.rs @@ -0,0 +1,519 @@ +// TODO z-order option? + +use crate::core::{ + layout::{self, Layout}, + mouse, renderer, + widget::{self, Widget}, + ContentFit, Element, Length, Rectangle, Size, +}; +use std::{ + cell::RefCell, + future::Future, + mem, + os::unix::io::{AsFd, OwnedFd}, + pin::Pin, + sync::Arc, + task, +}; + +use futures::channel::oneshot; +use sctk::{ + compositor::SurfaceData, + globals::GlobalData, + reexports::client::{ + protocol::{ + wl_buffer::{self, WlBuffer}, + wl_compositor::WlCompositor, + wl_shm::{self, WlShm}, + wl_shm_pool::{self, WlShmPool}, + wl_subcompositor::WlSubcompositor, + wl_subsurface::WlSubsurface, + wl_surface::WlSurface, + }, + Connection, Dispatch, Proxy, QueueHandle, + }, +}; +use wayland_protocols::wp::{ + linux_dmabuf::zv1::client::{ + zwp_linux_buffer_params_v1::{self, ZwpLinuxBufferParamsV1}, + zwp_linux_dmabuf_v1::{self, ZwpLinuxDmabufV1}, + }, + viewporter::client::{ + wp_viewport::WpViewport, wp_viewporter::WpViewporter, + }, +}; + +use crate::event_loop::state::SctkState; + +#[derive(Debug)] +pub struct Plane { + pub fd: OwnedFd, + pub plane_idx: u32, + pub offset: u32, + pub stride: u32, +} + +#[derive(Debug)] +pub struct Dmabuf { + pub width: i32, + pub height: i32, + pub planes: Vec, + pub format: u32, + pub modifier: u64, +} + +#[derive(Debug)] +pub struct Shmbuf { + pub fd: OwnedFd, + pub offset: i32, + pub width: i32, + pub height: i32, + pub stride: i32, + pub format: wl_shm::Format, +} + +#[derive(Debug)] +pub enum BufferSource { + Shm(Shmbuf), + Dma(Dmabuf), +} + +impl From for BufferSource { + fn from(buf: Shmbuf) -> Self { + Self::Shm(buf) + } +} + +impl From for BufferSource { + fn from(buf: Dmabuf) -> Self { + Self::Dma(buf) + } +} + +#[derive(Debug)] +struct SubsurfaceBufferInner { + source: Arc, + _sender: oneshot::Sender<()>, +} + +/// Refcounted type containing a `BufferSource` with a sender that is signaled +/// all references are dropped and `wl_buffer`s created from the source are +/// released. +#[derive(Clone, Debug)] +pub struct SubsurfaceBuffer(Arc); + +pub struct BufferData { + source: SubsurfaceBuffer, +} + +/// Future signalled when subsurface buffer is released +pub struct SubsurfaceBufferRelease(oneshot::Receiver<()>); + +impl SubsurfaceBufferRelease { + /// Non-blocking check if buffer is released yet, without awaiting + pub fn released(&mut self) -> bool { + self.0.try_recv() == Ok(None) + } +} + +impl Future for SubsurfaceBufferRelease { + type Output = (); + + fn poll( + mut self: Pin<&mut Self>, + cx: &mut task::Context, + ) -> task::Poll<()> { + Pin::new(&mut self.0).poll(cx).map(|_| ()) + } +} + +impl SubsurfaceBuffer { + pub fn new(source: Arc) -> (Self, SubsurfaceBufferRelease) { + let (_sender, receiver) = oneshot::channel(); + let subsurface_buffer = + SubsurfaceBuffer(Arc::new(SubsurfaceBufferInner { + source, + _sender, + })); + (subsurface_buffer, SubsurfaceBufferRelease(receiver)) + } + + // Behavior of `wl_buffer::released` is undefined if attached to multiple surfaces. To allow + // things like that, create a new `wl_buffer` each time. + fn create_buffer( + &self, + shm: &WlShm, + dmabuf: Option<&ZwpLinuxDmabufV1>, + qh: &QueueHandle>, + ) -> Option { + // create reference to source, that is dropped on release + match self.0.source.as_ref() { + BufferSource::Shm(buf) => { + let pool = shm.create_pool( + buf.fd.as_fd(), + buf.offset + buf.height * buf.stride, + qh, + GlobalData, + ); + let buffer = pool.create_buffer( + buf.offset, + buf.width, + buf.height, + buf.stride, + buf.format, + qh, + BufferData { + source: self.clone(), + }, + ); + pool.destroy(); + Some(buffer) + } + BufferSource::Dma(buf) => { + if let Some(dmabuf) = dmabuf { + let params = dmabuf.create_params(qh, GlobalData); + for plane in &buf.planes { + let modifier_hi = (buf.modifier >> 32) as u32; + let modifier_lo = (buf.modifier & 0xffffffff) as u32; + params.add( + plane.fd.as_fd(), + plane.plane_idx, + plane.offset, + plane.stride, + modifier_hi, + modifier_lo, + ); + } + // Will cause protocol error if format is not supported + Some(params.create_immed( + buf.width, + buf.height, + buf.format, + zwp_linux_buffer_params_v1::Flags::empty(), + qh, + BufferData { + source: self.clone(), + }, + )) + } else { + None + } + } + } + } + + fn for_buffer(buffer: &WlBuffer) -> Option<&Self> { + Some(&buffer.data::()?.source) + } +} + +impl PartialEq for SubsurfaceBuffer { + fn eq(&self, rhs: &Self) -> bool { + Arc::ptr_eq(&self.0, &rhs.0) + } +} + +impl Dispatch for SctkState { + fn event( + _: &mut SctkState, + _: &WlShmPool, + _: wl_shm_pool::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle>, + ) { + unreachable!() + } +} + +impl Dispatch for SctkState { + fn event( + _: &mut SctkState, + _: &ZwpLinuxDmabufV1, + _: zwp_linux_dmabuf_v1::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle>, + ) { + } +} + +impl Dispatch for SctkState { + fn event( + _: &mut SctkState, + _: &ZwpLinuxBufferParamsV1, + _: zwp_linux_buffer_params_v1::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle>, + ) { + } +} + +impl Dispatch for SctkState { + fn event( + _: &mut SctkState, + _: &WlBuffer, + event: wl_buffer::Event, + _: &BufferData, + _: &Connection, + _: &QueueHandle>, + ) { + match event { + wl_buffer::Event::Release => {} + _ => unreachable!(), + } + } +} + +// create wl_buffer from BufferSource (avoid create_immed?) +// release +#[doc(hidden)] +pub struct SubsurfaceState { + pub wl_compositor: WlCompositor, + pub wl_subcompositor: WlSubcompositor, + pub wp_viewporter: WpViewporter, + pub wl_shm: WlShm, + pub wp_dmabuf: Option, + pub qh: QueueHandle>, +} + +impl SubsurfaceState { + fn create_subsurface(&self, parent: &WlSurface) -> SubsurfaceInstance { + let wl_surface = self + .wl_compositor + .create_surface(&self.qh, SurfaceData::new(None, 1)); + let wl_subsurface = self.wl_subcompositor.get_subsurface( + &wl_surface, + parent, + &self.qh, + (), + ); + let wp_viewport = self.wp_viewporter.get_viewport( + &wl_surface, + &self.qh, + sctk::globals::GlobalData, + ); + SubsurfaceInstance { + wl_surface, + wl_subsurface, + wp_viewport, + wl_buffer: None, + } + } + + // Update `subsurfaces` from `view_subsurfaces` + pub(crate) fn update_subsurfaces( + &self, + parent: &WlSurface, + subsurfaces: &mut Vec, + view_subsurfaces: &[SubsurfaceInfo], + ) { + // If view requested fewer subsurfaces than there currently are, + // destroy excess. + if view_subsurfaces.len() < subsurfaces.len() { + subsurfaces.truncate(view_subsurfaces.len()); + } + // Create new subsurfaces if there aren't enough. + while subsurfaces.len() < view_subsurfaces.len() { + subsurfaces.push(self.create_subsurface(parent)); + } + // Attach buffers to subsurfaces, set viewports, and commit. + for (subsurface_data, subsurface) in + view_subsurfaces.iter().zip(subsurfaces.iter_mut()) + { + subsurface.attach_and_commit( + subsurface_data, + &self.wl_shm, + self.wp_dmabuf.as_ref(), + &self.qh, + ); + } + } +} + +pub(crate) struct SubsurfaceInstance { + wl_surface: WlSurface, + wl_subsurface: WlSubsurface, + wp_viewport: WpViewport, + wl_buffer: Option, +} + +impl SubsurfaceInstance { + // TODO correct damage? no damage/commit if unchanged? + fn attach_and_commit( + &mut self, + info: &SubsurfaceInfo, + shm: &WlShm, + dmabuf: Option<&ZwpLinuxDmabufV1>, + qh: &QueueHandle>, + ) { + let buffer = match self.wl_buffer.take() { + Some(buffer) + if SubsurfaceBuffer::for_buffer(&buffer) + == Some(&info.buffer) => + { + // Same buffer is already attached to this subsurface. Don't create new `wl_buffer`. + buffer + } + buffer => { + if let Some(buffer) = buffer { + buffer.destroy(); + } + if let Some(buffer) = info.buffer.create_buffer(shm, dmabuf, qh) + { + buffer + } else { + // TODO log error + self.wl_surface.attach(None, 0, 0); + return; + } + } + }; + + // XXX scale factor? + self.wl_subsurface + .set_position(info.bounds.x as i32, info.bounds.y as i32); + self.wp_viewport.set_destination( + info.bounds.width as i32, + info.bounds.height as i32, + ); + self.wl_surface.attach(Some(&buffer), 0, 0); + self.wl_surface.damage(0, 0, i32::MAX, i32::MAX); + self.wl_surface.commit(); + + self.wl_buffer = Some(buffer); + } +} + +impl Drop for SubsurfaceInstance { + fn drop(&mut self) { + self.wp_viewport.destroy(); + self.wl_subsurface.destroy(); + self.wl_surface.destroy(); + if let Some(wl_buffer) = self.wl_buffer.as_ref() { + wl_buffer.destroy(); + } + } +} + +pub(crate) struct SubsurfaceInfo { + pub buffer: SubsurfaceBuffer, + pub bounds: Rectangle, +} + +thread_local! { + static SUBSURFACES: RefCell> = RefCell::new(Vec::new()); +} + +pub(crate) fn take_subsurfaces() -> Vec { + SUBSURFACES.with(|subsurfaces| mem::take(&mut *subsurfaces.borrow_mut())) +} + +#[must_use] +pub struct Subsurface<'a> { + buffer_size: Size, + buffer: &'a SubsurfaceBuffer, + width: Length, + height: Length, + content_fit: ContentFit, +} + +impl<'a, Message, Theme, Renderer> Widget + for Subsurface<'a> +where + Renderer: renderer::Renderer, +{ + fn size(&self) -> Size { + Size::new(self.width, self.height) + } + + // Based on image widget + fn layout( + &self, + _tree: &mut widget::Tree, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let raw_size = + limits.resolve(self.width, self.height, self.buffer_size); + + let full_size = self.content_fit.fit(self.buffer_size, raw_size); + + let final_size = Size { + width: match self.width { + Length::Shrink => f32::min(raw_size.width, full_size.width), + _ => raw_size.width, + }, + height: match self.height { + Length::Shrink => f32::min(raw_size.height, full_size.height), + _ => raw_size.height, + }, + }; + + layout::Node::new(final_size) + } + + fn draw( + &self, + _state: &widget::Tree, + _renderer: &mut Renderer, + _theme: &Theme, + _style: &renderer::Style, + layout: Layout<'_>, + _cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + // Instead of using renderer, we need to add surface to a list that is + // read by the iced-sctk shell. + SUBSURFACES.with(|subsurfaces| { + subsurfaces.borrow_mut().push(SubsurfaceInfo { + buffer: self.buffer.clone(), + bounds: layout.bounds(), + }) + }); + } +} + +impl<'a> Subsurface<'a> { + pub fn new( + buffer_width: u32, + buffer_height: u32, + buffer: &'a SubsurfaceBuffer, + ) -> Self { + Self { + buffer_size: Size::new(buffer_width as f32, buffer_height as f32), + buffer, + // Matches defaults of image widget + width: Length::Shrink, + height: Length::Shrink, + content_fit: ContentFit::Contain, + } + } + + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } + + pub fn content_fit(mut self, content_fit: ContentFit) -> Self { + self.content_fit = content_fit; + self + } +} + +impl<'a, Message, Theme, Renderer> From> + for Element<'a, Message, Theme, Renderer> +where + Message: Clone + 'a, + Renderer: renderer::Renderer, +{ + fn from(subsurface: Subsurface<'a>) -> Self { + Self::new(subsurface) + } +} From 68b3e4966c0c0d66f138b8eabc37a2c117f95b6f Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Thu, 8 Feb 2024 15:10:17 -0700 Subject: [PATCH 72/93] Disable broken rustdoc links --- graphics/src/text/editor.rs | 2 +- src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index e1be3ff6b4..eba7f1e8f6 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -38,7 +38,7 @@ impl Editor { self.internal().editor.with_buffer(f) } - /// Returns the buffer of the [`Paragraph`]. + /// Returns the buffer of the `Paragraph`. pub fn buffer(&self) -> &cosmic_text::Buffer { match self.internal().editor.buffer_ref() { cosmic_text::BufferRef::Owned(buffer) => buffer, diff --git a/src/lib.rs b/src/lib.rs index 1b5bfe7b43..aa46198435 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -143,7 +143,7 @@ //! 1. Draw the resulting user interface. //! //! # Usage -//! The [`Application`] trait should get you started quickly, +//! The `Application` trait should get you started quickly, //! streamlining all the process described above! //! //! [Elm]: https://elm-lang.org/ From 484ac7be751d4cc6d87ef384562f0c137d86dfbb Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 8 Feb 2024 18:32:11 -0500 Subject: [PATCH 73/93] chore: update accesskit --- accessibility/src/id.rs | 33 ++++++++++++++------------------- examples/sctk_todos/Cargo.toml | 2 +- examples/todos/Cargo.toml | 2 +- sctk/src/application.rs | 7 +++---- sctk/src/event_loop/adapter.rs | 9 +++------ sctk/src/event_loop/mod.rs | 13 +++++-------- widget/src/checkbox.rs | 10 ++++------ widget/src/toggler.rs | 10 ++++------ winit/src/application.rs | 23 +++++++---------------- winit/src/multi_window.rs | 7 ++++--- 10 files changed, 46 insertions(+), 70 deletions(-) diff --git a/accessibility/src/id.rs b/accessibility/src/id.rs index d012f4da19..752e51c192 100644 --- a/accessibility/src/id.rs +++ b/accessibility/src/id.rs @@ -1,12 +1,12 @@ //! Widget and Window IDs. +use std::borrow; 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), + Window(u64), Widget(Id), } @@ -20,8 +20,8 @@ pub enum A11yId { // } // } -impl From for A11yId { - fn from(id: NonZeroU128) -> Self { +impl From for A11yId { + fn from(id: u64) -> Self { Self::Window(id) } } @@ -46,8 +46,8 @@ impl IdEq for A11yId { impl From for A11yId { fn from(value: accesskit::NodeId) -> Self { - let val = u128::from(value.0); - if val > u64::MAX as u128 { + let val = u64::from(value.0); + if val > u32::MAX as u64 { Self::Window(value.0) } else { Self::Widget(Id::from(val as u64)) @@ -110,13 +110,11 @@ impl From for Id { } // Not meant to be used directly -impl From for NonZeroU128 { - fn from(val: Id) -> NonZeroU128 { +impl From for u64 { + fn from(val: Id) -> u64 { match &val.0 { - Internal::Unique(id) => NonZeroU128::try_from(*id as u128).unwrap(), - Internal::Custom(id, _) => { - NonZeroU128::try_from(*id as u128).unwrap() - } + Internal::Unique(id) => *id, + Internal::Custom(id, _) => *id, // 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(_) => { @@ -136,14 +134,11 @@ impl ToString for Id { } } -// XXX WIndow IDs are made unique by adding u64::MAX to them +// XXX WIndow IDs are made unique by adding u32::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() +pub fn window_node_id() -> u64 { + u32::MAX as u64 + + NEXT_WINDOW_ID.fetch_add(1, atomic::Ordering::Relaxed) as u64 } // TODO refactor to make panic impossible? diff --git a/examples/sctk_todos/Cargo.toml b/examples/sctk_todos/Cargo.toml index fc1b9d0b8f..2797de7ef0 100644 --- a/examples/sctk_todos/Cargo.toml +++ b/examples/sctk_todos/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" publish = false [dependencies] -iced = { path = "../..", default-features=false, features = ["async-std", "wayland", "debug"] } +iced = { path = "../..", default-features=false, features = ["async-std", "wayland", "debug", "a11y"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" iced_core.workspace = true diff --git a/examples/todos/Cargo.toml b/examples/todos/Cargo.toml index 5926a39a7f..6ce3bbcb49 100644 --- a/examples/todos/Cargo.toml +++ b/examples/todos/Cargo.toml @@ -8,7 +8,7 @@ publish = false [dependencies] iced.workspace = true iced_core.workspace = true -iced.features = ["async-std", "debug", "winit"] +iced.features = ["async-std", "debug", "winit", "a11y"] once_cell.workspace = true serde = { version = "1.0", features = ["derive"] } diff --git a/sctk/src/application.rs b/sctk/src/application.rs index 065f386d94..27b47fb9d3 100644 --- a/sctk/src/application.rs +++ b/sctk/src/application.rs @@ -1347,8 +1347,9 @@ where ); let focus = focus .filter(|f_id| window_tree.contains(f_id)) - .map(|id| id.into()); - adapter.adapter.update(TreeUpdate { + .map(|id| id.into()) + .unwrap_or_else(|| tree.root); + adapter.adapter.update_if_active(|| TreeUpdate { nodes: window_tree.into(), tree: Some(tree), focus, @@ -1459,8 +1460,6 @@ where Action::Increment => todo!(), Action::HideTooltip => todo!(), Action::ShowTooltip => todo!(), - Action::InvalidateTree => todo!(), - Action::LoadInlineTextBoxes => todo!(), Action::ReplaceSelectedText => todo!(), Action::ScrollBackward => todo!(), Action::ScrollDown => todo!(), diff --git a/sctk/src/event_loop/adapter.rs b/sctk/src/event_loop/adapter.rs index a185ad9172..ae662f3cac 100644 --- a/sctk/src/event_loop/adapter.rs +++ b/sctk/src/event_loop/adapter.rs @@ -2,10 +2,7 @@ use crate::sctk_event::ActionRequestEvent; use iced_accessibility::{accesskit, accesskit_unix}; use sctk::reexports::client::protocol::wl_surface::WlSurface; use sctk::reexports::client::Proxy; -use std::{ - num::NonZeroU128, - sync::{Arc, Mutex}, -}; +use std::sync::{Arc, Mutex}; pub enum A11yWrapper { Enabled, @@ -13,7 +10,7 @@ pub enum A11yWrapper { } pub struct IcedSctkAdapter { - pub(crate) id: NonZeroU128, + pub(crate) id: u64, pub(crate) adapter: accesskit_unix::Adapter, } @@ -22,7 +19,7 @@ pub struct IcedSctkActionHandler { pub(crate) event_list: Arc>>, } impl accesskit::ActionHandler for IcedSctkActionHandler { - fn do_action(&self, request: accesskit::ActionRequest) { + fn do_action(&mut self, request: accesskit::ActionRequest) { let mut event_list = self.event_list.lock().unwrap(); event_list.push(A11yWrapper::Event( crate::sctk_event::ActionRequestEvent { diff --git a/sctk/src/event_loop/mod.rs b/sctk/src/event_loop/mod.rs index e728dd7b95..976891599d 100644 --- a/sctk/src/event_loop/mod.rs +++ b/sctk/src/event_loop/mod.rs @@ -259,9 +259,6 @@ where let event_list = self.a11y_events.clone(); adapter::IcedSctkAdapter { adapter: Adapter::new( - app_id.unwrap_or_else(|| String::from("None")), - "Iced".to_string(), - env!("CARGO_PKG_VERSION").to_string(), move || { event_list .lock() @@ -272,18 +269,18 @@ where node.set_name(name); } let node = node.build(&mut NodeClassSet::lock_global()); + let root = NodeId(node_id); TreeUpdate { - nodes: vec![(NodeId(node_id), node)], - tree: Some(Tree::new(NodeId(node_id))), - focus: None, + nodes: vec![(root, node)], + tree: Some(Tree::new(root)), + focus: root, } }, Box::new(adapter::IcedSctkActionHandler { wl_surface: surface.clone(), event_list: self.a11y_events.clone(), }), - ) - .unwrap(), + ), id: node_id, } } diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index 410f2ee629..c6f1fd26cc 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -392,9 +392,7 @@ where cursor: mouse::Cursor, ) -> iced_accessibility::A11yTree { use iced_accessibility::{ - accesskit::{ - Action, CheckedState, NodeBuilder, NodeId, Rect, Role, - }, + accesskit::{Action, Checked, NodeBuilder, NodeId, Rect, Role}, A11yNode, A11yTree, }; @@ -435,10 +433,10 @@ where } None => {} } - node.set_checked_state(if self.is_checked { - CheckedState::True + node.set_checked(if self.is_checked { + Checked::True } else { - CheckedState::False + Checked::False }); if is_hovered { node.set_hovered(); diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index fba9d63b96..be0880d4dd 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -419,9 +419,7 @@ where cursor: mouse::Cursor, ) -> iced_accessibility::A11yTree { use iced_accessibility::{ - accesskit::{ - Action, CheckedState, NodeBuilder, NodeId, Rect, Role, - }, + accesskit::{Action, Checked, NodeBuilder, NodeId, Rect, Role}, A11yNode, A11yTree, }; @@ -462,10 +460,10 @@ where } None => {} } - node.set_checked_state(if self.is_toggled { - CheckedState::True + node.set_checked(if self.is_toggled { + Checked::True } else { - CheckedState::False + Checked::False }); if is_hovered { node.set_hovered(); diff --git a/winit/src/application.rs b/winit/src/application.rs index 36a39d5cc6..e473177577 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -387,10 +387,11 @@ async fn run_instance( let mut node = NodeBuilder::new(Role::Window); node.set_name(title.clone()); let node = node.build(&mut iced_accessibility::accesskit::NodeClassSet::lock_global()); + let root = NodeId(node_id); TreeUpdate { - nodes: vec![(NodeId(node_id), node)], - tree: Some(Tree::new(NodeId(node_id))), - focus: None, + nodes: vec![(root, node)], + tree: Some(Tree::new(root)), + focus: root, } }, proxy.clone(), @@ -421,17 +422,6 @@ 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) => { match message { UserEventWrapper::Message(m) => messages.push(m), @@ -617,8 +607,9 @@ async fn run_instance( // TODO maybe optimize this? let focus = focus .filter(|f_id| window_tree.contains(f_id)) - .map(|id| id.into()); - adapter.update(TreeUpdate { + .map(|id| id.into()) + .unwrap_or_else(|| tree.root); + adapter.update_if_active(|| TreeUpdate { nodes: window_tree.into(), tree: Some(tree), focus, diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 88992bc137..6fe4657620 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -377,10 +377,11 @@ async fn run_instance( let mut node = NodeBuilder::new(Role::Window); node.set_name(title.clone()); let node = node.build(&mut iced_accessibility::accesskit::NodeClassSet::lock_global()); + let root = NodeId(node_id); TreeUpdate { - nodes: vec![(NodeId(node_id), node)], - tree: Some(Tree::new(NodeId(node_id))), - focus: None, + nodes: vec![(root, node)], + tree: Some(Tree::new(root)), + focus: root, } }, proxy.clone(), From 8b7b0bf54516c4b28cc38d328a01946b3800eec7 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 9 Feb 2024 09:04:48 -0700 Subject: [PATCH 74/93] Remove unnecessary redraw request This was particularly visible on Redox where there is no vsync, but also causes unnecessary redraws on Linux --- winit/src/application.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/winit/src/application.rs b/winit/src/application.rs index e473177577..9a36cc395f 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -533,8 +533,6 @@ async fn run_instance( mouse_interaction = new_mouse_interaction; } - window.request_redraw(); - redraw_pending = false; let physical_size = state.physical_size(); From ba4731f04dbc577f01f4a1ac88ea7f4bb5540e3f Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Fri, 12 Jan 2024 22:57:52 -0800 Subject: [PATCH 75/93] Add `show_window_menu` action Winit currently supports this only on Windows and Wayland. This requests that a context menu is shown at the cursor position, like the menu normally triggered by right clicking the title bar. This is important for implementing client side decorations with Iced widgets. --- runtime/src/window.rs | 5 +++++ runtime/src/window/action.rs | 9 +++++++++ winit/src/application.rs | 8 ++++++++ winit/src/multi_window.rs | 15 +++++++++++++++ 4 files changed, 37 insertions(+) diff --git a/runtime/src/window.rs b/runtime/src/window.rs index 217b6a00c0..8416cc4b71 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -179,6 +179,11 @@ pub fn change_level(id: Id, level: Level) -> Command { Command::single(command::Action::Window(Action::ChangeLevel(id, level))) } +/// Show window menu at cursor position. +pub fn show_window_menu(id: Id) -> Command { + Command::single(command::Action::Window(Action::ShowWindowMenu(id))) +} + /// Fetches an identifier unique to the window, provided by the underlying windowing system. This is /// not to be confused with [`Id`]. pub fn fetch_id( diff --git a/runtime/src/window/action.rs b/runtime/src/window/action.rs index 8b5325692c..dc168c5d4b 100644 --- a/runtime/src/window/action.rs +++ b/runtime/src/window/action.rs @@ -79,6 +79,11 @@ pub enum Action { GainFocus(Id), /// Change the window [`Level`]. ChangeLevel(Id, Level), + /// Show window menu at cursor position. + /// + /// ## Platform-specific + /// Android / iOS / macOS / Orbital / Wayland / Web / X11: Unsupported. + ShowWindowMenu(Id), /// Fetch the raw identifier unique to the window. FetchId(Id, Box T + 'static>), /// Change the window [`Icon`]. @@ -137,6 +142,7 @@ impl Action { } Self::GainFocus(id) => Action::GainFocus(id), Self::ChangeLevel(id, level) => Action::ChangeLevel(id, level), + Self::ShowWindowMenu(id) => Action::ShowWindowMenu(id), Self::FetchId(id, o) => { Action::FetchId(id, Box::new(move |s| f(o(s)))) } @@ -193,6 +199,9 @@ impl fmt::Debug for Action { Self::ChangeLevel(id, level) => { write!(f, "Action::ChangeLevel({id:?}, {level:?})") } + Self::ShowWindowMenu(id) => { + write!(f, "Action::ShowWindowMenu({id:?})") + } Self::FetchId(id, _) => write!(f, "Action::FetchId({id:?})"), Self::ChangeIcon(id, _icon) => { write!(f, "Action::ChangeIcon({id:?})") diff --git a/winit/src/application.rs b/winit/src/application.rs index 9a36cc395f..fc3e67f829 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -1003,6 +1003,14 @@ pub fn run_command( window::Action::ChangeLevel(_id, level) => { window.set_window_level(conversion::window_level(level)); } + window::Action::ShowWindowMenu(_id) => { + if let mouse::Cursor::Available(point) = state.cursor() { + window.show_window_menu(winit::dpi::LogicalPosition { + x: point.x, + y: point.y, + }); + } + } window::Action::FetchId(_id, tag) => { proxy .send_event(UserEventWrapper::Message(tag(window diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 6fe4657620..75b9c705e5 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -5,6 +5,7 @@ mod window_manager; 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; @@ -1272,6 +1273,20 @@ fn run_command( .set_window_level(conversion::window_level(level)); } } + window::Action::ShowWindowMenu(id) => { + if let Some(window) = window_manager.get_mut(id) { + if let mouse::Cursor::Available(point) = + window.state.cursor() + { + window.raw.show_window_menu( + winit::dpi::LogicalPosition { + x: point.x, + y: point.y, + }, + ); + } + } + } window::Action::FetchId(id, tag) => { if let Some(window) = window_manager.get_mut(id) { proxy From 268e21076e1cb06af9142aa193dcc8647bf0eb60 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Mon, 12 Feb 2024 09:03:59 -0700 Subject: [PATCH 76/93] sctk: add command to set maximize state --- sctk/src/commands/window.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/sctk/src/commands/window.rs b/sctk/src/commands/window.rs index 9f3bb0dd52..dc591cdd73 100644 --- a/sctk/src/commands/window.rs +++ b/sctk/src/commands/window.rs @@ -55,6 +55,18 @@ pub fn start_drag_window(id: window::Id) -> Command { )) } +pub fn maximize(id: window::Id, maximized: bool) -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::Window( + if maximized { + wayland::window::Action::Maximize { id } + } else { + wayland::window::Action::UnsetMaximize { id } + }, + )), + )) +} + pub fn toggle_maximize(id: window::Id) -> Command { Command::single(command::Action::PlatformSpecific( platform_specific::Action::Wayland(wayland::Action::Window( From 831fa81e1f47b210306255a796e7e7b7dda1a287 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 13 Feb 2024 14:18:10 -0500 Subject: [PATCH 77/93] fix(sctk): send close event instead of close requested when a window is closed --- sctk/src/sctk_event.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sctk/src/sctk_event.rs b/sctk/src/sctk_event.rs index f67cd8e241..06f54badff 100755 --- a/sctk/src/sctk_event.rs +++ b/sctk/src/sctk_event.rs @@ -627,7 +627,7 @@ impl SctkEvent { .map(|id| { iced_runtime::core::Event::Window( id.inner(), - window::Event::CloseRequested, + window::Event::Closed, ) }) .into_iter() From 53a8cc143c2499f3b68379a4939b30732c0efa6a Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 13 Feb 2024 19:26:23 -0500 Subject: [PATCH 78/93] fix: autosize surface layout Autosized surfaces perform the layout step to get the size and then again when building the interface, but sometimes the calculated size is not enough space when used as a bound, so we need to add a tiny amount to the calculated size. This also makes the event loop timeout duration configurable. Viewport physical size is calculated directly from the logical size now as well in iced-sctk to avoid inconsistencies that resulted from recalculating the logical size after using it to calculate the physical size. --- graphics/src/viewport.rs | 17 ++++ sctk/src/application.rs | 191 +++++++++++++++++---------------------- sctk/src/settings.rs | 4 + src/settings.rs | 1 + 4 files changed, 107 insertions(+), 106 deletions(-) diff --git a/graphics/src/viewport.rs b/graphics/src/viewport.rs index 5792555df7..4d31f2d343 100644 --- a/graphics/src/viewport.rs +++ b/graphics/src/viewport.rs @@ -12,6 +12,23 @@ pub struct Viewport { } impl Viewport { + /// Creates a new [`Viewport`] with the given logical dimensions and scale factor + pub fn with_logical_size(size: Size, scale_factor: f64) -> Viewport { + let physical_size = Size::new( + (size.width as f64 * scale_factor).ceil() as u32, + (size.height as f64 * scale_factor).ceil() as u32, + ); + Viewport { + physical_size, + logical_size: size, + scale_factor, + projection: Transformation::orthographic( + physical_size.width, + physical_size.height, + ), + } + } + /// Creates a new [`Viewport`] with the given physical dimensions and scale /// factor. pub fn with_physical_size(size: Size, scale_factor: f64) -> Viewport { diff --git a/sctk/src/application.rs b/sctk/src/application.rs index 27b47fb9d3..7c10decfd9 100644 --- a/sctk/src/application.rs +++ b/sctk/src/application.rs @@ -3,7 +3,7 @@ use crate::sctk_event::ActionRequestEvent; use crate::{ clipboard::Clipboard, commands::{layer_surface::get_layer_surface, window::get_window}, - dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}, + dpi::{LogicalPosition, PhysicalPosition}, error::{self, Error}, event_loop::{ control_flow::ControlFlow, proxy, state::SctkState, SctkEventLoop, @@ -43,14 +43,8 @@ use sctk::{ seat::{keyboard::Modifiers, pointer::PointerEventKind}, }; use std::{ - borrow::BorrowMut, - collections::HashMap, - hash::Hash, - marker::PhantomData, - os::raw::c_void, - ptr::NonNull, - sync::{Arc, Mutex}, - time::Duration, + collections::HashMap, hash::Hash, marker::PhantomData, os::raw::c_void, + ptr::NonNull, time::Duration, }; use wayland_backend::client::ObjectId; use wayland_protocols::wp::viewporter::client::wp_viewport::WpViewport; @@ -65,7 +59,7 @@ use iced_runtime::{ wayland::{data_device::DndIcon, popup}, }, }, - core::{mouse::Interaction, Color, Point, Renderer, Size}, + core::{mouse::Interaction, Color, Point, Size}, multi_window::Program, system, user_interface, window::Id as SurfaceId, @@ -74,9 +68,9 @@ use iced_runtime::{ use iced_style::application::{self, StyleSheet}; use itertools::Itertools; use raw_window_handle::{ - DisplayHandle, HandleError, HasDisplayHandle, HasRawDisplayHandle, - HasRawWindowHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle, - WaylandDisplayHandle, WaylandWindowHandle, WindowHandle, + DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, + RawDisplayHandle, RawWindowHandle, WaylandDisplayHandle, + WaylandWindowHandle, WindowHandle, }; use std::mem::ManuallyDrop; @@ -116,11 +110,11 @@ pub struct SurfaceDisplayWrapper { impl HasDisplayHandle for SurfaceDisplayWrapper { fn display_handle(&self) -> Result { - let mut ptr = self.backend.display_ptr() as *mut c_void; + let ptr = self.backend.display_ptr() as *mut c_void; let Some(ptr) = NonNull::new(ptr) else { return Err(HandleError::Unavailable); }; - let mut display_handle = WaylandDisplayHandle::new(ptr); + let display_handle = WaylandDisplayHandle::new(ptr); Ok(unsafe { DisplayHandle::borrow_raw(RawDisplayHandle::Wayland(display_handle)) }) @@ -298,6 +292,7 @@ where init_command, exit_on_close_request, qh, + settings.control_flow_timeout, )); let mut context = task::Context::from_waker(task::noop_waker_ref()); @@ -352,6 +347,7 @@ async fn run_instance( init_command: Command, exit_on_close_request: bool, queue_handle: QueueHandle::Message>>, + wait: Option, ) -> Result<(), Error> where A: Application + 'static, @@ -527,6 +523,12 @@ where compositor.configure_surface(&mut c_surface, configure.new_size.0.unwrap().get(), configure.new_size.1.unwrap().get()); state.surface = Some(c_surface); } + if let Some((w, h, _, is_dirty)) = auto_size_surfaces.get_mut(id) { + *is_dirty = first || *w != configure.new_size.0.map(|w| w.get()).unwrap_or_default() || *h != configure.new_size.1.map(|h| h.get()).unwrap_or_default(); + state.set_logical_size(*w as f32, *h as f32); + } else { + state.set_logical_size(configure.new_size.0.unwrap().get() as f32 , configure.new_size.1.unwrap().get() as f32); + } if first { let user_interface = build_user_interface( &application, @@ -541,14 +543,6 @@ where ); interfaces.insert(id.inner(), user_interface); } - if let Some((w, h, _, dirty)) = auto_size_surfaces.get_mut(id) { - if *w == configure.new_size.0.unwrap().get() && *h == configure.new_size.1.unwrap().get() { - *dirty = false; - } else { - continue; - } - } - state.set_logical_size(configure.new_size.0.unwrap().get() as f64 , configure.new_size.1.unwrap().get() as f64); } } crate::sctk_event::WindowEventVariant::ScaleFactorChanged(sf, viewport) => { @@ -607,6 +601,15 @@ where compositor.configure_surface(&mut c_surface, configure.new_size.0, configure.new_size.1); state.surface = Some(c_surface); }; + if let Some((w, h, _, is_dirty)) = auto_size_surfaces.get_mut(id) { + *is_dirty = first || *w != configure.new_size.0 || *h != configure.new_size.1; + state.set_logical_size(*w as f32, *h as f32); + } else { + state.set_logical_size( + configure.new_size.0 as f32, + configure.new_size.1 as f32, + ); + } if first { let user_interface = build_user_interface( &application, @@ -621,19 +624,6 @@ where ); interfaces.insert(id.inner(), user_interface); } - if let Some((w, h, _, dirty)) = auto_size_surfaces.get_mut(id) { - if *w == configure.new_size.0 && *h == configure.new_size.1 { - *dirty = false; - } else { - continue; - } - } - if let Some(state) = states.get_mut(&id.inner()) { - state.set_logical_size( - configure.new_size.0 as f64, - configure.new_size.1 as f64, - ); - } } } LayerSurfaceEventVariant::ScaleFactorChanged(sf, viewport) => { @@ -658,8 +648,6 @@ where backend: backend.clone(), wl_surface })); - - } PopupEventVariant::Done => { if let Some(surface_id) = surface_ids.remove(&wl_surface.id()) { @@ -683,6 +671,15 @@ where state.surface = Some(c_surface); } + if let Some((w, h, _, is_dirty)) = auto_size_surfaces.get_mut(id) { + *is_dirty |= first || *w != configure.width as u32 || *h != configure.height as u32; + state.set_logical_size(*w as f32, *h as f32); + } else { + state.set_logical_size( + configure.width as f32, + configure.height as f32, + ); + }; if first { let user_interface = build_user_interface( &application, @@ -697,33 +694,20 @@ where ); interfaces.insert(id.inner(), user_interface); } - if let Some((w, h, _, dirty)) = auto_size_surfaces.get_mut(id) { - if *w == configure.width as u32 && *h == configure.height as u32 { - *dirty = false; - } else { - continue; - } - } - state.set_logical_size( - configure.width as f64, - configure.height as f64, - ); } } PopupEventVariant::RepositionionedPopup { .. } => {} PopupEventVariant::Size(width, height) => { if let Some(id) = surface_ids.get(&wl_surface.id()) { if let Some(state) = states.get_mut(&id.inner()) { - state.set_logical_size( - width as f64, - height as f64, - ); - } - if let Some((w, h, _, dirty)) = auto_size_surfaces.get_mut(id) { - if *w == width && *h == height { - *dirty = false; + if let Some((w, h, _, is_dirty)) = auto_size_surfaces.get_mut(id) { + *is_dirty = *w != width || *h != height; + state.set_logical_size(*w as f32, *h as f32); } else { - continue; + state.set_logical_size( + width as f32, + height as f32, + ); } } } @@ -791,7 +775,7 @@ where interfaces.insert(id.inner(), user_interface); } - state.set_logical_size(configure.new_size.0 as f64 , configure.new_size.1 as f64); + state.set_logical_size(configure.new_size.0 as f32 , configure.new_size.1 as f32); } } @@ -854,8 +838,8 @@ where let bounds = node.bounds(); let (w, h) = ( - (bounds.width.round()) as u32, - (bounds.height.round()) as u32, + (bounds.width.ceil()) as u32, + (bounds.height.ceil()) as u32, ); if w == 0 || h == 0 { error!("Dnd surface has zero size, ignoring"); @@ -883,7 +867,7 @@ where wrapper, ); state.surface = Some(c_surface); - state.set_logical_size(w as f64, h as f64); + state.set_logical_size(w as f32, h as f32); let mut user_interface = build_user_interface( &application, user_interface::Cache::default(), @@ -1182,8 +1166,8 @@ where *dirty || { let Size { width, height } = s.logical_size(); - width.round() as u32 != *w - || height.round() as u32 + width.ceil() as u32 != *w + || height.ceil() as u32 != *h } }) @@ -1246,10 +1230,14 @@ where }); } if !sent_control_flow { - let mut wait_500_ms = Instant::now(); - wait_500_ms += Duration::from_millis(250); - _ = control_sender - .start_send(ControlFlow::WaitUntil(wait_500_ms)); + if let Some(d) = wait { + let mut wait_until = Instant::now(); + wait_until += d; + _ = control_sender + .start_send(ControlFlow::WaitUntil(wait_until)); + } else { + _ = control_sender.start_send(ControlFlow::Wait); + } } redraw_pending = false; } @@ -1358,7 +1346,7 @@ where if state.viewport_changed() { let physical_size = state.physical_size(); - let logical_size = state.logical_size(); + let mut logical_size = state.logical_size(); compositor.configure_surface( &mut comp_surface, physical_size.width, @@ -1366,6 +1354,11 @@ where ); debug.layout_started(); + // XXX must add a small number to the autosize surface size here + if auto_size_surfaces.contains_key(&native_id) { + logical_size.width += 0.001; + logical_size.height += 0.001; + } user_interface = user_interface .relayout(logical_size, &mut renderer); debug.layout_finished(); @@ -1557,14 +1550,13 @@ where .layout(&mut tree, renderer, &limits) .bounds() .size(); - // XXX add a small number to make sure it doesn't get truncated... let (w, h) = ( - (bounds.width.round()) as u32, - (bounds.height.round()) as u32, + (bounds.width.ceil()).max(1.0) as u32, + (bounds.height.ceil()).max(1.0) as u32, ); let dirty = dirty - || w != size.width.round() as u32 - || h != size.height.round() as u32 + || w != size.width.ceil() as u32 + || h != size.height.ceil() as u32 || w != auto_size_w || h != auto_size_h; @@ -1597,7 +1589,11 @@ where }; } - Size::new(w as f32, h as f32) + // XXX must add a small amount to the size. + // Layout seems to sometimes build the interface slightly + // differently when given a size versus just limits + // this is problematic for autosize surfaces that rely on the size previously calculated + Size::new(w as f32 + 0.001, h as f32 + 0.001) } else { size }; @@ -1722,26 +1718,19 @@ where } /// Sets the logical [`Size`] of the [`Viewport`] of the [`State`]. - pub fn set_logical_size(&mut self, w: f64, h: f64) { + pub fn set_logical_size(&mut self, w: f32, h: f32) { let old_size = self.viewport.logical_size(); if !approx_eq!(f32, w as f32, old_size.width, F32Margin::default()) || !approx_eq!(f32, h as f32, old_size.height, F32Margin::default()) { - let logical_size = LogicalSize::::new(w, h); - let physical_size: PhysicalSize = - logical_size.to_physical(self.scale_factor()); + let logical_size = Size::::new(w, h); self.viewport_changed = true; - self.viewport = Viewport::with_physical_size( - Size { - width: physical_size.width, - height: physical_size.height, - }, - self.scale_factor(), - ); + self.viewport = + Viewport::with_logical_size(logical_size, self.scale_factor()); if let Some(wp_viewport) = self.wp_viewport.as_ref() { wp_viewport.set_destination( - logical_size.width.round() as i32, - logical_size.height.round() as i32, + logical_size.width.ceil() as i32, + logical_size.height.ceil() as i32, ); } } @@ -1761,25 +1750,15 @@ where ) { self.viewport_changed = true; let logical_size = self.viewport.logical_size(); - let logical_size = LogicalSize::::new( - logical_size.width as f64, - logical_size.height as f64, - ); self.surface_scale_factor = scale_factor; - let physical_size: PhysicalSize = logical_size.to_physical( - self.application_scale_factor * self.surface_scale_factor, - ); - self.viewport = Viewport::with_physical_size( - Size { - width: physical_size.width, - height: physical_size.height, - }, + self.viewport = Viewport::with_logical_size( + logical_size, self.application_scale_factor * self.surface_scale_factor, ); if let Some(wp_viewport) = self.wp_viewport.as_ref() { wp_viewport.set_destination( - logical_size.width.round() as i32, - logical_size.height.round() as i32, + logical_size.width.ceil() as i32, + logical_size.height.ceil() as i32, ); } } @@ -2100,7 +2079,7 @@ where let mut tree = Tree::new(e.as_widget()); let node = Widget::layout(e.as_widget(), &mut tree, renderer, &builder.size_limits); let bounds = node.bounds(); - let (w, h) = ((bounds.width.round()) as u32, (bounds.height.round()) as u32); + let (w, h) = ((bounds.width.ceil()).max(1.0) as u32, (bounds.height.ceil()).max(1.0) as u32); auto_size_surfaces.insert(SurfaceIdWrapper::LayerSurface(builder.id), (w, h, builder.size_limits, false)); builder.size = Some((Some(bounds.width as u32), Some(bounds.height as u32))); } @@ -2120,7 +2099,7 @@ where let mut tree = Tree::new(e.as_widget()); let node = Widget::layout(e.as_widget(), &mut tree, renderer, &builder.size_limits); let bounds = node.bounds(); - let (w, h) = ((bounds.width.round()) as u32, (bounds.height.round()) as u32); + let (w, h) = ((bounds.width.ceil()).max(1.0) as u32, (bounds.height.ceil()).max(1.0) as u32); auto_size_surfaces.insert(SurfaceIdWrapper::Window(builder.window_id), (w, h, builder.size_limits, false)); builder.size = (bounds.width as u32, bounds.height as u32); } @@ -2140,7 +2119,7 @@ where let mut tree = Tree::new(e.as_widget()); let node = Widget::layout( e.as_widget(), &mut tree, renderer, &popup.positioner.size_limits); let bounds = node.bounds(); - let (w, h) = ((bounds.width.round()) as u32, (bounds.height.round()) as u32); + let (w, h) = (bounds.width.ceil().max(1.0) as u32, bounds.height.ceil().max(1.0) as u32); auto_size_surfaces.insert(SurfaceIdWrapper::Popup(popup.id), (w, h, popup.positioner.size_limits, false)); popup.positioner.size = Some((w, h)); } @@ -2162,7 +2141,7 @@ where command::Action::PlatformSpecific(platform_specific::Action::Wayland(platform_specific::wayland::Action::SessionLock(session_lock_action))) => { proxy.send_event(Event::SessionLock(session_lock_action)); } - _ => {} + _ => {} }; None } diff --git a/sctk/src/settings.rs b/sctk/src/settings.rs index f3299b06a8..8fd5456a59 100644 --- a/sctk/src/settings.rs +++ b/sctk/src/settings.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use iced_runtime::command::platform_specific::wayland::{ layer_surface::SctkLayerSurfaceSettings, window::SctkWindowSettings, }; @@ -16,6 +18,8 @@ pub struct Settings { pub surface: InitialSurface, /// whether the application should exit on close of all windows pub exit_on_close_request: bool, + /// event loop dispatch timeout + pub control_flow_timeout: Option, } #[derive(Debug, Clone)] diff --git a/src/settings.rs b/src/settings.rs index c45922f5f4..d257455a7f 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -190,6 +190,7 @@ impl From> for iced_sctk::Settings { flags: settings.flags, exit_on_close_request: settings.exit_on_close_request, ptr_theme: None, + control_flow_timeout: Some(std::time::Duration::from_millis(250)), } } } From d2dc82733de4d6d009f0ab9499076d9d26cd2805 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 21 Feb 2024 15:19:19 +0100 Subject: [PATCH 79/93] feat(slider): add breakpoints --- style/src/slider.rs | 9 +++++++ style/src/theme.rs | 3 +++ widget/src/slider.rs | 56 +++++++++++++++++++++++++++++++++++++++----- 3 files changed, 62 insertions(+), 6 deletions(-) diff --git a/style/src/slider.rs b/style/src/slider.rs index 4c2de9c9c8..d1e427028c 100644 --- a/style/src/slider.rs +++ b/style/src/slider.rs @@ -8,6 +8,15 @@ pub struct Appearance { pub rail: Rail, /// The appearance of the [`Handle`] of the slider. pub handle: Handle, + /// The appearance of breakpoints. + pub breakpoint: Breakpoint, +} + +/// The appearance of slider breakpoints. +#[derive(Debug, Clone, Copy)] +pub struct Breakpoint { + /// The color of the slider breakpoint. + pub color: Color, } /// The appearance of a slider rail diff --git a/style/src/theme.rs b/style/src/theme.rs index 179ba37252..1dd87ed150 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -501,6 +501,9 @@ impl slider::StyleSheet for Theme { border_color: palette.primary.base.color, ..handle }, + breakpoint: slider::Breakpoint { + color: palette.background.weak.text, + }, } } Slider::Custom(custom) => custom.active(self), diff --git a/widget/src/slider.rs b/widget/src/slider.rs index 7640c908af..ebdc4f7994 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -9,15 +9,15 @@ 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, + Border, Clipboard, Color, Element, Layout, Length, Pixels, Point, + Rectangle, Shell, Size, Widget, }; use std::ops::RangeInclusive; use iced_renderer::core::{border::Radius, Degrees, Radians}; pub use iced_style::slider::{ - Appearance, Handle, HandleShape, Rail, StyleSheet, + Appearance, Handle, HandleShape, Rail, RailBackground, StyleSheet, }; #[cfg(feature = "a11y")] @@ -48,6 +48,7 @@ use std::borrow::Cow; /// /// ![Slider drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/slider.png?raw=true) #[allow(missing_debug_implementations)] +#[must_use] pub struct Slider<'a, T, Message, Theme = crate::Theme> where Theme: StyleSheet, @@ -62,6 +63,7 @@ where range: RangeInclusive, step: T, value: T, + breakpoints: &'a [T], on_change: Box Message + 'a>, on_release: Option, width: Length, @@ -113,6 +115,7 @@ where value, range, step: T::from(1), + breakpoints: &[], on_change: Box::new(on_change), on_release: None, width: Length::Fill, @@ -121,12 +124,20 @@ where } } + /// Defines breakpoints to visibly mark on the slider. + /// + /// The slider will gravitate towards a breakpoint when near it. + pub fn breakpoints(mut self, breakpoints: &'a [T]) -> Self { + self.breakpoints = breakpoints; + self + } + /// Sets the release message of the [`Slider`]. /// This is called when the mouse is released from the slider. /// /// Typically, the user's interaction with the slider is finished when this message is produced. /// This is useful if you need to spawn a long-running task from the slider's result, where - /// the default on_change message could create too many events. + /// the default `on_change` message could create too many events. pub fn on_release(mut self, on_release: Message) -> Self { self.on_release = Some(on_release); self @@ -266,6 +277,7 @@ where tree.state.downcast_ref::(), self.value, &self.range, + self.breakpoints, theme, &self.style, ); @@ -475,6 +487,7 @@ pub fn draw( state: &State, value: T, range: &RangeInclusive, + breakpoints: &[T], theme: &Theme, style: &Theme::Style, ) where @@ -492,6 +505,7 @@ pub fn draw( } else { theme.active(style) }; + let border_width = style .handle .border_width @@ -539,8 +553,38 @@ pub fn draw( let rail_y = bounds.y + bounds.height / 2.0; + // Draw the breakpoint indicators beneath the slider. + const BREAKPOINT_WIDTH: f32 = 2.0; + for &value in breakpoints { + let value: f64 = value.into(); + let offset = if range_start >= range_end { + 0.0 + } else { + (bounds.width - BREAKPOINT_WIDTH) * (value as f32 - range_start) + / (range_end - range_start) + }; + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + offset, + y: rail_y + 6.0, + width: BREAKPOINT_WIDTH, + height: 8.0, + }, + border: Border { + radius: 0.0.into(), + width: 0.0, + color: Color::TRANSPARENT, + }, + ..renderer::Quad::default() + }, + crate::core::Background::Color(style.breakpoint.color), + ); + } + match style.rail.colors { - iced_style::slider::RailBackground::Pair(l, r) => { + RailBackground::Pair(l, r) => { // rail renderer.fill_quad( renderer::Quad { @@ -571,7 +615,7 @@ pub fn draw( r, ); } - iced_style::slider::RailBackground::Gradient { + RailBackground::Gradient { mut gradient, auto_angle, } => renderer.fill_quad( From b1a684e102d418ff33d03e3cad9187f79a411f91 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Wed, 14 Feb 2024 13:21:11 -0800 Subject: [PATCH 80/93] sctk/subsurface: Avoid unnecessary subsurface commits if unchanged --- sctk/src/subsurface_widget.rs | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/sctk/src/subsurface_widget.rs b/sctk/src/subsurface_widget.rs index 699ae9e5a6..016e0fb5e6 100644 --- a/sctk/src/subsurface_widget.rs +++ b/sctk/src/subsurface_widget.rs @@ -299,6 +299,7 @@ impl SubsurfaceState { wl_subsurface, wp_viewport, wl_buffer: None, + bounds: None, } } @@ -337,6 +338,7 @@ pub(crate) struct SubsurfaceInstance { wl_subsurface: WlSubsurface, wp_viewport: WpViewport, wl_buffer: Option, + bounds: Option>, } impl SubsurfaceInstance { @@ -348,12 +350,14 @@ impl SubsurfaceInstance { dmabuf: Option<&ZwpLinuxDmabufV1>, qh: &QueueHandle>, ) { + let buffer_changed; let buffer = match self.wl_buffer.take() { Some(buffer) if SubsurfaceBuffer::for_buffer(&buffer) == Some(&info.buffer) => { // Same buffer is already attached to this subsurface. Don't create new `wl_buffer`. + buffer_changed = false; buffer } buffer => { @@ -362,6 +366,7 @@ impl SubsurfaceInstance { } if let Some(buffer) = info.buffer.create_buffer(shm, dmabuf, qh) { + buffer_changed = true; buffer } else { // TODO log error @@ -372,17 +377,26 @@ impl SubsurfaceInstance { }; // XXX scale factor? - self.wl_subsurface - .set_position(info.bounds.x as i32, info.bounds.y as i32); - self.wp_viewport.set_destination( - info.bounds.width as i32, - info.bounds.height as i32, - ); - self.wl_surface.attach(Some(&buffer), 0, 0); - self.wl_surface.damage(0, 0, i32::MAX, i32::MAX); - self.wl_surface.commit(); + let bounds_changed = self.bounds != Some(info.bounds); + // wlroots seems to have issues changing buffer without running this + if bounds_changed || buffer_changed { + self.wl_subsurface + .set_position(info.bounds.x as i32, info.bounds.y as i32); + self.wp_viewport.set_destination( + info.bounds.width as i32, + info.bounds.height as i32, + ); + } + if buffer_changed { + self.wl_surface.attach(Some(&buffer), 0, 0); + self.wl_surface.damage(0, 0, i32::MAX, i32::MAX); + } + if buffer_changed || bounds_changed { + self.wl_surface.commit(); + } self.wl_buffer = Some(buffer); + self.bounds = Some(info.bounds); } } From e83c288e2b5846cc8c6edae6f4819c38d452d12b Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Mon, 19 Feb 2024 16:57:27 -0800 Subject: [PATCH 81/93] sctk/subsurface: Cache `wl_buffer`s Creating a new `wl_buffer` each frame seems to perform poorly. We can instead keep a cache of `wl_buffer`s we have created from a `BufferSource`. --- sctk/src/application.rs | 4 +- sctk/src/event_loop/mod.rs | 1 + sctk/src/subsurface_widget.rs | 176 +++++++++++++++++++++++++--------- 3 files changed, 136 insertions(+), 45 deletions(-) diff --git a/sctk/src/application.rs b/sctk/src/application.rs index 7c10decfd9..6e61f7ad04 100644 --- a/sctk/src/application.rs +++ b/sctk/src/application.rs @@ -898,7 +898,7 @@ where ); let subsurfaces = crate::subsurface_widget::take_subsurfaces(); - if let Some(subsurface_state) = subsurface_state.as_ref() { + if let Some(subsurface_state) = subsurface_state.as_mut() { subsurface_state.update_subsurfaces( &state.wrapper.wl_surface, &mut state.subsurfaces, @@ -1385,7 +1385,7 @@ where // Update subsurfaces based on what view requested. let subsurfaces = crate::subsurface_widget::take_subsurfaces(); - if let Some(subsurface_state) = subsurface_state.as_ref() { + if let Some(subsurface_state) = subsurface_state.as_mut() { subsurface_state.update_subsurfaces( &state.wrapper.wl_surface, &mut state.subsurfaces, diff --git a/sctk/src/event_loop/mod.rs b/sctk/src/event_loop/mod.rs index 976891599d..d711fdf6af 100644 --- a/sctk/src/event_loop/mod.rs +++ b/sctk/src/event_loop/mod.rs @@ -333,6 +333,7 @@ where wl_shm, wp_dmabuf, qh: self.state.queue_handle.clone(), + buffers: HashMap::new(), }), &self.state, &mut control_flow, diff --git a/sctk/src/subsurface_widget.rs b/sctk/src/subsurface_widget.rs index 016e0fb5e6..ba187ca584 100644 --- a/sctk/src/subsurface_widget.rs +++ b/sctk/src/subsurface_widget.rs @@ -8,11 +8,15 @@ use crate::core::{ }; use std::{ cell::RefCell, + collections::HashMap, + fmt::Debug, future::Future, + hash::{Hash, Hasher}, mem, os::unix::io::{AsFd, OwnedFd}, pin::Pin, - sync::Arc, + ptr, + sync::{Arc, Mutex, Weak}, task, }; @@ -103,7 +107,15 @@ struct SubsurfaceBufferInner { pub struct SubsurfaceBuffer(Arc); pub struct BufferData { - source: SubsurfaceBuffer, + source: WeakBufferSource, + // This reference is held until the surface `release`s the buffer + subsurface_buffer: Mutex>, +} + +impl BufferData { + fn for_buffer(buffer: &WlBuffer) -> Option<&Self> { + buffer.data::() + } } /// Future signalled when subsurface buffer is released @@ -163,7 +175,10 @@ impl SubsurfaceBuffer { buf.format, qh, BufferData { - source: self.clone(), + source: WeakBufferSource(Arc::downgrade( + &self.0.source, + )), + subsurface_buffer: Mutex::new(Some(self.clone())), }, ); pool.destroy(); @@ -192,7 +207,10 @@ impl SubsurfaceBuffer { zwp_linux_buffer_params_v1::Flags::empty(), qh, BufferData { - source: self.clone(), + source: WeakBufferSource(Arc::downgrade( + &self.0.source, + )), + subsurface_buffer: Mutex::new(Some(self.clone())), }, )) } else { @@ -201,10 +219,6 @@ impl SubsurfaceBuffer { } } } - - fn for_buffer(buffer: &WlBuffer) -> Option<&Self> { - Some(&buffer.data::()?.source) - } } impl PartialEq for SubsurfaceBuffer { @@ -255,17 +269,38 @@ impl Dispatch for SctkState { _: &mut SctkState, _: &WlBuffer, event: wl_buffer::Event, - _: &BufferData, + data: &BufferData, _: &Connection, _: &QueueHandle>, ) { match event { - wl_buffer::Event::Release => {} + wl_buffer::Event::Release => { + // Release reference to `SubsurfaceBuffer` + data.subsurface_buffer.lock().unwrap().take(); + } _ => unreachable!(), } } } +#[doc(hidden)] +#[derive(Clone, Debug)] +pub(crate) struct WeakBufferSource(Weak); + +impl PartialEq for WeakBufferSource { + fn eq(&self, rhs: &Self) -> bool { + Weak::ptr_eq(&self.0, &rhs.0) + } +} + +impl Eq for WeakBufferSource {} + +impl Hash for WeakBufferSource { + fn hash(&self, state: &mut H) { + ptr::hash::(self.0.as_ptr(), state) + } +} + // create wl_buffer from BufferSource (avoid create_immed?) // release #[doc(hidden)] @@ -276,9 +311,10 @@ pub struct SubsurfaceState { pub wl_shm: WlShm, pub wp_dmabuf: Option, pub qh: QueueHandle>, + pub(crate) buffers: HashMap>, } -impl SubsurfaceState { +impl SubsurfaceState { fn create_subsurface(&self, parent: &WlSurface) -> SubsurfaceInstance { let wl_surface = self .wl_compositor @@ -305,11 +341,20 @@ impl SubsurfaceState { // Update `subsurfaces` from `view_subsurfaces` pub(crate) fn update_subsurfaces( - &self, + &mut self, parent: &WlSurface, subsurfaces: &mut Vec, view_subsurfaces: &[SubsurfaceInfo], ) { + // Remove cached `wl_buffers` for any `BufferSource`s that no longer exist. + self.buffers.retain(|k, v| { + let retain = k.0.strong_count() > 0; + if !retain { + v.iter().for_each(|b| b.destroy()); + } + retain + }); + // If view requested fewer subsurfaces than there currently are, // destroy excess. if view_subsurfaces.len() < subsurfaces.len() { @@ -323,14 +368,54 @@ impl SubsurfaceState { for (subsurface_data, subsurface) in view_subsurfaces.iter().zip(subsurfaces.iter_mut()) { - subsurface.attach_and_commit( - subsurface_data, - &self.wl_shm, - self.wp_dmabuf.as_ref(), - &self.qh, - ); + subsurface.attach_and_commit(subsurface_data, self); } } + + // Cache `wl_buffer` for use when `BufferSource` is used in future + // (Avoid creating wl_buffer each buffer swap) + fn insert_cached_wl_buffer(&mut self, buffer: WlBuffer) { + let source = BufferData::for_buffer(&buffer).unwrap().source.clone(); + self.buffers.entry(source).or_default().push(buffer); + } + + // Gets a cached `wl_buffer` for the `SubsurfaceBuffer`, if any. And stores `SubsurfaceBuffer` + // reference to be releated on `wl_buffer` release. + // + // If `wl_buffer` isn't released, it is destroyed instead. + fn get_cached_wl_buffer( + &mut self, + subsurface_buffer: &SubsurfaceBuffer, + ) -> Option { + let buffers = self.buffers.get_mut(&WeakBufferSource( + Arc::downgrade(&subsurface_buffer.0.source), + ))?; + while let Some(buffer) = buffers.pop() { + let mut subsurface_buffer_ref = buffer + .data::() + .unwrap() + .subsurface_buffer + .lock() + .unwrap(); + if subsurface_buffer_ref.is_none() { + *subsurface_buffer_ref = Some(subsurface_buffer.clone()); + drop(subsurface_buffer_ref); + return Some(buffer); + } else { + buffer.destroy(); + } + } + None + } +} + +impl Drop for SubsurfaceState { + fn drop(&mut self) { + self.buffers + .values() + .flatten() + .for_each(|buffer| buffer.destroy()); + } } pub(crate) struct SubsurfaceInstance { @@ -343,36 +428,41 @@ pub(crate) struct SubsurfaceInstance { impl SubsurfaceInstance { // TODO correct damage? no damage/commit if unchanged? - fn attach_and_commit( + fn attach_and_commit( &mut self, info: &SubsurfaceInfo, - shm: &WlShm, - dmabuf: Option<&ZwpLinuxDmabufV1>, - qh: &QueueHandle>, + state: &mut SubsurfaceState, ) { let buffer_changed; - let buffer = match self.wl_buffer.take() { - Some(buffer) - if SubsurfaceBuffer::for_buffer(&buffer) - == Some(&info.buffer) => - { - // Same buffer is already attached to this subsurface. Don't create new `wl_buffer`. - buffer_changed = false; - buffer + + let old_buffer = self.wl_buffer.take(); + let old_buffer_data = + old_buffer.as_ref().and_then(|b| BufferData::for_buffer(&b)); + let buffer = if old_buffer_data.is_some_and(|b| { + b.subsurface_buffer.lock().unwrap().as_ref() == Some(&info.buffer) + }) { + // Same "BufferSource" is already attached to this subsurface. Don't create new `wl_buffer`. + buffer_changed = false; + old_buffer.unwrap() + } else { + if let Some(old_buffer) = old_buffer { + state.insert_cached_wl_buffer(old_buffer); } - buffer => { - if let Some(buffer) = buffer { - buffer.destroy(); - } - if let Some(buffer) = info.buffer.create_buffer(shm, dmabuf, qh) - { - buffer_changed = true; - buffer - } else { - // TODO log error - self.wl_surface.attach(None, 0, 0); - return; - } + + buffer_changed = true; + + if let Some(buffer) = state.get_cached_wl_buffer(&info.buffer) { + buffer + } else if let Some(buffer) = info.buffer.create_buffer( + &state.wl_shm, + state.wp_dmabuf.as_ref(), + &state.qh, + ) { + buffer + } else { + // TODO log error + self.wl_surface.attach(None, 0, 0); + return; } }; From bf5e6b579e04d5ebc3489cc4c2844cd66bdb1e79 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Mon, 19 Feb 2024 16:58:47 -0800 Subject: [PATCH 82/93] examples/sctk_subsurface_gst: Cache `BufferSource` in `BufferRef` qdata Similar to `waylandsink`. Allows us to avoid creating a buffer source (and ultimately `wl_buffer`) for every buffer swap. --- examples/sctk_subsurface_gst/src/pipewire.rs | 118 +++++++++++++------ 1 file changed, 83 insertions(+), 35 deletions(-) diff --git a/examples/sctk_subsurface_gst/src/pipewire.rs b/examples/sctk_subsurface_gst/src/pipewire.rs index 302f19c4b9..2b2accfd58 100644 --- a/examples/sctk_subsurface_gst/src/pipewire.rs +++ b/examples/sctk_subsurface_gst/src/pipewire.rs @@ -1,8 +1,48 @@ use drm_fourcc::{DrmFourcc, DrmModifier}; +use gst::glib::{self, translate::IntoGlib}; use gst::prelude::*; use iced::futures::{executor::block_on, SinkExt}; -use iced_sctk::subsurface_widget::{Dmabuf, Plane, SubsurfaceBuffer}; -use std::{os::unix::io::BorrowedFd, sync::Arc, thread}; +use iced_sctk::subsurface_widget::{ + BufferSource, Dmabuf, Plane, SubsurfaceBuffer, +}; +use std::{ffi::c_void, os::unix::io::BorrowedFd, sync::Arc, thread}; + +// Store a reference to the `BufferSource` in the data assocaited with the `BufferRef`. +// So the `BufferSource` can be re-used, instead of dupping fds and creating a new +// `wl_buffer` each buffer swap. +// +// See https://gitlab.freedesktop.org/gstreamer/gstreamer/-/blob/main/subprojects/gst-plugins-bad/gst-libs/gst/wayland/gstwlbuffer.c +// for information about how `waylandsink` does this. +fn get_buffer_source(buffer: &gst::BufferRef) -> Option> { + let buffer_source_quark = glib::Quark::from_str("SctkBufferSource"); + unsafe { + let data = gst::ffi::gst_mini_object_get_qdata( + buffer.upcast_ref().as_mut_ptr(), + buffer_source_quark.into_glib(), + ); + if data.is_null() { + None + } else { + Arc::increment_strong_count(data as *const BufferSource); + Some(Arc::from_raw(data as *const BufferSource)) + } + } +} + +fn set_buffer_source(buffer: &gst::BufferRef, source: Arc) { + let buffer_source_quark = glib::Quark::from_str("SctkBufferSource"); + unsafe extern "C" fn destroy_buffer_source(data: *mut c_void) { + Arc::from_raw(data); + } + unsafe { + gst::ffi::gst_mini_object_set_qdata( + buffer.upcast_ref().as_mut_ptr(), + buffer_source_quark.into_glib(), + Arc::into_raw(source) as *mut c_void, + Some(destroy_buffer_source), + ); + } +} #[derive(Debug, Clone)] pub enum Event { @@ -55,42 +95,50 @@ fn pipewire_thread( let buffer = sample.buffer().unwrap(); let meta = buffer.meta::().unwrap(); - let planes = (0..meta.n_planes()) - .map(|plane_idx| { - let memory = buffer - .memory(plane_idx) - .unwrap() - .downcast_memory::() - .unwrap(); - - // TODO avoid dup? - let fd = unsafe { BorrowedFd::borrow_raw(memory.fd()) } - .try_clone_to_owned() - .unwrap(); - - Plane { - fd, - plane_idx, - offset: meta.offset()[plane_idx as usize] as u32, - stride: meta.stride()[plane_idx as usize] as u32, - } - }) - .collect(); - - let dmabuf = Dmabuf { - width: meta.width() as i32, - height: meta.height() as i32, - planes, - // TODO should use dmabuf protocol to get supported formats, - // convert if needed. - format: DrmFourcc::Argb8888 as u32, - //format: DrmFourcc::Nv12 as u32, - // TODO modifier negotiation - modifier: DrmModifier::Linear.into(), + let buffer_source = if let Some(buffer_source) = get_buffer_source(buffer) { + buffer_source + } else { + let planes = (0..meta.n_planes()) + .map(|plane_idx| { + let memory = buffer + .memory(plane_idx) + .unwrap() + .downcast_memory::() + .unwrap(); + + // TODO avoid dup? + let fd = unsafe { BorrowedFd::borrow_raw(memory.fd()) } + .try_clone_to_owned() + .unwrap(); + + Plane { + fd, + plane_idx, + offset: meta.offset()[plane_idx as usize] as u32, + stride: meta.stride()[plane_idx as usize] as u32, + } + }) + .collect(); + + let dmabuf = Dmabuf { + width: meta.width() as i32, + height: meta.height() as i32, + planes, + // TODO should use dmabuf protocol to get supported formats, + // convert if needed. + format: DrmFourcc::Argb8888 as u32, + //format: DrmFourcc::Nv12 as u32, + // TODO modifier negotiation + modifier: DrmModifier::Linear.into(), + }; + + let buffer_source = Arc::new(BufferSource::from(dmabuf)); + set_buffer_source(buffer, buffer_source.clone()); + buffer_source }; let (buffer, new_subsurface_release) = - SubsurfaceBuffer::new(Arc::new(dmabuf.into())); + SubsurfaceBuffer::new(buffer_source); block_on(sender.send(Event::Frame(buffer))).unwrap(); // Wait for server to release other buffer From 3231920eaa41212a4fbd037694d3aa42f1c45b52 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Tue, 20 Feb 2024 15:39:44 -0800 Subject: [PATCH 83/93] Handle frame callbacks for subsurfaces, and `commit` parent surface If the main surface is occluded completely by opaque subsurfaces, it may not receive `frame` events. So we need to request frame events for all subsurfaces as well. Additionally, with "synchronized" subsurfaces, we need to `commit` the parent surface for subsurface changes to take effect. Fixes issues with subsurfaces updating slowly, or only when mouse moved under some circumstances. --- sctk/src/application.rs | 50 ++++++++++++++++++--------- sctk/src/event_loop/mod.rs | 2 +- sctk/src/event_loop/state.rs | 2 +- sctk/src/handlers/compositor.rs | 3 +- sctk/src/handlers/shell/layer.rs | 3 +- sctk/src/handlers/shell/xdg_popup.rs | 2 +- sctk/src/handlers/shell/xdg_window.rs | 2 +- sctk/src/sctk_event.rs | 2 +- sctk/src/subsurface_widget.rs | 3 +- 9 files changed, 45 insertions(+), 24 deletions(-) diff --git a/sctk/src/application.rs b/sctk/src/application.rs index 6e61f7ad04..6e1c9b6fe8 100644 --- a/sctk/src/application.rs +++ b/sctk/src/application.rs @@ -1091,7 +1091,7 @@ where || state.viewport_changed; if redraw_pending || needs_update { state.set_needs_redraw( - state.frame.is_some() || needs_update, + state.frame_pending || needs_update, ); state.set_first(false); } @@ -1259,14 +1259,6 @@ where let state = states.get_mut(&id.inner()); Some((*id, interface, state)) }) { - // request a new frame - // NOTE Ashley: this is done here only after a redraw for now instead of the event handler. - // Otherwise cpu goes up in the running application as well as in cosmic-comp - if let Some(surface) = state.frame.take() { - surface.frame(&queue_handle, surface.clone()); - surface.commit(); - } - let Some(mut comp_surface) = state.surface.take() else { error!("missing surface!"); continue; @@ -1403,6 +1395,13 @@ where let _ = interfaces.insert(native_id.inner(), user_interface); + if state.frame_pending { + // request a new frame + state.wrapper.wl_surface.frame( + &queue_handle, + state.wrapper.wl_surface.clone(), + ); + } let _ = compositor.present( &mut renderer, &mut comp_surface, @@ -1410,6 +1409,9 @@ where state.background_color(), &debug.overlay(), ); + // Need commit to get frame event, and update subsurfaces, even if main surface wasn't changed + state.wrapper.wl_surface.commit(); + state.frame_pending = false; state.surface = Some(comp_surface); debug.render_finished(); } @@ -1479,13 +1481,21 @@ where IcedSctkEvent::A11ySurfaceCreated(surface_id, adapter) => { adapters.insert(surface_id.inner(), adapter); } - IcedSctkEvent::Frame(surface) => { + IcedSctkEvent::Frame(surface, time) => { if let Some(id) = surface_ids.get(&surface.id()) { if let Some(state) = states.get_mut(&id.inner()) { - // TODO set this to the callback? - state.set_frame(Some(surface)); + state.set_frame(time); + continue; } } + if let Some(state) = states.values_mut().find(|state| { + state + .subsurfaces + .iter() + .any(|subsurface| subsurface.wl_surface == surface) + }) { + state.set_frame(time); + } } IcedSctkEvent::Subcompositor(state) => { subsurface_state = Some(state); @@ -1622,7 +1632,9 @@ where theme: ::Theme, appearance: application::Appearance, application: PhantomData, - frame: Option, + // Time of last frame event, or 0 + frame_pending: bool, + last_frame_time: u32, needs_redraw: bool, first: bool, wp_viewport: Option, @@ -1661,7 +1673,8 @@ where theme, appearance, application: PhantomData, - frame: None, + frame_pending: false, + last_frame_time: 0, needs_redraw: false, first: true, wp_viewport: None, @@ -1680,8 +1693,13 @@ where self.needs_redraw } - pub(crate) fn set_frame(&mut self, frame: Option) { - self.frame = frame; + fn set_frame(&mut self, time: u32) { + // If we get frame events from mulitple subsurface, should have same time. So ignore if + // time isn't newer. + if time == 0 || time > self.last_frame_time { + self.frame_pending = true; + self.last_frame_time = time; + } } pub(crate) fn first(&self) -> bool { diff --git a/sctk/src/event_loop/mod.rs b/sctk/src/event_loop/mod.rs index d711fdf6af..955206111e 100644 --- a/sctk/src/event_loop/mod.rs +++ b/sctk/src/event_loop/mod.rs @@ -528,7 +528,7 @@ where for event in frame_event_back_buffer.drain(..) { sticky_exit_callback( - IcedSctkEvent::Frame(event), + IcedSctkEvent::Frame(event.0, event.1), &self.state, &mut control_flow, &mut callback, diff --git a/sctk/src/event_loop/state.rs b/sctk/src/event_loop/state.rs index 9f3fafaf0d..bcc6c2f94b 100644 --- a/sctk/src/event_loop/state.rs +++ b/sctk/src/event_loop/state.rs @@ -296,7 +296,7 @@ pub struct SctkState { /// A sink for window and device events that is being filled during dispatching /// event loop and forwarded downstream afterwards. pub(crate) sctk_events: Vec, - pub(crate) frame_events: Vec, + pub(crate) frame_events: Vec<(WlSurface, u32)>, /// pending user events pub pending_user_events: Vec>, diff --git a/sctk/src/handlers/compositor.rs b/sctk/src/handlers/compositor.rs index 797861258a..8c46af974e 100644 --- a/sctk/src/handlers/compositor.rs +++ b/sctk/src/handlers/compositor.rs @@ -26,7 +26,8 @@ impl CompositorHandler for SctkState { surface: &wl_surface::WlSurface, _time: u32, ) { - self.frame_events.push(surface.clone()); + // TODO time; map subsurface to parent:w + self.frame_events.push((surface.clone(), 0)); } fn transform_changed( diff --git a/sctk/src/handlers/shell/layer.rs b/sctk/src/handlers/shell/layer.rs index e45f5e9387..89c3ddfd32 100644 --- a/sctk/src/handlers/shell/layer.rs +++ b/sctk/src/handlers/shell/layer.rs @@ -72,7 +72,8 @@ impl LayerShellHandler for SctkState { ), id: layer.surface.wl_surface().clone(), }); - self.frame_events.push(layer.surface.wl_surface().clone()); + self.frame_events + .push((layer.surface.wl_surface().clone(), 0)); } } diff --git a/sctk/src/handlers/shell/xdg_popup.rs b/sctk/src/handlers/shell/xdg_popup.rs index 8be7166323..79deb0a549 100644 --- a/sctk/src/handlers/shell/xdg_popup.rs +++ b/sctk/src/handlers/shell/xdg_popup.rs @@ -36,7 +36,7 @@ impl PopupHandler for SctkState { SctkSurface::Popup(s) => s.clone(), }, }); - self.frame_events.push(popup.wl_surface().clone()); + self.frame_events.push((popup.wl_surface().clone(), 0)); } fn done( diff --git a/sctk/src/handlers/shell/xdg_window.rs b/sctk/src/handlers/shell/xdg_window.rs index bf0d6fb207..6b4505737b 100644 --- a/sctk/src/handlers/shell/xdg_window.rs +++ b/sctk/src/handlers/shell/xdg_window.rs @@ -104,7 +104,7 @@ impl WindowHandler for SctkState { ), id, }); - self.frame_events.push(wl_surface.clone()); + self.frame_events.push((wl_surface.clone(), 0)); } } diff --git a/sctk/src/sctk_event.rs b/sctk/src/sctk_event.rs index 06f54badff..f5370e7318 100755 --- a/sctk/src/sctk_event.rs +++ b/sctk/src/sctk_event.rs @@ -122,7 +122,7 @@ pub enum IcedSctkEvent { DndSurfaceCreated(WlSurface, DndIcon, SurfaceId), /// Frame callback event - Frame(WlSurface), + Frame(WlSurface, u32), Subcompositor(SubsurfaceState), } diff --git a/sctk/src/subsurface_widget.rs b/sctk/src/subsurface_widget.rs index ba187ca584..e28c74a2c8 100644 --- a/sctk/src/subsurface_widget.rs +++ b/sctk/src/subsurface_widget.rs @@ -419,7 +419,7 @@ impl Drop for SubsurfaceState { } pub(crate) struct SubsurfaceInstance { - wl_surface: WlSurface, + pub(crate) wl_surface: WlSurface, wl_subsurface: WlSubsurface, wp_viewport: WpViewport, wl_buffer: Option, @@ -482,6 +482,7 @@ impl SubsurfaceInstance { self.wl_surface.damage(0, 0, i32::MAX, i32::MAX); } if buffer_changed || bounds_changed { + self.wl_surface.frame(&state.qh, self.wl_surface.clone()); self.wl_surface.commit(); } From 7b7760f1613d434550556be68e99897b912da13e Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Mon, 26 Feb 2024 10:30:22 -0800 Subject: [PATCH 84/93] sctk_subsurface_gst: NV12 surface suppport; disabled Whether or not this works seems to depend on driver, or gstreamer version... --- examples/sctk_subsurface_gst/src/pipewire.rs | 40 ++++++++++++++++---- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/examples/sctk_subsurface_gst/src/pipewire.rs b/examples/sctk_subsurface_gst/src/pipewire.rs index 2b2accfd58..9933ee099d 100644 --- a/examples/sctk_subsurface_gst/src/pipewire.rs +++ b/examples/sctk_subsurface_gst/src/pipewire.rs @@ -7,6 +7,8 @@ use iced_sctk::subsurface_widget::{ }; use std::{ffi::c_void, os::unix::io::BorrowedFd, sync::Arc, thread}; +const USE_NV12: bool = false; + // Store a reference to the `BufferSource` in the data assocaited with the `BufferRef`. // So the `BufferSource` can be re-used, instead of dupping fds and creating a new // `wl_buffer` each buffer swap. @@ -63,20 +65,40 @@ fn pipewire_thread( ) { gst::init().unwrap(); - // `vapostproc` can be added to convert color format - // TODO had issue on smithay using NV12? let pipeline = gst::parse_launch(&format!( - "filesrc location={path} ! + "filesrc name=filesrc ! qtdemux ! h264parse ! vah264dec ! - vapostproc ! - video/x-raw(memory:DMABuf),format=BGRA ! + vapostproc name=postproc ! + capsfilter name=capfilter ! appsink name=sink", )) .unwrap() .dynamic_cast::() .unwrap(); + pipeline + .by_name("filesrc") + .unwrap() + .set_property("location", path); + + let format = if USE_NV12 { + /* + pipeline + .remove(&pipeline.by_name("postproc").unwrap()) + .unwrap(); + */ + gst_video::VideoFormat::Nv12 + } else { + gst_video::VideoFormat::Bgra + }; + pipeline.by_name("capfilter").unwrap().set_property( + "caps", + gst_video::VideoCapsBuilder::new() + .features(["memory:DMABuf"]) + .format(format) + .build(), + ); let appsink = pipeline .by_name("sink") @@ -120,14 +142,18 @@ fn pipewire_thread( }) .collect(); + let format = if USE_NV12 { + DrmFourcc::Nv12 + } else { + DrmFourcc::Argb8888 + }; let dmabuf = Dmabuf { width: meta.width() as i32, height: meta.height() as i32, planes, // TODO should use dmabuf protocol to get supported formats, // convert if needed. - format: DrmFourcc::Argb8888 as u32, - //format: DrmFourcc::Nv12 as u32, + format: format as u32, // TODO modifier negotiation modifier: DrmModifier::Linear.into(), }; From bf032f861b3bfc4027e1339803f68666ac68ba35 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Tue, 27 Feb 2024 09:36:09 -0800 Subject: [PATCH 85/93] sctk: Add `subsurface_ids` mapping subsurface to parent and offset --- sctk/src/application.rs | 21 +++++++++++---------- sctk/src/subsurface_widget.rs | 22 +++++++++++++++++++++- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/sctk/src/application.rs b/sctk/src/application.rs index 6e1c9b6fe8..3bfb034de4 100644 --- a/sctk/src/application.rs +++ b/sctk/src/application.rs @@ -270,6 +270,7 @@ where let auto_size_surfaces = HashMap::new(); let surface_ids = Default::default(); + let subsurface_ids = Default::default(); let (mut sender, receiver) = mpsc::unbounded::>(); let (control_sender, mut control_receiver) = mpsc::unbounded(); @@ -284,6 +285,7 @@ where receiver, control_sender, surface_ids, + subsurface_ids, auto_size_surfaces, // display, // context, @@ -342,6 +344,7 @@ async fn run_instance( mut receiver: mpsc::UnboundedReceiver>, mut control_sender: mpsc::UnboundedSender, mut surface_ids: HashMap, + mut subsurface_ids: HashMap, mut auto_size_surfaces: HashMap, backend: wayland_backend::client::Backend, init_command: Command, @@ -900,7 +903,9 @@ where let subsurfaces = crate::subsurface_widget::take_subsurfaces(); if let Some(subsurface_state) = subsurface_state.as_mut() { subsurface_state.update_subsurfaces( + &mut subsurface_ids, &state.wrapper.wl_surface, + state.id, &mut state.subsurfaces, &subsurfaces, ); @@ -1379,7 +1384,9 @@ where crate::subsurface_widget::take_subsurfaces(); if let Some(subsurface_state) = subsurface_state.as_mut() { subsurface_state.update_subsurfaces( + &mut subsurface_ids, &state.wrapper.wl_surface, + state.id, &mut state.subsurfaces, &subsurfaces, ); @@ -1482,20 +1489,14 @@ where adapters.insert(surface_id.inner(), adapter); } IcedSctkEvent::Frame(surface, time) => { - if let Some(id) = surface_ids.get(&surface.id()) { + if let Some(id) = surface_ids + .get(&surface.id()) + .or_else(|| Some(&subsurface_ids.get(&surface.id())?.2)) + { if let Some(state) = states.get_mut(&id.inner()) { state.set_frame(time); - continue; } } - if let Some(state) = states.values_mut().find(|state| { - state - .subsurfaces - .iter() - .any(|subsurface| subsurface.wl_surface == surface) - }) { - state.set_frame(time); - } } IcedSctkEvent::Subcompositor(state) => { subsurface_state = Some(state); diff --git a/sctk/src/subsurface_widget.rs b/sctk/src/subsurface_widget.rs index e28c74a2c8..cc10832aba 100644 --- a/sctk/src/subsurface_widget.rs +++ b/sctk/src/subsurface_widget.rs @@ -1,5 +1,6 @@ // TODO z-order option? +use crate::application::SurfaceIdWrapper; use crate::core::{ layout::{self, Layout}, mouse, renderer, @@ -37,6 +38,7 @@ use sctk::{ Connection, Dispatch, Proxy, QueueHandle, }, }; +use wayland_backend::client::ObjectId; use wayland_protocols::wp::{ linux_dmabuf::zv1::client::{ zwp_linux_buffer_params_v1::{self, ZwpLinuxBufferParamsV1}, @@ -342,7 +344,9 @@ impl SubsurfaceState { // Update `subsurfaces` from `view_subsurfaces` pub(crate) fn update_subsurfaces( &mut self, + subsurface_ids: &mut HashMap, parent: &WlSurface, + parent_id: SurfaceIdWrapper, subsurfaces: &mut Vec, view_subsurfaces: &[SubsurfaceInfo], ) { @@ -368,7 +372,16 @@ impl SubsurfaceState { for (subsurface_data, subsurface) in view_subsurfaces.iter().zip(subsurfaces.iter_mut()) { - subsurface.attach_and_commit(subsurface_data, self); + subsurface.attach_and_commit( + parent_id, + subsurface_ids, + subsurface_data, + self, + ); + } + + if let Some(backend) = parent.backend().upgrade() { + subsurface_ids.retain(|k, _| backend.info(k.clone()).is_ok()); } } @@ -430,6 +443,8 @@ impl SubsurfaceInstance { // TODO correct damage? no damage/commit if unchanged? fn attach_and_commit( &mut self, + parent_id: SurfaceIdWrapper, + subsurface_ids: &mut HashMap, info: &SubsurfaceInfo, state: &mut SubsurfaceState, ) { @@ -486,6 +501,11 @@ impl SubsurfaceInstance { self.wl_surface.commit(); } + subsurface_ids.insert( + self.wl_surface.id(), + (info.bounds.x as i32, info.bounds.y as i32, parent_id), + ); + self.wl_buffer = Some(buffer); self.bounds = Some(info.bounds); } From b2c581f49233db7ec681484027bb14f5e9f964e7 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Wed, 14 Feb 2024 13:32:13 -0800 Subject: [PATCH 86/93] sctk_subsurface: Use two surfaces, handle button presses Useful for testing pointer input to subsurfaces. --- examples/sctk_subsurface/src/main.rs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/examples/sctk_subsurface/src/main.rs b/examples/sctk_subsurface/src/main.rs index c26034134a..34649400e0 100644 --- a/examples/sctk_subsurface/src/main.rs +++ b/examples/sctk_subsurface/src/main.rs @@ -26,6 +26,7 @@ struct SubsurfaceApp { pub enum Message { WaylandEvent(WaylandEvent), Wayland(wayland::Event), + Pressed(&'static str), } impl Application for SubsurfaceApp { @@ -65,16 +66,32 @@ impl Application for SubsurfaceApp { self.red_buffer = Some(buffer); } }, + Message::Pressed(side) => println!("{side} surface pressed"), } Command::none() } fn view(&self, _id: window::Id) -> Element { if let Some(buffer) = &self.red_buffer { - iced_sctk::subsurface_widget::Subsurface::new(1, 1, buffer) + iced::widget::row![ + iced::widget::button( + iced_sctk::subsurface_widget::Subsurface::new(1, 1, buffer) + .width(Length::Fill) + .height(Length::Fill) + ) .width(Length::Fill) .height(Length::Fill) - .into() + .on_press(Message::Pressed("left")), + iced::widget::button( + iced_sctk::subsurface_widget::Subsurface::new(1, 1, buffer) + .width(Length::Fill) + .height(Length::Fill) + ) + .width(Length::Fill) + .height(Length::Fill) + .on_press(Message::Pressed("right")) + ] + .into() } else { text("No subsurface").into() } From 982174af4c8a8796aacb020a965c611decba2918 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Tue, 27 Feb 2024 14:46:09 -0800 Subject: [PATCH 87/93] sctk: Map subsurface pointer events to parent surface, with offset --- sctk/src/application.rs | 34 ++++++++++++++++++++++++++++------ sctk/src/sctk_event.rs | 12 ++++++++++-- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/sctk/src/application.rs b/sctk/src/application.rs index 3bfb034de4..bcf9de0cdd 100644 --- a/sctk/src/application.rs +++ b/sctk/src/application.rs @@ -437,22 +437,30 @@ where variant, .. } => { + let mut offset = (0., 0.); let (state, _native_id) = match surface_ids .get(&variant.surface.id()) .and_then(|id| states.get_mut(&id.inner()).map(|state| (state, id))) { Some(s) => s, - None => continue, + None => { + if let Some((x_offset, y_offset, id)) = subsurface_ids.get(&variant.surface.id()) { + offset = (f64::from(*x_offset), f64::from(*y_offset)); + states.get_mut(&id.inner()).map(|state| (state, id)).unwrap() + } else { + continue + } + }, }; match variant.kind { PointerEventKind::Enter { .. } => { - state.set_cursor_position(Some(LogicalPosition { x: variant.position.0, y: variant.position.1 })); + state.set_cursor_position(Some(LogicalPosition { x: variant.position.0 + offset.0, y: variant.position.1 + offset.1 })); } PointerEventKind::Leave { .. } => { state.set_cursor_position(None); } PointerEventKind::Motion { .. } => { - state.set_cursor_position(Some(LogicalPosition { x: variant.position.0, y: variant.position.1 })); + state.set_cursor_position(Some(LogicalPosition { x: variant.position.0 + offset.0, y: variant.position.1 + offset.1 })); } PointerEventKind::Press { .. } | PointerEventKind::Release { .. } @@ -952,6 +960,7 @@ where &mut mods, &surface_ids, &destroyed_surface_ids, + &subsurface_ids, ) { runtime.broadcast(native_event, Status::Ignored); } @@ -1017,6 +1026,7 @@ where if event_is_for_surface( &sctk_events[i], object_id, + state, has_kbd_focus, ) { filtered_sctk.push(sctk_events.remove(i)); @@ -1034,6 +1044,7 @@ where &mut mods, &surface_ids, &destroyed_surface_ids, + &subsurface_ids, ) }) .collect(); @@ -2215,15 +2226,26 @@ where } // Determine if `SctkEvent` is for surface with given object id. -fn event_is_for_surface( +fn event_is_for_surface<'a, A, C>( evt: &SctkEvent, object_id: &ObjectId, + state: &State, has_kbd_focus: bool, -) -> bool { +) -> bool +where + A: Application + 'static, + ::Theme: StyleSheet, + C: Compositor, +{ match evt { SctkEvent::SeatEvent { id, .. } => &id.id() == object_id, SctkEvent::PointerEvent { variant, .. } => { - &variant.surface.id() == object_id + let event_object_id = variant.surface.id(); + &event_object_id == object_id + || state + .subsurfaces + .iter() + .any(|s| s.wl_surface.id() == event_object_id) } SctkEvent::KeyboardEvent { variant, .. } => match variant { KeyboardEventVariant::Leave(id) => &id.id() == object_id, diff --git a/sctk/src/sctk_event.rs b/sctk/src/sctk_event.rs index f5370e7318..3d9a3c6da3 100755 --- a/sctk/src/sctk_event.rs +++ b/sctk/src/sctk_event.rs @@ -393,6 +393,7 @@ impl SctkEvent { modifiers: &mut Modifiers, surface_ids: &HashMap, destroyed_surface_ids: &HashMap, + subsurface_ids: &HashMap, ) -> Vec { match self { // TODO Ashley: Platform specific multi-seat events? @@ -409,11 +410,18 @@ impl SctkEvent { )] } PointerEventKind::Motion { .. } => { + let offset = if let Some((x_offset, y_offset, _)) = + subsurface_ids.get(&variant.surface.id()) + { + (*x_offset, *y_offset) + } else { + (0, 0) + }; vec![iced_runtime::core::Event::Mouse( mouse::Event::CursorMoved { position: Point::new( - variant.position.0 as f32, - variant.position.1 as f32, + variant.position.0 as f32 + offset.0 as f32, + variant.position.1 as f32 + offset.1 as f32, ), }, )] From e1d1689ff1cdef26a42bf705c5325e86c223b249 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Mon, 4 Mar 2024 15:30:39 -0700 Subject: [PATCH 88/93] fix(multi_window): enable drag resize --- winit/src/application/drag_resize.rs | 6 +++--- winit/src/multi_window.rs | 16 ++++++++++++++++ winit/src/multi_window/window_manager.rs | 15 +++++++++++++++ 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/winit/src/application/drag_resize.rs b/winit/src/application/drag_resize.rs index ac4e3d063e..0584fe263a 100644 --- a/winit/src/application/drag_resize.rs +++ b/winit/src/application/drag_resize.rs @@ -5,13 +5,13 @@ pub fn event_func( window: &winit::window::Window, border_size: f64, ) -> Option< - impl FnMut(&winit::window::Window, &winit::event::WindowEvent) -> bool, + Box bool>, > { if window.drag_resize_window(ResizeDirection::East).is_ok() { // Keep track of cursor when it is within a resizeable border. let mut cursor_prev_resize_direction = None; - Some( + Some(Box::new( move |window: &winit::window::Window, window_event: &winit::event::WindowEvent| -> bool { @@ -50,7 +50,7 @@ pub fn event_func( false }, - ) + )) } else { None } diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 75b9c705e5..ec17b00f0a 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -1,4 +1,6 @@ //! Create interactive, native cross-platform applications for WGPU. +#[path = "application/drag_resize.rs"] +mod drag_resize; mod state; mod window_manager; @@ -151,6 +153,7 @@ where let should_main_be_visible = settings.window.visible; let exit_on_close_request = settings.window.exit_on_close_request; + let resize_border = settings.window.resize_border; let builder = conversion::window_settings( settings.window, @@ -207,6 +210,7 @@ where &application, &mut compositor, exit_on_close_request, + resize_border, ); let (mut event_sender, event_receiver) = mpsc::unbounded(); @@ -223,6 +227,7 @@ where init_command, window_manager, should_main_be_visible, + resize_border, )); let mut context = task::Context::from_waker(task::noop_waker_ref()); @@ -338,6 +343,7 @@ async fn run_instance( init_command: Command, mut window_manager: WindowManager, should_main_window_be_visible: bool, + resize_border: u32, ) where A: Application + 'static, E: Executor + 'static, @@ -451,6 +457,7 @@ async fn run_instance( &application, &mut compositor, exit_on_close_request, + resize_border, ); let logical_size = window.state.logical_size(); @@ -698,6 +705,15 @@ async fn run_instance( continue; }; + // Initiates a drag resize window state when found. + if let Some(func) = + window.drag_resize_window_func.as_mut() + { + if func(&window.raw, &window_event) { + continue; + } + } + if matches!( window_event, winit::event::WindowEvent::CloseRequested diff --git a/winit/src/multi_window/window_manager.rs b/winit/src/multi_window/window_manager.rs index 23f3c0ba0b..8b070da174 100644 --- a/winit/src/multi_window/window_manager.rs +++ b/winit/src/multi_window/window_manager.rs @@ -39,6 +39,7 @@ where application: &A, compositor: &mut C, exit_on_close_request: bool, + resize_border: u32, ) -> &mut Window { let state = State::new(application, id, &window); let viewport_version = state.viewport_version(); @@ -52,6 +53,11 @@ where let _ = self.aliases.insert(window.id(), id); + let drag_resize_window_func = super::drag_resize::event_func( + &window, + resize_border as f64 * window.scale_factor(), + ); + let _ = self.entries.insert( id, Window { @@ -59,6 +65,7 @@ where state, viewport_version, exit_on_close_request, + drag_resize_window_func, surface, renderer, mouse_interaction: mouse::Interaction::Idle, @@ -127,6 +134,14 @@ where pub state: State, pub viewport_version: u64, pub exit_on_close_request: bool, + pub drag_resize_window_func: Option< + Box< + dyn FnMut( + &winit::window::Window, + &winit::event::WindowEvent, + ) -> bool, + >, + >, pub mouse_interaction: mouse::Interaction, pub surface: C::Surface, pub renderer: A::Renderer, From 91d912d2d15de67cc194b64fd7095fa86c9ef328 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 23 Feb 2024 10:16:50 -0500 Subject: [PATCH 89/93] fix(sctk): broadcast events after update when broadcasting events for no specific surface, it should be done after update so that the runtime subscription is current --- sctk/src/application.rs | 58 ++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/sctk/src/application.rs b/sctk/src/application.rs index bcf9de0cdd..b32af23c48 100644 --- a/sctk/src/application.rs +++ b/sctk/src/application.rs @@ -940,35 +940,6 @@ where continue; } - let mut i = 0; - while i < sctk_events.len() { - let remove = matches!( - sctk_events[i], - SctkEvent::NewOutput { .. } - | SctkEvent::UpdateOutput { .. } - | SctkEvent::RemovedOutput(_) - | SctkEvent::SessionLocked - | SctkEvent::SessionLockFinished - | SctkEvent::SessionUnlocked - | SctkEvent::PopupEvent { .. } - | SctkEvent::LayerSurfaceEvent { .. } - | SctkEvent::WindowEvent { .. } - ); - if remove { - let event = sctk_events.remove(i); - for native_event in event.to_native( - &mut mods, - &surface_ids, - &destroyed_surface_ids, - &subsurface_ids, - ) { - runtime.broadcast(native_event, Status::Ignored); - } - } else { - i += 1; - } - } - if surface_ids.is_empty() && !messages.is_empty() { // Update application let pure_states: HashMap<_, _> = @@ -1258,6 +1229,35 @@ where redraw_pending = false; } + let mut i = 0; + while i < sctk_events.len() { + let remove = matches!( + sctk_events[i], + SctkEvent::NewOutput { .. } + | SctkEvent::UpdateOutput { .. } + | SctkEvent::RemovedOutput(_) + | SctkEvent::SessionLocked + | SctkEvent::SessionLockFinished + | SctkEvent::SessionUnlocked + | SctkEvent::PopupEvent { .. } + | SctkEvent::LayerSurfaceEvent { .. } + | SctkEvent::WindowEvent { .. } + ); + if remove { + let event = sctk_events.remove(i); + for native_event in event.to_native( + &mut mods, + &surface_ids, + &destroyed_surface_ids, + &subsurface_ids, + ) { + runtime.broadcast(native_event, Status::Ignored); + } + } else { + i += 1; + } + } + sctk_events.clear(); // clear the destroyed surfaces after they have been handled destroyed_surface_ids.clear(); From d06486a23835212f8263480ee8d239bcdd6071d2 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 5 Mar 2024 11:20:36 -0500 Subject: [PATCH 90/93] refactor(sctk): optional clipboard --- Cargo.toml | 4 ++- sctk/Cargo.toml | 3 +- sctk/src/application.rs | 30 ++++++++++---------- sctk/src/{ => clipboard}/clipboard.rs | 4 +++ sctk/src/clipboard/mod.rs | 41 +++++++++++++++++++++++++++ 5 files changed, 65 insertions(+), 17 deletions(-) rename sctk/src/{ => clipboard}/clipboard.rs (97%) create mode 100644 sctk/src/clipboard/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 0abc67abb6..417df6c127 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,8 +54,10 @@ advanced = [] a11y = ["iced_accessibility", "iced_core/a11y", "iced_widget/a11y", "iced_winit?/a11y", "iced_sctk?/a11y"] # Enables the winit shell. Conflicts with `wayland` and `glutin`. winit = ["iced_winit", "iced_accessibility?/accesskit_winit"] -# Enables the sctk shell. COnflicts with `winit` and `glutin`. +# Enables the sctk shell. Conflicts with `winit` and `glutin`. wayland = ["iced_sctk", "iced_widget/wayland", "iced_accessibility?/accesskit_unix", "iced_core/wayland"] +# Enables clipboard for iced_sctk +wayland-clipboard = ["iced_sctk?/clipboard"] [dependencies] iced_core.workspace = true diff --git a/sctk/Cargo.toml b/sctk/Cargo.toml index 4be53dd5c5..7eb18c8202 100644 --- a/sctk/Cargo.toml +++ b/sctk/Cargo.toml @@ -10,6 +10,7 @@ debug = ["iced_runtime/debug"] system = ["sysinfo"] application = [] a11y = ["iced_accessibility", "iced_runtime/a11y"] +clipboard = ["smithay-clipboard"] [dependencies] tracing = "0.1" @@ -22,7 +23,7 @@ enum-repr = "0.2" futures = "0.3" wayland-backend = {version = "0.3.1", features = ["client_system"]} float-cmp = "0.9" -smithay-clipboard = "0.6" +smithay-clipboard = { version = "0.7", optional = true } xkbcommon-dl = "0.4.1" xkbcommon = { version = "0.7", features = ["wayland"] } itertools = "0.12" diff --git a/sctk/src/application.rs b/sctk/src/application.rs index b32af23c48..19057be0a2 100644 --- a/sctk/src/application.rs +++ b/sctk/src/application.rs @@ -38,17 +38,7 @@ use iced_futures::{ }; use tracing::error; -use sctk::{ - reexports::client::{protocol::wl_surface::WlSurface, Proxy, QueueHandle}, - seat::{keyboard::Modifiers, pointer::PointerEventKind}, -}; -use std::{ - collections::HashMap, hash::Hash, marker::PhantomData, os::raw::c_void, - ptr::NonNull, time::Duration, -}; -use wayland_backend::client::ObjectId; -use wayland_protocols::wp::viewporter::client::wp_viewport::WpViewport; - +use iced_futures::core::Clipboard as IcedClipboard; use iced_graphics::{compositor, Compositor, Viewport}; use iced_runtime::{ clipboard, @@ -72,7 +62,17 @@ use raw_window_handle::{ RawDisplayHandle, RawWindowHandle, WaylandDisplayHandle, WaylandWindowHandle, WindowHandle, }; +use sctk::{ + reexports::client::{protocol::wl_surface::WlSurface, Proxy, QueueHandle}, + seat::{keyboard::Modifiers, pointer::PointerEventKind}, +}; use std::mem::ManuallyDrop; +use std::{ + collections::HashMap, hash::Hash, marker::PhantomData, os::raw::c_void, + ptr::NonNull, time::Duration, +}; +use wayland_backend::client::ObjectId; +use wayland_protocols::wp::viewporter::client::wp_viewport::WpViewport; use crate::subsurface_widget::{SubsurfaceInstance, SubsurfaceState}; @@ -523,7 +523,7 @@ where backend: backend.clone(), wl_surface }; - if matches!(simple_clipboard.state, crate::clipboard::State::Unavailable) { + if matches!(simple_clipboard.state(), crate::clipboard::State::Unavailable) { if let Ok(h) = wrapper.display_handle() { if let RawDisplayHandle::Wayland(mut h) = h.as_raw() { simple_clipboard = unsafe { Clipboard::connect(h.display.as_mut()) }; @@ -601,7 +601,7 @@ where backend: backend.clone(), wl_surface }; - if matches!(simple_clipboard.state, crate::clipboard::State::Unavailable) { + if matches!(simple_clipboard.state(), crate::clipboard::State::Unavailable) { if let Ok(h) = wrapper.display_handle() { if let RawDisplayHandle::Wayland(mut h) = h.as_raw() { simple_clipboard = unsafe { Clipboard::connect(h.display.as_mut()) }; @@ -2006,14 +2006,14 @@ where } command::Action::Clipboard(action) => match action { clipboard::Action::Read(s_to_msg) => { - if matches!(clipboard.state, crate::clipboard::State::Connected(_)) { + if matches!(clipboard.state(), crate::clipboard::State::Connected(_)) { let contents = clipboard.read(); let message = s_to_msg(contents); proxy.send_event(Event::Message(message)); } } clipboard::Action::Write(contents) => { - if matches!(clipboard.state, crate::clipboard::State::Connected(_)) { + if matches!(clipboard.state(), crate::clipboard::State::Connected(_)) { clipboard.write(contents) } } diff --git a/sctk/src/clipboard.rs b/sctk/src/clipboard/clipboard.rs similarity index 97% rename from sctk/src/clipboard.rs rename to sctk/src/clipboard/clipboard.rs index 74ab0c6c94..1b07100625 100644 --- a/sctk/src/clipboard.rs +++ b/sctk/src/clipboard/clipboard.rs @@ -28,6 +28,10 @@ impl Clipboard { } } + pub fn state(&self) -> State { + self.state + } + /// Creates a new [`Clipboard`] that isn't associated with a window. /// This clipboard will never contain a copied value. pub fn unconnected() -> Clipboard { diff --git a/sctk/src/clipboard/mod.rs b/sctk/src/clipboard/mod.rs new file mode 100644 index 0000000000..b2f47ed814 --- /dev/null +++ b/sctk/src/clipboard/mod.rs @@ -0,0 +1,41 @@ +#[cfg(feature = "clipboard")] +mod clipboard; + +#[cfg(not(feature = "clipboard"))] +mod clipboard { + use std::ffi::c_void; + /// A buffer for short-term storage and transfer within and between + /// applications. + #[allow(missing_debug_implementations)] + pub struct Clipboard; + + pub(crate) enum State { + Connected(()), + Unavailable, + } + + impl Clipboard { + pub unsafe fn connect(display: *mut c_void) -> Clipboard { + Clipboard + } + + pub fn state(&self) -> State { + State::Connected(()) + } + + /// Creates a new [`Clipboard`] + pub fn unconnected() -> Clipboard { + Clipboard + } + } +} + +impl iced_runtime::core::clipboard::Clipboard for Clipboard { + fn read(&self) -> Option { + None + } + + fn write(&mut self, contents: String) {} +} + +pub use clipboard::*; From c2d5d60a3d3d19f248f3d5c737de106c1ea5391c Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 5 Mar 2024 12:42:07 -0500 Subject: [PATCH 91/93] fix(sctk): clipboard dummy impl typo --- sctk/src/clipboard/mod.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sctk/src/clipboard/mod.rs b/sctk/src/clipboard/mod.rs index b2f47ed814..27c6dcc640 100644 --- a/sctk/src/clipboard/mod.rs +++ b/sctk/src/clipboard/mod.rs @@ -28,14 +28,14 @@ mod clipboard { Clipboard } } -} -impl iced_runtime::core::clipboard::Clipboard for Clipboard { - fn read(&self) -> Option { - None + impl iced_runtime::core::clipboard::Clipboard for Clipboard { + fn read(&self) -> Option { + None + } + + fn write(&mut self, _contents: String) {} } - - fn write(&mut self, contents: String) {} } pub use clipboard::*; From c17007d401ccd0debbf8865096659dcd3cbe053b Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 5 Mar 2024 12:47:35 -0500 Subject: [PATCH 92/93] fix: clipboard cleanup --- examples/sctk_todos/Cargo.toml | 2 +- sctk/src/clipboard/clipboard.rs | 4 ++-- sctk/src/clipboard/mod.rs | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/sctk_todos/Cargo.toml b/examples/sctk_todos/Cargo.toml index 2797de7ef0..b21a9c96d7 100644 --- a/examples/sctk_todos/Cargo.toml +++ b/examples/sctk_todos/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" publish = false [dependencies] -iced = { path = "../..", default-features=false, features = ["async-std", "wayland", "debug", "a11y"] } +iced = { path = "../..", default-features=false, features = ["async-std", "wayland", "debug", "a11y", "wayland-clipboard"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" iced_core.workspace = true diff --git a/sctk/src/clipboard/clipboard.rs b/sctk/src/clipboard/clipboard.rs index 1b07100625..69b9434160 100644 --- a/sctk/src/clipboard/clipboard.rs +++ b/sctk/src/clipboard/clipboard.rs @@ -28,8 +28,8 @@ impl Clipboard { } } - pub fn state(&self) -> State { - self.state + pub(crate) fn state(&self) -> &State { + &self.state } /// Creates a new [`Clipboard`] that isn't associated with a window. diff --git a/sctk/src/clipboard/mod.rs b/sctk/src/clipboard/mod.rs index 27c6dcc640..bb0496d026 100644 --- a/sctk/src/clipboard/mod.rs +++ b/sctk/src/clipboard/mod.rs @@ -15,12 +15,12 @@ mod clipboard { } impl Clipboard { - pub unsafe fn connect(display: *mut c_void) -> Clipboard { + pub unsafe fn connect(_display: *mut c_void) -> Clipboard { Clipboard } - pub fn state(&self) -> State { - State::Connected(()) + pub(crate) fn state(&self) -> &State { + &State::Connected(()) } /// Creates a new [`Clipboard`] @@ -33,7 +33,7 @@ mod clipboard { fn read(&self) -> Option { None } - + fn write(&mut self, _contents: String) {} } } From a85a3f0151ac862703b248cfce3374206e1b3cca Mon Sep 17 00:00:00 2001 From: Dominic Gerhauser Date: Thu, 7 Mar 2024 03:12:26 +0100 Subject: [PATCH 93/93] mousearea_scroll: init --- widget/src/mouse_area.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index 5b93d88eb1..21bb81cd33 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -30,6 +30,7 @@ pub struct MouseArea< on_right_release: Option, on_middle_press: Option, on_middle_release: Option, + on_mouse_wheel: Option Message + 'a>>, } impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { @@ -81,6 +82,15 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { self.on_middle_release = Some(message); self } + #[must_use] + /// The message to emit when the mouse wheel is released. + pub fn on_mouse_wheel( + mut self, + message: impl Fn(mouse::ScrollDelta) -> Message + 'a, + ) -> Self { + self.on_mouse_wheel = Some(Box::new(message)); + self + } } /// Local state of the [`MouseArea`]. @@ -104,6 +114,7 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { on_right_release: None, on_middle_press: None, on_middle_release: None, + on_mouse_wheel: None, } } } @@ -339,6 +350,20 @@ fn update( } } + if let Some(message) = widget.on_mouse_wheel.as_ref() { + if let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event { + // todo should scroll x and y be seperate functions or parameters? + // todo threshold, local state, parameter? (pixels laptop scroll) + if let mouse::ScrollDelta::Pixels { y, .. } = delta { + if y.abs() < 5. { + return event::Status::Ignored; + } + } + shell.publish((message)(*delta)); + return event::Status::Captured; + } + } + if state.drag_initiated.is_none() && widget.on_drag.is_some() { if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) = event