From 27174aaaedc2a250ebe607f5bea59fa735aa657d Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 2 May 2023 15:48:20 -0700 Subject: [PATCH 001/178] 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 002/178] 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 003/178] 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 004/178] 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 005/178] 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 006/178] 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 007/178] 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 008/178] 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 009/178] 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 010/178] 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 011/178] 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 012/178] 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 013/178] 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 014/178] 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 015/178] 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 016/178] 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 017/178] 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 018/178] 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 019/178] 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 020/178] 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 021/178] 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 022/178] 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 023/178] 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 024/178] 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 025/178] 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 026/178] 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 027/178] 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 028/178] 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 029/178] 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 030/178] 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 031/178] 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 032/178] 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 033/178] 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 034/178] 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 035/178] 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 036/178] 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 037/178] 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 038/178] 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 039/178] 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 040/178] 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 041/178] 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 042/178] 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 043/178] 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 044/178] 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 045/178] 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 046/178] 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 047/178] 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 048/178] 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 049/178] 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 050/178] 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 051/178] 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 052/178] 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 053/178] 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 054/178] 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 055/178] 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 056/178] 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 057/178] 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 058/178] 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 059/178] 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 060/178] 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 061/178] 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 062/178] 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 063/178] 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 064/178] 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 065/178] 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 066/178] 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 067/178] 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 068/178] 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 069/178] 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 070/178] 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 071/178] 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 072/178] 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 073/178] 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 074/178] 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 075/178] 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 076/178] 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 077/178] 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 078/178] 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 079/178] 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 080/178] 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 081/178] 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 082/178] 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 083/178] 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 084/178] 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 085/178] 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 086/178] 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 087/178] 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 088/178] 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 089/178] 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 090/178] 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 091/178] 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 092/178] 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 8cbd24ef9584eef361df33fab19fb096e30d509b Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 7 Mar 2024 15:17:19 -0500 Subject: [PATCH 093/178] fix(sctk): destroy drag icon and send event after cancel action --- sctk/src/event_loop/mod.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/sctk/src/event_loop/mod.rs b/sctk/src/event_loop/mod.rs index 955206111e..06f023fe3a 100644 --- a/sctk/src/event_loop/mod.rs +++ b/sctk/src/event_loop/mod.rs @@ -16,8 +16,9 @@ use crate::{ wp_viewporter::ViewporterState, }, sctk_event::{ - DndOfferEvent, IcedSctkEvent, LayerSurfaceEventVariant, - PopupEventVariant, SctkEvent, StartCause, WindowEventVariant, + DataSourceEvent, DndOfferEvent, IcedSctkEvent, + LayerSurfaceEventVariant, PopupEventVariant, SctkEvent, StartCause, + WindowEventVariant, }, settings, subsurface_widget::SubsurfaceState, @@ -1197,8 +1198,16 @@ where } }, platform_specific::wayland::data_device::ActionInner::DndCancelled => { - if let Some(source) = self.state.dnd_source.as_mut() { - source.source = None; + if let Some(mut source) = self.state.dnd_source.take() { + if let Some(s) = source.icon_surface.take() { + s.0.destroy(); + } + sticky_exit_callback( + IcedSctkEvent::SctkEvent(SctkEvent::DataSource(DataSourceEvent::DndCancelled)), + &self.state, + &mut control_flow, + &mut callback + ); } }, platform_specific::wayland::data_device::ActionInner::RequestDndData (mime_type) => { From 9f8dc936a3764c9c15b0d79e0cb7671d133245e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 5 Feb 2024 00:15:35 +0100 Subject: [PATCH 094/178] Use `TypeId` to identify `subscription::Map` (cherry picked from commit f39a5fd8953494fd8e41c05bc053519740d09612) --- futures/src/subscription.rs | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs index 7163248dc8..4d5a119293 100644 --- a/futures/src/subscription.rs +++ b/futures/src/subscription.rs @@ -10,6 +10,7 @@ use crate::{BoxStream, MaybeSend}; use futures::channel::mpsc; use futures::never::Never; +use std::any::TypeId; use std::hash::Hash; /// A stream of runtime events. @@ -88,7 +89,10 @@ impl Subscription { } /// Transforms the [`Subscription`] output with the given function. - pub fn map(mut self, f: fn(Message) -> A) -> Subscription + pub fn map( + mut self, + f: impl Fn(Message) -> A + MaybeSend + Clone + 'static, + ) -> Subscription where Message: 'static, A: 'static, @@ -97,8 +101,9 @@ impl Subscription { recipes: self .recipes .drain(..) - .map(|recipe| { - Box::new(Map::new(recipe, f)) as Box> + .map(move |recipe| { + Box::new(Map::new(recipe, f.clone())) + as Box> }) .collect(), } @@ -143,27 +148,39 @@ pub trait Recipe { fn stream(self: Box, input: EventStream) -> BoxStream; } -struct Map { +struct Map +where + F: Fn(A) -> B + 'static, +{ + id: TypeId, recipe: Box>, - mapper: fn(A) -> B, + mapper: F, } -impl Map { - fn new(recipe: Box>, mapper: fn(A) -> B) -> Self { - Map { recipe, mapper } +impl Map +where + F: Fn(A) -> B + 'static, +{ + fn new(recipe: Box>, mapper: F) -> Self { + Map { + id: TypeId::of::(), + recipe, + mapper, + } } } -impl Recipe for Map +impl Recipe for Map where A: 'static, B: 'static, + F: Fn(A) -> B + 'static + MaybeSend, { type Output = B; fn hash(&self, state: &mut Hasher) { + self.id.hash(state); self.recipe.hash(state); - self.mapper.hash(state); } fn stream(self: Box, input: EventStream) -> BoxStream { From 9bcfc8476f588900e6235844eac28e72c53fb3ab Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 8 Mar 2024 06:33:34 +0100 Subject: [PATCH 095/178] fix(winit): add static lifetimes to application update --- winit/src/application.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winit/src/application.rs b/winit/src/application.rs index fc3e67f829..aae6738372 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -804,7 +804,7 @@ where /// Updates an [`Application`] by feeding it the provided messages, spawning any /// resulting [`Command`], and tracking its [`Subscription`]. -pub fn update( +pub fn update( application: &mut A, compositor: &mut C, surface: &mut C::Surface, From e7d10f4137cf09136f8eebe9bc1478ec428eec6a Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 8 Mar 2024 12:58:19 +0100 Subject: [PATCH 096/178] fix(winit): add static lifetimes to multi-window application update --- winit/src/multi_window.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index ec17b00f0a..2ed427b8e2 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -1049,7 +1049,7 @@ where /// Updates a multi-window [`Application`] by feeding it messages, spawning any /// resulting [`Command`], and tracking its [`Subscription`]. -fn update( +fn update( application: &mut A, compositor: &mut C, runtime: &mut Runtime< From 39426ead895849bb3f55143c408a3b160049bfee Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 13 Mar 2024 08:43:14 +0100 Subject: [PATCH 097/178] fix(tiny_skia): disable shadows due to rendering glitch --- tiny_skia/src/backend.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 57f7e7ee24..ce94b3d0c7 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -193,7 +193,9 @@ impl Backend { } let path = rounded_rectangle(path_bounds, fill_border_radius); - if shadow.color.a > 0.0 { + // TODO: Disabled due to graphical glitches + // if shadow.color.a > 0.0 { + if false { let shadow_bounds = (Rectangle { x: bounds.x + shadow.offset.x - shadow.blur_radius, y: bounds.y + shadow.offset.y - shadow.blur_radius, From 6f9281234702727dec95a72c3cdb247b1961222f Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Wed, 13 Mar 2024 22:22:36 -0700 Subject: [PATCH 098/178] sctk: Fix handling of layer surface `pointer_interactivity` (#115) A null `region` represents an infinite region (the default). To set an empty region, we need to create a `wl_region`. --- sctk/src/event_loop/state.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/sctk/src/event_loop/state.rs b/sctk/src/event_loop/state.rs index bcc6c2f94b..fa151f1f2c 100644 --- a/sctk/src/event_loop/state.rs +++ b/sctk/src/event_loop/state.rs @@ -45,6 +45,7 @@ use sctk::{ protocol::{ wl_keyboard::WlKeyboard, wl_output::WlOutput, + wl_region::WlRegion, wl_seat::WlSeat, wl_subsurface::WlSubsurface, wl_surface::{self, WlSurface}, @@ -786,7 +787,12 @@ where .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); + let region = self + .compositor_state + .wl_compositor() + .create_region(&self.queue_handle, ()); + layer_surface.set_input_region(Some(®ion)); + region.destroy(); } layer_surface.commit(); @@ -846,3 +852,4 @@ where } delegate_noop!(@ SctkState: ignore WlSubsurface); +delegate_noop!(@ SctkState: ignore WlRegion); From 454b95356f5f6832c62029fbbb8aff87917f7043 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 14 Mar 2024 17:45:41 -0400 Subject: [PATCH 099/178] feat: custom mime types for Clipboard --- Cargo.toml | 3 +- core/Cargo.toml | 1 + core/src/clipboard.rs | 61 ++++++++++++++++++++ sctk/Cargo.toml | 4 +- sctk/src/application.rs | 12 ++-- sctk/src/clipboard/clipboard.rs | 98 ++++++++++++++++++++++++++------- src/lib.rs | 1 + winit/src/clipboard.rs | 75 +++++++++++++++++++++++++ 8 files changed, 223 insertions(+), 32 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 417df6c127..2d81063f48 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -178,5 +178,6 @@ web-time = "0.2" # 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" +window_clipboard = { git = "https://github.com/pop-os/window_clipboard.git", branch = "mime-types" } +# window_clipboard = { path = "../window_clipboard" } winit = { git = "https://github.com/pop-os/winit.git", branch = "winit-0.29" } diff --git a/core/Cargo.toml b/core/Cargo.toml index 8608dd11f0..5eb0780980 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -22,6 +22,7 @@ smol_str.workspace = true thiserror.workspace = true web-time.workspace = true xxhash-rust.workspace = true +window_clipboard.workspace = true sctk.workspace = true sctk.optional = true diff --git a/core/src/clipboard.rs b/core/src/clipboard.rs index 081b40046c..f465a430c3 100644 --- a/core/src/clipboard.rs +++ b/core/src/clipboard.rs @@ -1,5 +1,7 @@ //! Access the clipboard. +use window_clipboard::mime::{self, AllowedMimeTypes, ClipboardStoreData}; + /// A buffer for short-term storage and transfer within and between /// applications. pub trait Clipboard { @@ -8,6 +10,47 @@ pub trait Clipboard { /// Writes the given text contents to the [`Clipboard`]. fn write(&mut self, contents: String); + + /// Read the current content of the primary [`Clipboard`] as text. + fn read_primary(&self) -> Option { + None + } + + /// Writes the given text contents to the primary [`Clipboard`]. + fn write_primary(&mut self, _contents: String) {} + + /// Consider using [`read_data`] instead + /// Reads the current content of the [`Clipboard`] as text. + fn read_data(&self, _mimes: Vec) -> Option<(Vec, String)> { + None + } + + /// Writes the given contents to the [`Clipboard`]. + fn write_data( + &mut self, + _contents: ClipboardStoreData< + Box, + >, + ) { + } + + /// Consider using [`read_primary_data`] instead + /// Reads the current content of the primary [`Clipboard`] as text. + fn read_primary_data( + &self, + _mimes: Vec, + ) -> Option<(Vec, String)> { + None + } + + /// Writes the given text contents to the primary [`Clipboard`]. + fn write_primary_data( + &mut self, + _contents: ClipboardStoreData< + Box, + >, + ) { + } } /// A null implementation of the [`Clipboard`] trait. @@ -21,3 +64,21 @@ impl Clipboard for Null { fn write(&mut self, _contents: String) {} } + +/// Reads the current content of the [`Clipboard`]. +pub fn read_data( + clipboard: &mut dyn Clipboard, +) -> Option { + clipboard + .read_data(T::allowed().into()) + .and_then(|data| T::try_from(data).ok()) +} + +/// Reads the current content of the primary [`Clipboard`]. +pub fn read_primary_data( + clipboard: &mut dyn Clipboard, +) -> Option { + clipboard + .read_data(T::allowed().into()) + .and_then(|data| T::try_from(data).ok()) +} diff --git a/sctk/Cargo.toml b/sctk/Cargo.toml index 7eb18c8202..6c01eeeebc 100644 --- a/sctk/Cargo.toml +++ b/sctk/Cargo.toml @@ -10,20 +10,20 @@ debug = ["iced_runtime/debug"] system = ["sysinfo"] application = [] a11y = ["iced_accessibility", "iced_runtime/a11y"] -clipboard = ["smithay-clipboard"] +clipboard = [] [dependencies] tracing = "0.1" thiserror = "1.0" sctk.workspace = true wayland-protocols.workspace = true +window_clipboard.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 = { 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 19057be0a2..9f570a1cff 100644 --- a/sctk/src/application.rs +++ b/sctk/src/application.rs @@ -524,11 +524,9 @@ where 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()) }; - } - } + if let Ok(h) = wrapper.display_handle() { + simple_clipboard = unsafe {Clipboard::connect(&h)}; + } } 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()); @@ -603,9 +601,7 @@ where }; 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()) }; - } + simple_clipboard = unsafe {Clipboard::connect(&h)}; } } let mut c_surface = compositor.create_surface(wrapper.clone(), configure.new_size.0, configure.new_size.1); diff --git a/sctk/src/clipboard/clipboard.rs b/sctk/src/clipboard/clipboard.rs index 69b9434160..bbc5e6e7e7 100644 --- a/sctk/src/clipboard/clipboard.rs +++ b/sctk/src/clipboard/clipboard.rs @@ -2,8 +2,8 @@ pub use iced_runtime::clipboard::Action; use iced_runtime::command::{self, Command}; -use std::ffi::c_void; -use std::sync::{Arc, Mutex}; +use raw_window_handle::HasDisplayHandle; +use window_clipboard::mime::{self, ClipboardStoreData}; /// A buffer for short-term storage and transfer within and between /// applications. @@ -13,18 +13,16 @@ pub struct Clipboard { } pub(crate) enum State { - Connected(Arc>), + Connected(window_clipboard::Clipboard), 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 _, - ))); + pub unsafe fn connect(display: &impl HasDisplayHandle) -> Clipboard { + let context = window_clipboard::Clipboard::connect(display); Clipboard { - state: State::Connected(context), + state: context.map(State::Connected).unwrap_or(State::Unavailable), } } @@ -39,36 +37,94 @@ impl Clipboard { state: State::Unavailable, } } +} - /// Reads the current content of the [`Clipboard`] as text. - pub fn read(&self) -> Option { +impl iced_runtime::core::clipboard::Clipboard for Clipboard { + fn read(&self) -> Option { + match &self.state { + State::Connected(clipboard) => clipboard.read().ok(), + State::Unavailable => None, + } + } + + fn write(&mut self, contents: String) { + match &mut self.state { + State::Connected(clipboard) => _ = clipboard.write(contents), + State::Unavailable => {} + } + } + + /// Read the current content of the primary [`Clipboard`] as text. + fn read_primary(&self) -> Option { match &self.state { State::Connected(clipboard) => { - let clipboard = clipboard.lock().unwrap(); - clipboard.load().ok() + clipboard.read_primary().and_then(|res| res.ok()) } State::Unavailable => None, } } - /// Writes the given text contents to the [`Clipboard`]. - pub fn write(&mut self, contents: String) { + /// Writes the given text contents to the primary [`Clipboard`]. + fn write_primary(&mut self, contents: String) { match &mut self.state { State::Connected(clipboard) => { - clipboard.lock().unwrap().store(contents) + _ = clipboard.write_primary(contents) } State::Unavailable => {} } } -} -impl iced_runtime::core::clipboard::Clipboard for Clipboard { - fn read(&self) -> Option { - self.read() + /// Consider using [`read_data`] instead + /// Reads the current content of the [`Clipboard`] as text. + fn read_data(&self, mimes: Vec) -> Option<(Vec, String)> { + match &self.state { + State::Connected(clipboard) => { + clipboard.read_raw(mimes).and_then(|res| res.ok()) + } + State::Unavailable => None, + } } - fn write(&mut self, contents: String) { - self.write(contents) + /// Writes the given contents to the [`Clipboard`]. + fn write_data( + &mut self, + contents: ClipboardStoreData< + Box, + >, + ) { + match &mut self.state { + State::Connected(clipboard) => _ = clipboard.write_data(contents), + State::Unavailable => {} + } + } + + /// Consider using [`read_primary_data`] instead + /// Reads the current content of the primary [`Clipboard`] as text. + fn read_primary_data( + &self, + mimes: Vec, + ) -> Option<(Vec, String)> { + match &self.state { + State::Connected(clipboard) => { + clipboard.read_primary_raw(mimes).and_then(|res| res.ok()) + } + State::Unavailable => None, + } + } + + /// Writes the given text contents to the primary [`Clipboard`]. + fn write_primary_data( + &mut self, + contents: ClipboardStoreData< + Box, + >, + ) { + match &mut self.state { + State::Connected(clipboard) => { + _ = clipboard.write_primary_data(contents) + } + State::Unavailable => {} + } } } diff --git a/src/lib.rs b/src/lib.rs index aa46198435..4c335927e1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -225,6 +225,7 @@ pub use crate::core::{ pub mod clipboard { //! Access the clipboard. pub use crate::runtime::clipboard::{read, write}; + pub use iced_core::clipboard::{read_data, read_primary_data}; } pub mod executor { diff --git a/winit/src/clipboard.rs b/winit/src/clipboard.rs index 8f5c5e63e7..2b95d665e8 100644 --- a/winit/src/clipboard.rs +++ b/winit/src/clipboard.rs @@ -1,5 +1,7 @@ //! Access the clipboard. +use window_clipboard::mime::{self, ClipboardStoreData}; + /// A buffer for short-term storage and transfer within and between /// applications. #[allow(missing_debug_implementations)] @@ -62,4 +64,77 @@ impl crate::core::Clipboard for Clipboard { fn write(&mut self, contents: String) { self.write(contents); } + + /// Read the current content of the primary [`Clipboard`] as text. + fn read_primary(&self) -> Option { + match &self.state { + State::Connected(clipboard) => { + clipboard.read_primary().and_then(|res| res.ok()) + } + State::Unavailable => None, + } + } + + /// Writes the given text contents to the primary [`Clipboard`]. + fn write_primary(&mut self, contents: String) { + match &mut self.state { + State::Connected(clipboard) => { + _ = clipboard.write_primary(contents) + } + State::Unavailable => {} + } + } + + /// Consider using [`read_data`] instead + /// Reads the current content of the [`Clipboard`] as text. + fn read_data(&self, mimes: Vec) -> Option<(Vec, String)> { + match &self.state { + State::Connected(clipboard) => { + clipboard.read_raw(mimes).and_then(|res| res.ok()) + } + State::Unavailable => None, + } + } + + /// Writes the given contents to the [`Clipboard`]. + fn write_data( + &mut self, + contents: ClipboardStoreData< + Box, + >, + ) { + match &mut self.state { + State::Connected(clipboard) => _ = clipboard.write_data(contents), + State::Unavailable => {} + } + } + + /// Consider using [`read_primary_data`] instead + /// Reads the current content of the primary [`Clipboard`] as text. + fn read_primary_data( + &self, + mimes: Vec, + ) -> Option<(Vec, String)> { + match &self.state { + State::Connected(clipboard) => { + clipboard.read_primary_raw(mimes).and_then(|res| res.ok()) + } + State::Unavailable => None, + } + } + + /// Writes the given text contents to the primary [`Clipboard`]. + fn write_primary_data( + &mut self, + contents: ClipboardStoreData< + Box, + >, + ) { + match &mut self.state { + State::Connected(clipboard) => { + _ = clipboard.write_primary_data(contents) + } + State::Unavailable => {} + } + } } From 7e85bd87b1e3f4c5ac8b2e5af762108ff729bc78 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 14 Mar 2024 17:54:51 -0400 Subject: [PATCH 100/178] cleanup docs --- sctk/src/clipboard/clipboard.rs | 8 -------- winit/src/clipboard.rs | 8 -------- 2 files changed, 16 deletions(-) diff --git a/sctk/src/clipboard/clipboard.rs b/sctk/src/clipboard/clipboard.rs index bbc5e6e7e7..17941aa461 100644 --- a/sctk/src/clipboard/clipboard.rs +++ b/sctk/src/clipboard/clipboard.rs @@ -54,7 +54,6 @@ impl iced_runtime::core::clipboard::Clipboard for Clipboard { } } - /// Read the current content of the primary [`Clipboard`] as text. fn read_primary(&self) -> Option { match &self.state { State::Connected(clipboard) => { @@ -64,7 +63,6 @@ impl iced_runtime::core::clipboard::Clipboard for Clipboard { } } - /// Writes the given text contents to the primary [`Clipboard`]. fn write_primary(&mut self, contents: String) { match &mut self.state { State::Connected(clipboard) => { @@ -74,8 +72,6 @@ impl iced_runtime::core::clipboard::Clipboard for Clipboard { } } - /// Consider using [`read_data`] instead - /// Reads the current content of the [`Clipboard`] as text. fn read_data(&self, mimes: Vec) -> Option<(Vec, String)> { match &self.state { State::Connected(clipboard) => { @@ -85,7 +81,6 @@ impl iced_runtime::core::clipboard::Clipboard for Clipboard { } } - /// Writes the given contents to the [`Clipboard`]. fn write_data( &mut self, contents: ClipboardStoreData< @@ -98,8 +93,6 @@ impl iced_runtime::core::clipboard::Clipboard for Clipboard { } } - /// Consider using [`read_primary_data`] instead - /// Reads the current content of the primary [`Clipboard`] as text. fn read_primary_data( &self, mimes: Vec, @@ -112,7 +105,6 @@ impl iced_runtime::core::clipboard::Clipboard for Clipboard { } } - /// Writes the given text contents to the primary [`Clipboard`]. fn write_primary_data( &mut self, contents: ClipboardStoreData< diff --git a/winit/src/clipboard.rs b/winit/src/clipboard.rs index 2b95d665e8..019384f583 100644 --- a/winit/src/clipboard.rs +++ b/winit/src/clipboard.rs @@ -65,7 +65,6 @@ impl crate::core::Clipboard for Clipboard { self.write(contents); } - /// Read the current content of the primary [`Clipboard`] as text. fn read_primary(&self) -> Option { match &self.state { State::Connected(clipboard) => { @@ -75,7 +74,6 @@ impl crate::core::Clipboard for Clipboard { } } - /// Writes the given text contents to the primary [`Clipboard`]. fn write_primary(&mut self, contents: String) { match &mut self.state { State::Connected(clipboard) => { @@ -85,8 +83,6 @@ impl crate::core::Clipboard for Clipboard { } } - /// Consider using [`read_data`] instead - /// Reads the current content of the [`Clipboard`] as text. fn read_data(&self, mimes: Vec) -> Option<(Vec, String)> { match &self.state { State::Connected(clipboard) => { @@ -96,7 +92,6 @@ impl crate::core::Clipboard for Clipboard { } } - /// Writes the given contents to the [`Clipboard`]. fn write_data( &mut self, contents: ClipboardStoreData< @@ -109,8 +104,6 @@ impl crate::core::Clipboard for Clipboard { } } - /// Consider using [`read_primary_data`] instead - /// Reads the current content of the primary [`Clipboard`] as text. fn read_primary_data( &self, mimes: Vec, @@ -123,7 +116,6 @@ impl crate::core::Clipboard for Clipboard { } } - /// Writes the given text contents to the primary [`Clipboard`]. fn write_primary_data( &mut self, contents: ClipboardStoreData< From 2bd599c3ca0b19977980f925d603acbf4b0604cd Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 14 Mar 2024 19:20:00 -0400 Subject: [PATCH 101/178] feat: add actions and commands for new clipboard methods --- runtime/Cargo.toml | 1 + runtime/src/clipboard.rs | 81 +++++++++++++++++++++++++++++++++++++++ sctk/src/application.rs | 37 ++++++++++++++---- winit/src/application.rs | 26 +++++++++++++ winit/src/multi_window.rs | 26 +++++++++++++ 5 files changed, 163 insertions(+), 8 deletions(-) diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 3a06eaa594..3186662182 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -25,3 +25,4 @@ sctk.optional = true thiserror.workspace = true iced_accessibility.workspace = true iced_accessibility.optional = true +window_clipboard.workspace = true \ No newline at end of file diff --git a/runtime/src/clipboard.rs b/runtime/src/clipboard.rs index bc45091266..66ad213331 100644 --- a/runtime/src/clipboard.rs +++ b/runtime/src/clipboard.rs @@ -1,4 +1,6 @@ //! Access the clipboard. +use window_clipboard::mime::{AllowedMimeTypes, AsMimeTypes}; + use crate::command::{self, Command}; use crate::futures::MaybeSend; @@ -13,6 +15,24 @@ pub enum Action { /// Write the given contents to the clipboard. Write(String), + + /// Write the given contents to the clipboard. + WriteData(Box), + + /// Read the clipboard and produce `T` with the result. + ReadData(Vec, Box, String)>) -> T>), + + /// Read the clipboard and produce `T` with the result. + ReadPrimary(Box) -> T>), + + /// Write the given contents to the clipboard. + WritePrimary(String), + + /// Write the given contents to the clipboard. + WritePrimaryData(Box), + + /// Read the clipboard and produce `T` with the result. + ReadPrimaryData(Vec, Box, String)>) -> T>), } impl Action { @@ -27,6 +47,20 @@ impl Action { match self { Self::Read(o) => Action::Read(Box::new(move |s| f(o(s)))), Self::Write(content) => Action::Write(content), + Self::WriteData(content) => Action::WriteData(content), + Self::ReadData(a, o) => { + Action::ReadData(a, Box::new(move |s| f(o(s)))) + } + Self::ReadPrimary(o) => { + Action::ReadPrimary(Box::new(move |s| f(o(s)))) + } + Self::WritePrimary(content) => Action::WritePrimary(content), + Self::WritePrimaryData(content) => { + Action::WritePrimaryData(content) + } + Self::ReadPrimaryData(a, o) => { + Action::ReadPrimaryData(a, Box::new(move |s| f(o(s)))) + } } } } @@ -36,6 +70,12 @@ impl fmt::Debug for Action { match self { Self::Read(_) => write!(f, "Action::Read"), Self::Write(_) => write!(f, "Action::Write"), + Self::WriteData(_) => write!(f, "Action::WriteData"), + Self::ReadData(_, _) => write!(f, "Action::ReadData"), + Self::ReadPrimary(_) => write!(f, "Action::ReadPrimary"), + Self::WritePrimary(_) => write!(f, "Action::WritePrimary"), + Self::WritePrimaryData(_) => write!(f, "Action::WritePrimaryData"), + Self::ReadPrimaryData(_, _) => write!(f, "Action::ReadPrimaryData"), } } } @@ -51,3 +91,44 @@ pub fn read( pub fn write(contents: String) -> Command { Command::single(command::Action::Clipboard(Action::Write(contents))) } + +/// Read the current contents of the clipboard. +pub fn read_data( + f: impl Fn(Option) -> Message + 'static, +) -> Command { + Command::single(command::Action::Clipboard(Action::ReadData( + T::allowed().into(), + Box::new(move |d| f(d.and_then(|d| T::try_from(d).ok()))), + ))) +} + +/// Write the given contents to the clipboard. +pub fn write_data( + contents: impl AsMimeTypes + std::marker::Sync + std::marker::Send + 'static, +) -> Command { + Command::single(command::Action::Clipboard(Action::WriteData(Box::new( + contents, + )))) +} + +/// Read the current contents of the clipboard. +pub fn read_primary_data< + T: AllowedMimeTypes + Send + Sync + 'static, + Message, +>( + f: impl Fn(Option) -> Message + 'static, +) -> Command { + Command::single(command::Action::Clipboard(Action::ReadPrimaryData( + T::allowed().into(), + Box::new(move |d| f(d.and_then(|d| T::try_from(d).ok()))), + ))) +} + +/// Write the given contents to the clipboard. +pub fn write_primary_data( + contents: impl AsMimeTypes + std::marker::Sync + std::marker::Send + 'static, +) -> Command { + Command::single(command::Action::Clipboard(Action::WritePrimaryData( + Box::new(contents), + ))) +} diff --git a/sctk/src/application.rs b/sctk/src/application.rs index 9f570a1cff..333e6b3316 100644 --- a/sctk/src/application.rs +++ b/sctk/src/application.rs @@ -73,6 +73,7 @@ use std::{ }; use wayland_backend::client::ObjectId; use wayland_protocols::wp::viewporter::client::wp_viewport::WpViewport; +use window_clipboard::mime::ClipboardStoreData; use crate::subsurface_widget::{SubsurfaceInstance, SubsurfaceState}; @@ -2002,17 +2003,37 @@ where } 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)); - } + 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) - } + clipboard.write(contents) } + clipboard::Action::WriteData(contents) => { + clipboard.write_data(ClipboardStoreData(contents)) + }, + clipboard::Action::ReadData(allowed, to_msg) => { + let contents = clipboard.read_data(allowed); + let message = to_msg(contents); + proxy.send_event(Event::Message(message)); + }, + clipboard::Action::ReadPrimary(s_to_msg) => { + let contents = clipboard.read_primary(); + let message = s_to_msg(contents); + proxy.send_event(Event::Message(message)); + }, + clipboard::Action::WritePrimary(content) => { + clipboard.write_primary(content) + }, + clipboard::Action::WritePrimaryData(content) => { + clipboard.write_primary_data(ClipboardStoreData(content)) + }, + clipboard::Action::ReadPrimaryData(a, to_msg) => { + let contents = clipboard.read_primary_data(a); + let message = to_msg(contents); + proxy.send_event(Event::Message(message)); + }, }, command::Action::Window(..) => { unimplemented!("Use platform specific events instead") diff --git a/winit/src/application.rs b/winit/src/application.rs index aae6738372..afc0b2fe44 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -7,7 +7,9 @@ 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; +use iced_style::core::Clipboard as CoreClipboard; pub use state::State; +use window_clipboard::mime::ClipboardStoreData; use crate::conversion; use crate::core; @@ -908,6 +910,30 @@ pub fn run_command( clipboard::Action::Write(contents) => { clipboard.write(contents); } + clipboard::Action::WriteData(contents) => { + clipboard.write_data(ClipboardStoreData(contents)) + } + clipboard::Action::ReadData(allowed, to_msg) => { + let contents = clipboard.read_data(allowed); + let message = to_msg(contents); + _ = proxy.send_event(UserEventWrapper::Message(message)); + } + clipboard::Action::ReadPrimary(s_to_msg) => { + let contents = clipboard.read_primary(); + let message = s_to_msg(contents); + _ = proxy.send_event(UserEventWrapper::Message(message)); + } + clipboard::Action::WritePrimary(content) => { + clipboard.write_primary(content) + } + clipboard::Action::WritePrimaryData(content) => { + clipboard.write_primary_data(ClipboardStoreData(content)) + } + clipboard::Action::ReadPrimaryData(a, to_msg) => { + let contents = clipboard.read_primary_data(a); + let message = to_msg(contents); + _ = proxy.send_event(UserEventWrapper::Message(message)); + } }, command::Action::Window(action) => match action { window::Action::Close(_id) => { diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 2ed427b8e2..14dafdfa59 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -26,9 +26,11 @@ use crate::runtime::user_interface::{self, UserInterface}; use crate::runtime::Debug; use crate::style::application::StyleSheet; use crate::{Clipboard, Error, Proxy, Settings}; +use core::Clipboard as CoreClipboard; use iced_runtime::futures::futures::FutureExt; use iced_style::Theme; pub use state::State; +use window_clipboard::mime::ClipboardStoreData; use std::collections::HashMap; use std::mem::ManuallyDrop; @@ -1142,6 +1144,30 @@ fn run_command( clipboard::Action::Write(contents) => { clipboard.write(contents); } + clipboard::Action::WriteData(contents) => { + clipboard.write_data(ClipboardStoreData(contents)) + } + clipboard::Action::ReadData(allowed, to_msg) => { + let contents = clipboard.read_data(allowed); + let message = to_msg(contents); + _ = proxy.send_event(UserEventWrapper::Message(message)); + } + clipboard::Action::ReadPrimary(s_to_msg) => { + let contents = clipboard.read_primary(); + let message = s_to_msg(contents); + _ = proxy.send_event(UserEventWrapper::Message(message)); + } + clipboard::Action::WritePrimary(content) => { + clipboard.write_primary(content) + } + clipboard::Action::WritePrimaryData(content) => { + clipboard.write_primary_data(ClipboardStoreData(content)) + } + clipboard::Action::ReadPrimaryData(a, to_msg) => { + let contents = clipboard.read_primary_data(a); + let message = to_msg(contents); + _ = proxy.send_event(UserEventWrapper::Message(message)); + } }, command::Action::Window(action) => match action { window::Action::Spawn(id, settings) => { From f4bafb8d1d40c75d33b7ed64f9018569ebbfc5d4 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 14 Mar 2024 19:40:18 -0400 Subject: [PATCH 102/178] clippy --- core/src/element.rs | 4 ++-- core/src/widget/tree.rs | 2 +- graphics/src/text/editor.rs | 2 +- runtime/src/clipboard.rs | 2 ++ sctk/src/clipboard/mod.rs | 4 +++- style/src/theme.rs | 6 ------ widget/src/keyed/column.rs | 2 +- widget/src/pane_grid.rs | 6 +++++- 8 files changed, 15 insertions(+), 13 deletions(-) diff --git a/core/src/element.rs b/core/src/element.rs index 61dc3b3b08..12047d660a 100644 --- a/core/src/element.rs +++ b/core/src/element.rs @@ -1,7 +1,7 @@ use crate::event::{self, Event}; use crate::id::Id; use crate::layout; -use crate::mouse::{self, Cursor}; +use crate::mouse; use crate::overlay; use crate::renderer; use crate::widget; @@ -489,7 +489,7 @@ where &self, _layout: Layout<'_>, _state: &Tree, - _cursor_position: Cursor, + _cursor_position: mouse::Cursor, ) -> iced_accessibility::A11yTree { self.widget.a11y_nodes(_layout, _state, _cursor_position) } diff --git a/core/src/widget/tree.rs b/core/src/widget/tree.rs index 1a1ed1b8df..bfd44492a1 100644 --- a/core/src/widget/tree.rs +++ b/core/src/widget/tree.rs @@ -161,7 +161,7 @@ impl Tree { } else if child_state_i < id_list.len() { let c = &mut id_list[child_state_i]; if len_changed { - c.id = new_id.clone(); + c.id.clone_from(new_id); } child_state_i += 1; c diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index eba7f1e8f6..4b12fa8330 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::{BufferRef, Edit as _}; +use cosmic_text::Edit as _; use std::fmt; use std::sync::{self, Arc}; diff --git a/runtime/src/clipboard.rs b/runtime/src/clipboard.rs index 66ad213331..1e78c61249 100644 --- a/runtime/src/clipboard.rs +++ b/runtime/src/clipboard.rs @@ -19,6 +19,7 @@ pub enum Action { /// Write the given contents to the clipboard. WriteData(Box), + #[allow(clippy::type_complexity)] /// Read the clipboard and produce `T` with the result. ReadData(Vec, Box, String)>) -> T>), @@ -31,6 +32,7 @@ pub enum Action { /// Write the given contents to the clipboard. WritePrimaryData(Box), + #[allow(clippy::type_complexity)] /// Read the clipboard and produce `T` with the result. ReadPrimaryData(Vec, Box, String)>) -> T>), } diff --git a/sctk/src/clipboard/mod.rs b/sctk/src/clipboard/mod.rs index bb0496d026..dc75d699af 100644 --- a/sctk/src/clipboard/mod.rs +++ b/sctk/src/clipboard/mod.rs @@ -15,7 +15,9 @@ mod clipboard { } impl Clipboard { - pub unsafe fn connect(_display: *mut c_void) -> Clipboard { + pub unsafe fn connect( + _display: &impl raw_window_handle::HasDisplayHandle, + ) -> Clipboard { Clipboard } diff --git a/style/src/theme.rs b/style/src/theme.rs index 1dd87ed150..8f38fae819 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -22,12 +22,6 @@ 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; -use iced_core::Radians; use iced_core::{Background, Border, Color, Shadow, Vector}; use std::fmt; diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs index 32b9299bc7..2ac60ab84e 100644 --- a/widget/src/keyed/column.rs +++ b/widget/src/keyed/column.rs @@ -184,7 +184,7 @@ where ); if state.keys != self.keys { - state.keys = self.keys.clone(); + state.keys.clone_from(&self.keys); } } diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 6cb64a4c6c..b29adae9df 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -111,6 +111,7 @@ pub struct PaneGrid< spacing: f32, on_click: Option Message + 'a>>, on_drag: Option Message + 'a>>, + #[allow(clippy::type_complexity)] on_resize: Option<(f32, Box Message + 'a>)>, style: ::Style, } @@ -534,7 +535,10 @@ pub fn update<'a, Message, T: Draggable>( contents: impl Iterator, on_click: &Option Message + 'a>>, on_drag: &Option Message + 'a>>, - on_resize: &Option<(f32, Box Message + 'a>)>, + #[allow(clippy::type_complexity)] on_resize: &Option<( + f32, + Box Message + 'a>, + )>, ) -> event::Status { const DRAG_DEADBAND_DISTANCE: f32 = 10.0; From c76eae7ba8cdd02b74b52905331981b4a81198e6 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 14 Mar 2024 19:53:17 -0400 Subject: [PATCH 103/178] chore: use tag --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 2d81063f48..3e1294f815 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -178,6 +178,6 @@ web-time = "0.2" # Newer wgpu commit that fixes Vulkan backend on Nvidia wgpu = { git = "https://github.com/gfx-rs/wgpu", rev = "20fda69" } winapi = "0.3" -window_clipboard = { git = "https://github.com/pop-os/window_clipboard.git", branch = "mime-types" } +window_clipboard = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-mime-types" } # window_clipboard = { path = "../window_clipboard" } winit = { git = "https://github.com/pop-os/winit.git", branch = "winit-0.29" } From 0260e6c8320c1c1dc185c4142709401b685fb42e Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 15 Mar 2024 11:06:24 -0400 Subject: [PATCH 104/178] chore: reexport mime from window_clipboard --- Cargo.toml | 1 + src/lib.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 3e1294f815..d6e3ede884 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,6 +74,7 @@ iced_highlighter.optional = true iced_accessibility.workspace = true iced_accessibility.optional = true thiserror.workspace = true +window_clipboard.workspace = true image.workspace = true image.optional = true diff --git a/src/lib.rs b/src/lib.rs index 4c335927e1..3b8241b7a4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -226,6 +226,7 @@ pub mod clipboard { //! Access the clipboard. pub use crate::runtime::clipboard::{read, write}; pub use iced_core::clipboard::{read_data, read_primary_data}; + pub use window_clipboard::mime; } pub mod executor { From 1d9bb7fdff21f71c06c04e0c70087a5a8b0bc99e Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 15 Mar 2024 19:18:21 +0100 Subject: [PATCH 105/178] fix: ambiguous import --- winit/src/multi_window.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 14dafdfa59..a07ae9934b 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -26,7 +26,7 @@ use crate::runtime::user_interface::{self, UserInterface}; use crate::runtime::Debug; use crate::style::application::StyleSheet; use crate::{Clipboard, Error, Proxy, Settings}; -use core::Clipboard as CoreClipboard; +use crate::core::Clipboard as CoreClipboard; use iced_runtime::futures::futures::FutureExt; use iced_style::Theme; pub use state::State; From c57f7157de296c45b604f0c25f5c02d70c1f1b0f Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 29 Mar 2024 16:27:34 -0400 Subject: [PATCH 106/178] feat: winit dnd --- Cargo.toml | 31 ++- core/Cargo.toml | 2 + core/src/clipboard.rs | 85 ++++++- core/src/event.rs | 6 + core/src/widget/tree.rs | 15 ++ runtime/Cargo.toml | 3 +- runtime/src/command/action.rs | 8 + runtime/src/dnd.rs | 162 +++++++++++++ runtime/src/lib.rs | 1 + runtime/src/user_interface.rs | 5 + sctk/src/event_loop/mod.rs | 24 +- sctk/src/event_loop/state.rs | 3 +- sctk/src/handlers/data_device/data_device.rs | 53 ++-- sctk/src/handlers/data_device/data_offer.rs | 4 +- sctk/src/handlers/seat/keyboard.rs | 1 + src/application.rs | 2 +- src/lib.rs | 3 +- src/sandbox.rs | 3 +- widget/Cargo.toml | 2 + winit/Cargo.toml | 1 + winit/src/application.rs | 202 +++++++++++++++- winit/src/clipboard.rs | 138 +++++++++-- winit/src/lib.rs | 1 - winit/src/multi_window.rs | 241 ++++++++++++++++++- winit/src/proxy.rs | 31 ++- 25 files changed, 950 insertions(+), 77 deletions(-) create mode 100644 runtime/src/dnd.rs diff --git a/Cargo.toml b/Cargo.toml index d6e3ede884..42e9826f2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,11 +51,22 @@ 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", "iced_sctk?/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"] +wayland = [ + "iced_sctk", + "iced_widget/wayland", + "iced_accessibility?/accesskit_unix", + "iced_core/wayland", +] # Enables clipboard for iced_sctk wayland-clipboard = ["iced_sctk?/clipboard"] @@ -75,6 +86,8 @@ iced_accessibility.workspace = true iced_accessibility.optional = true thiserror.workspace = true window_clipboard.workspace = true +mime.workspace = true +dnd.workspace = true image.workspace = true image.optional = true @@ -94,7 +107,7 @@ members = [ "winit", "examples/*", "accessibility", - "sctk" + "sctk", ] exclude = ["examples/integration"] @@ -158,7 +171,7 @@ qrcode = { version = "0.12", default-features = false } raw-window-handle = "0.6" resvg = "0.37" rustc-hash = "1.0" -sctk = { package = "smithay-client-toolkit", git = "https://github.com/smithay/client-toolkit", rev = "2e9bf9f" } +sctk = { package = "smithay-client-toolkit", git = "https://github.com/smithay/client-toolkit", rev = "3bed072" } smol = "1.0" smol_str = "0.2" softbuffer = { git = "https://github.com/pop-os/softbuffer", tag = "cosmic-4.0" } @@ -172,13 +185,17 @@ 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"]} +wayland-protocols = { version = "0.31.0", features = ["staging"] } web-sys = "0.3" web-time = "0.2" # 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 = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-mime-types" } -# window_clipboard = { path = "../window_clipboard" } +window_clipboard = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd" } +dnd = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd" } +mime = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd" } +# window_clipboard = { path = "../window_clipboard" } +# dnd = { path = "../window_clipboard/dnd" } +# mime = { path = "../window_clipboard/mime" } winit = { git = "https://github.com/pop-os/winit.git", branch = "winit-0.29" } diff --git a/core/Cargo.toml b/core/Cargo.toml index 5eb0780980..40966bb89f 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -23,6 +23,8 @@ thiserror.workspace = true web-time.workspace = true xxhash-rust.workspace = true window_clipboard.workspace = true +dnd.workspace = true +mime.workspace = true sctk.workspace = true sctk.optional = true diff --git a/core/src/clipboard.rs b/core/src/clipboard.rs index f465a430c3..c97fa6e09f 100644 --- a/core/src/clipboard.rs +++ b/core/src/clipboard.rs @@ -1,6 +1,11 @@ //! Access the clipboard. -use window_clipboard::mime::{self, AllowedMimeTypes, ClipboardStoreData}; +use std::{any::Any, borrow::Cow, sync::Arc}; + +use dnd::{DndAction, DndDestinationRectangle, DndSurface}; +use mime::{self, AllowedMimeTypes, AsMimeTypes, ClipboardStoreData}; + +use crate::{widget::tree::State, window, Element}; /// A buffer for short-term storage and transfer within and between /// applications. @@ -51,6 +56,61 @@ pub trait Clipboard { >, ) { } + + /// Starts a DnD operation. + fn register_dnd_destination( + &self, + _surface: DndSurface, + _rectangles: Vec, + ) { + } + + /// Set the final action for the DnD operation. + /// Only should be done if it is requested. + fn set_action(&self, _action: DndAction) {} + + /// Registers Dnd destinations + fn start_dnd( + &self, + _internal: bool, + _source_surface: Option, + _icon_surface: Option>, + _content: Box, + _actions: DndAction, + ) { + } + + /// Ends a DnD operation. + fn end_dnd(&self) {} + + /// Consider using [`peek_dnd`] instead + /// Peeks the data on the DnD with a specific mime type. + /// Will return an error if there is no ongoing DnD operation. + fn peek_dnd(&self, _mime: String) -> Option<(Vec, String)> { + None + } +} + +/// Starts a DnD operation. +/// icon surface is a tuple of the icon element and optionally the icon element state. +pub fn start_dnd( + clipboard: &mut dyn Clipboard, + internal: bool, + source_surface: Option, + icon_surface: Option<(Element<'static, M, T, R>, State)>, + content: Box, + actions: DndAction, +) { + clipboard.start_dnd( + internal, + source_surface, + icon_surface.map(|i| { + let i: Box = Box::new(Arc::new(i)); + i + }), + content, + actions, + ); } /// A null implementation of the [`Clipboard`] trait. @@ -82,3 +142,26 @@ pub fn read_primary_data( .read_data(T::allowed().into()) .and_then(|data| T::try_from(data).ok()) } + +/// Reads the current content of the primary [`Clipboard`]. +pub fn peek_dnd( + clipboard: &mut dyn Clipboard, + mime: Option, +) -> Option { + let Some(mime) = mime.or_else(|| T::allowed().first().cloned().into()) + else { + return None; + }; + clipboard + .peek_dnd(mime) + .and_then(|data| T::try_from(data).ok()) +} + +/// Source of a DnD operation. +#[derive(Debug, Clone)] +pub enum DndSource { + /// A widget is the source of the DnD operation. + Widget(crate::id::Id), + /// A surface is the source of the DnD operation. + Surface(window::Id), +} diff --git a/core/src/event.rs b/core/src/event.rs index f029b40d14..5aec00ba9e 100644 --- a/core/src/event.rs +++ b/core/src/event.rs @@ -1,4 +1,7 @@ //! Handle events of a user interface. +use dnd::DndEvent; +use dnd::DndSurface; + use crate::keyboard; use crate::mouse; use crate::touch; @@ -33,6 +36,9 @@ pub enum Event { iced_accessibility::accesskit::ActionRequest, ), + /// A DnD event. + Dnd(DndEvent), + /// A platform specific event PlatformSpecific(PlatformSpecific), } diff --git a/core/src/widget/tree.rs b/core/src/widget/tree.rs index bfd44492a1..d9e53acbc2 100644 --- a/core/src/widget/tree.rs +++ b/core/src/widget/tree.rs @@ -53,6 +53,21 @@ impl Tree { } } + /// Finds a widget state in the tree by its id. + pub fn find<'a>(&'a self, id: &Id) -> Option<&'a Tree> { + if self.id == Some(id.clone()) { + return Some(self); + } + + for child in self.children.iter() { + if let Some(tree) = child.find(id) { + return Some(tree); + } + } + + None + } + /// Reconciliates the current tree with the provided [`Widget`]. /// /// If the tag of the [`Widget`] matches the tag of the [`Tree`], then the diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 3186662182..d867ceb9b0 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -25,4 +25,5 @@ sctk.optional = true thiserror.workspace = true iced_accessibility.workspace = true iced_accessibility.optional = true -window_clipboard.workspace = true \ No newline at end of file +window_clipboard.workspace = true +dnd.workspace = true diff --git a/runtime/src/command/action.rs b/runtime/src/command/action.rs index b8ed8d4b2b..0f412eddab 100644 --- a/runtime/src/command/action.rs +++ b/runtime/src/command/action.rs @@ -4,6 +4,7 @@ use crate::font; use crate::system; use crate::window; +use dnd::DndAction; use iced_futures::MaybeSend; use std::borrow::Cow; @@ -35,6 +36,9 @@ pub enum Action { /// Run a widget action. Widget(Box>), + /// Run a Dnd action. + Dnd(crate::dnd::DndAction), + /// Load a font from its bytes. LoadFont { /// The bytes of the font to load. @@ -78,6 +82,9 @@ impl Action { Self::PlatformSpecific(action) => { Action::PlatformSpecific(action.map(f)) } + Self::Dnd(a) => Action::Dnd(a.map(f)), + Action::LoadFont { bytes, tagger } => todo!(), + Action::PlatformSpecific(_) => todo!(), } } } @@ -99,6 +106,7 @@ impl fmt::Debug for Action { Self::PlatformSpecific(action) => { write!(f, "Action::PlatformSpecific({:?})", action) } + Self::Dnd(action) => write!(f, "Action::Dnd"), } } } diff --git a/runtime/src/dnd.rs b/runtime/src/dnd.rs new file mode 100644 index 0000000000..87d7a98411 --- /dev/null +++ b/runtime/src/dnd.rs @@ -0,0 +1,162 @@ +//! Access the clipboard. + +use std::any::Any; + +use dnd::{DndDestinationRectangle, DndSurface}; +use iced_core::clipboard::DndSource; +use iced_futures::MaybeSend; +use window_clipboard::mime::{AllowedMimeTypes, AsMimeTypes}; + +use crate::{command, Command}; + +/// An action to be performed on the system. +pub enum DndAction { + /// Register a Dnd destination. + RegisterDndDestination { + /// The surface to register. + surface: DndSurface, + /// The rectangles to register. + rectangles: Vec, + }, + /// Start a Dnd operation. + StartDnd { + /// Whether the Dnd operation is internal. + internal: bool, + /// The source surface of the Dnd operation. + source_surface: Option, + /// The icon surface of the Dnd operation. + icon_surface: Option>, + /// The content of the Dnd operation. + content: Box, + /// The actions of the Dnd operation. + actions: dnd::DndAction, + }, + /// End a Dnd operation. + EndDnd, + /// Peek the current Dnd operation. + PeekDnd(String, Box, String)>) -> T>), + /// Set the action of the Dnd operation. + SetAction(dnd::DndAction), +} + +impl std::fmt::Debug for DndAction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::RegisterDndDestination { + surface, + rectangles, + } => f + .debug_struct("RegisterDndDestination") + .field("surface", surface) + .field("rectangles", rectangles) + .finish(), + Self::StartDnd { + internal, + source_surface, + icon_surface, + content: _, + actions, + } => f + .debug_struct("StartDnd") + .field("internal", internal) + .field("source_surface", source_surface) + .field("icon_surface", icon_surface) + .field("actions", actions) + .finish(), + Self::EndDnd => f.write_str("EndDnd"), + Self::PeekDnd(mime, _) => { + f.debug_struct("PeekDnd").field("mime", mime).finish() + } + Self::SetAction(a) => f.debug_tuple("SetAction").field(a).finish(), + } + } +} + +impl DndAction { + /// Maps the output of a system [`Action`] using the provided closure. + pub fn map( + self, + f: impl Fn(T) -> A + 'static + MaybeSend + Sync, + ) -> DndAction + where + T: 'static, + { + match self { + Self::PeekDnd(m, o) => { + DndAction::PeekDnd(m, Box::new(move |d| f(o(d)))) + } + Self::EndDnd => DndAction::EndDnd, + Self::SetAction(a) => DndAction::SetAction(a), + Self::StartDnd { + internal, + source_surface, + icon_surface, + content, + actions, + } => DndAction::StartDnd { + internal, + source_surface, + icon_surface, + content, + actions, + }, + Self::RegisterDndDestination { + surface, + rectangles, + } => DndAction::RegisterDndDestination { + surface, + rectangles, + }, + } + } +} + +/// Read the current contents of the Dnd operation. +pub fn peek_dnd( + f: impl Fn(Option) -> Message + 'static, +) -> Command { + Command::single(command::Action::Dnd(DndAction::PeekDnd( + T::allowed() + .get(0) + .map_or_else(|| String::new(), |s| s.to_string()), + Box::new(move |d| f(d.and_then(|d| T::try_from(d).ok()))), + ))) +} + +/// Register a Dnd destination. +pub fn register_dnd_destination( + surface: DndSurface, + rectangles: Vec, +) -> Command { + Command::single(command::Action::Dnd(DndAction::RegisterDndDestination { + surface, + rectangles, + })) +} + +/// Start a Dnd operation. +pub fn start_dnd( + internal: bool, + source_surface: Option, + icon_surface: Option>, + content: Box, + actions: dnd::DndAction, +) -> Command { + Command::single(command::Action::Dnd(DndAction::StartDnd { + internal, + source_surface, + icon_surface, + content, + actions, + })) +} + +/// End a Dnd operation. +pub fn end_dnd() -> Command { + Command::single(command::Action::Dnd(DndAction::EndDnd)) +} + +/// Set the action of the Dnd operation. +pub fn set_action(a: dnd::DndAction) -> Command { + Command::single(command::Action::Dnd(DndAction::SetAction(a))) +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 03906f459a..eb1561d2d5 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -18,6 +18,7 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] pub mod clipboard; pub mod command; +pub mod dnd; pub mod font; pub mod keyboard; pub mod overlay; diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index e831a7f65c..5999c94db0 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -620,6 +620,11 @@ where cursor, ) } + + /// Find widget with given id + pub fn find(&self, id: &widget::Id) -> Option<&widget::Tree> { + self.state.find(id) + } } /// Reusable data of a specific [`UserInterface`]. diff --git a/sctk/src/event_loop/mod.rs b/sctk/src/event_loop/mod.rs index 06f023fe3a..9ae5aff86e 100644 --- a/sctk/src/event_loop/mod.rs +++ b/sctk/src/event_loop/mod.rs @@ -1079,11 +1079,11 @@ where 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() { + let drag_offer = match self.state.dnd_offer.as_mut().and_then(|o| o.offer.as_ref()) { Some(d) => d, None => continue, }; - drag_offer.offer.accept_mime_type(drag_offer.offer.serial, mime_type); + drag_offer.accept_mime_type(drag_offer.serial, mime_type); } platform_specific::wayland::data_device::ActionInner::StartInternalDnd { origin_id, icon_id } => { let qh = &self.state.queue_handle.clone(); @@ -1188,9 +1188,9 @@ where 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 let Some(offer) = self.state.dnd_offer.take().filter(|o| o.offer.is_some()) { if offer.dropped { - offer.offer.finish(); + offer.offer.unwrap().finish(); } else { self.state.dnd_offer = Some(offer); @@ -1212,7 +1212,10 @@ where }, 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()) { + let Some(offer) = dnd_offer.offer.as_ref() else { + continue; + }; + let read_pipe = match offer.receive(mime_type.clone()) { Ok(p) => p, Err(_) => continue, // TODO error handling }; @@ -1222,6 +1225,9 @@ where Some(s) => s, None => return PostAction::Continue, }; + let Some(offer) = dnd_offer.offer.as_ref() else { + return PostAction::Remove; + }; let (mime_type, data, token) = match dnd_offer.cur_read.take() { Some(s) => s, None => return PostAction::Continue, @@ -1231,9 +1237,9 @@ where 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() }); + state.sctk_events.push(SctkEvent::DndOffer { event: DndOfferEvent::Data { data, mime_type }, surface: dnd_offer.surface.clone() }); if dnd_offer.dropped { - dnd_offer.offer.finish(); + offer.finish(); } else { state.dnd_offer = Some(dnd_offer); } @@ -1269,8 +1275,8 @@ where } } 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); + if let Some(offer) = self.state.dnd_offer.as_ref().and_then(|o| o.offer.as_ref()) { + offer.set_actions(accepted, preferred); } } } diff --git a/sctk/src/event_loop/state.rs b/sctk/src/event_loop/state.rs index fa151f1f2c..8ee524e5f3 100644 --- a/sctk/src/event_loop/state.rs +++ b/sctk/src/event_loop/state.rs @@ -248,8 +248,9 @@ impl Debug for Dnd { #[derive(Debug)] pub struct SctkDragOffer { pub(crate) dropped: bool, - pub(crate) offer: DragOffer, + pub(crate) offer: Option, pub(crate) cur_read: Option<(String, Vec, RegistrationToken)>, + pub(crate) surface: WlSurface, } #[derive(Debug)] diff --git a/sctk/src/handlers/data_device/data_device.rs b/sctk/src/handlers/data_device/data_device.rs index 5eb4b36747..e7d950363e 100644 --- a/sctk/src/handlers/data_device/data_device.rs +++ b/sctk/src/handlers/data_device/data_device.rs @@ -2,7 +2,10 @@ use sctk::{ data_device_manager::{ data_device::DataDeviceHandler, data_offer::DragOffer, }, - reexports::client::{protocol::wl_data_device, Connection, QueueHandle}, + reexports::client::{ + protocol::{wl_data_device, wl_surface::WlSurface}, + Connection, QueueHandle, + }, }; use crate::{ @@ -16,6 +19,9 @@ impl DataDeviceHandler for SctkState { _conn: &Connection, _qh: &QueueHandle, wl_data_device: &wl_data_device::WlDataDevice, + x: f64, + y: f64, + s: &WlSurface, ) { let data_device = if let Some(seat) = self .seats @@ -27,20 +33,20 @@ impl DataDeviceHandler for SctkState { return; }; - let drag_offer = data_device.data().drag_offer().unwrap(); - let mime_types = drag_offer.with_mime_types(|types| types.to_vec()); + let drag_offer = data_device.data().drag_offer(); + let mime_types = drag_offer + .as_ref() + .map(|offer| offer.with_mime_types(|types| types.to_vec())) + .unwrap_or_default(); self.dnd_offer = Some(SctkDragOffer { dropped: false, offer: drag_offer.clone(), cur_read: None, + surface: s.clone(), }); self.sctk_events.push(SctkEvent::DndOffer { - event: DndOfferEvent::Enter { - mime_types, - x: drag_offer.x, - y: drag_offer.y, - }, - surface: drag_offer.surface.clone(), + event: DndOfferEvent::Enter { mime_types, x, y }, + surface: s.clone(), }); } @@ -62,7 +68,7 @@ impl DataDeviceHandler for SctkState { self.sctk_events.push(SctkEvent::DndOffer { event: DndOfferEvent::Leave, - surface: dnd_offer.offer.surface.clone(), + surface: dnd_offer.surface.clone(), }); } } @@ -72,6 +78,8 @@ impl DataDeviceHandler for SctkState { _conn: &Connection, _qh: &QueueHandle, wl_data_device: &wl_data_device::WlDataDevice, + x: f64, + y: f64, ) { let data_device = if let Some(seat) = self .seats @@ -85,14 +93,19 @@ impl DataDeviceHandler for SctkState { 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) { + if offer.as_ref() + != self.dnd_offer.as_ref().and_then(|o| o.offer.as_ref()) + { return; } - let DragOffer { x, y, surface, .. } = - data_device.data().drag_offer().unwrap(); + + let Some(surface) = offer.as_ref().map(|o| o.surface.clone()) else { + return; + }; + self.sctk_events.push(SctkEvent::DndOffer { event: DndOfferEvent::Motion { x, y }, - surface: surface.clone(), + surface, }); } @@ -121,17 +134,15 @@ impl DataDeviceHandler for SctkState { 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; + if let Some(dnd_offer) = self.dnd_offer.as_mut() { + if data_device.data().drag_offer() != dnd_offer.offer { + return; } self.sctk_events.push(SctkEvent::DndOffer { event: DndOfferEvent::DropPerformed, - surface: offer.surface.clone(), + surface: dnd_offer.surface.clone(), }); + dnd_offer.dropped = true; } } } diff --git a/sctk/src/handlers/data_device/data_offer.rs b/sctk/src/handlers/data_device/data_offer.rs index 2901c07038..5ef6381ceb 100644 --- a/sctk/src/handlers/data_device/data_offer.rs +++ b/sctk/src/handlers/data_device/data_offer.rs @@ -18,7 +18,7 @@ impl DataOfferHandler for SctkState { if self .dnd_offer .as_ref() - .map(|o| o.offer.inner() == offer.inner()) + .map(|o| o.offer.as_ref().map(|o| o.inner()) == Some(offer.inner())) .unwrap_or(false) { self.sctk_events @@ -41,7 +41,7 @@ impl DataOfferHandler for SctkState { if self .dnd_offer .as_ref() - .map(|o| o.offer.inner() == offer.inner()) + .map(|o| o.offer.as_ref().map(|o| o.inner()) == Some(offer.inner())) .unwrap_or(false) { self.sctk_events diff --git a/sctk/src/handlers/seat/keyboard.rs b/sctk/src/handlers/seat/keyboard.rs index c150c077bf..a7a865a4a3 100644 --- a/sctk/src/handlers/seat/keyboard.rs +++ b/sctk/src/handlers/seat/keyboard.rs @@ -172,6 +172,7 @@ impl KeyboardHandler for SctkState { keyboard: &sctk::reexports::client::protocol::wl_keyboard::WlKeyboard, _serial: u32, modifiers: sctk::seat::keyboard::Modifiers, + layout: u32, ) { let (is_active, my_seat) = match self.seats.iter_mut().enumerate().find_map(|(i, s)| { diff --git a/src/application.rs b/src/application.rs index e3988e793a..db04ef772d 100644 --- a/src/application.rs +++ b/src/application.rs @@ -97,7 +97,7 @@ pub trait Application: Sized { type Executor: Executor; /// The type of __messages__ your [`Application`] will produce. - type Message: std::fmt::Debug + Send; + type Message: std::fmt::Debug + Send + Sync + 'static; /// The theme of your [`Application`]. type Theme: Default + StyleSheet; diff --git a/src/lib.rs b/src/lib.rs index 3b8241b7a4..90fdfdf4e4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -225,8 +225,9 @@ pub use crate::core::{ pub mod clipboard { //! Access the clipboard. pub use crate::runtime::clipboard::{read, write}; + pub use dnd; pub use iced_core::clipboard::{read_data, read_primary_data}; - pub use window_clipboard::mime; + pub use mime; } pub mod executor { diff --git a/src/sandbox.rs b/src/sandbox.rs index 65a48519e2..9ccbebb4dc 100644 --- a/src/sandbox.rs +++ b/src/sandbox.rs @@ -84,7 +84,7 @@ use crate::{Application, Command, Element, Error, Settings, Subscription}; /// ``` pub trait Sandbox { /// The type of __messages__ your [`Sandbox`] will produce. - type Message: std::fmt::Debug + Send; + type Message: std::fmt::Debug + Send + Sync + 'static; /// Initializes the [`Sandbox`]. /// @@ -155,6 +155,7 @@ pub trait Sandbox { impl Application for T where T: Sandbox, + T::Message: Send + Sync + 'static, { type Executor = iced_futures::backend::null::Executor; type Flags = (); diff --git a/widget/Cargo.toml b/widget/Cargo.toml index 64dde4be8a..fa26061721 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -35,6 +35,8 @@ sctk.optional = true num-traits.workspace = true thiserror.workspace = true unicode-segmentation.workspace = true +window_clipboard.workspace = true +dnd.workspace = true ouroboros.workspace = true ouroboros.optional = true diff --git a/winit/Cargo.toml b/winit/Cargo.toml index c9da88e7d7..a32bdac83d 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -33,6 +33,7 @@ log.workspace = true thiserror.workspace = true tracing.workspace = true window_clipboard.workspace = true +dnd.workspace = true winit.workspace = true sysinfo.workspace = true diff --git a/winit/src/application.rs b/winit/src/application.rs index afc0b2fe44..76fb85d47a 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -2,19 +2,28 @@ mod drag_resize; mod state; +use dnd::DndAction; +use dnd::DndEvent; +use dnd::DndSurface; +use dnd::Icon; #[cfg(feature = "a11y")] use iced_graphics::core::widget::operation::focusable::focus; use iced_graphics::core::widget::operation::OperationWrapper; use iced_graphics::core::widget::Operation; +use iced_graphics::Viewport; use iced_runtime::futures::futures::FutureExt; +use iced_style::core::clipboard::DndSource; use iced_style::core::Clipboard as CoreClipboard; +use iced_style::core::Length; pub use state::State; +use window_clipboard::mime; use window_clipboard::mime::ClipboardStoreData; use crate::conversion; use crate::core; use crate::core::mouse; use crate::core::renderer; +use crate::core::renderer::Renderer; use crate::core::time::Instant; use crate::core::widget::operation; use crate::core::window; @@ -31,6 +40,7 @@ use crate::{Clipboard, Error, Proxy, Settings}; use futures::channel::mpsc; use futures::stream::StreamExt; +use std::any::Any; use std::mem::ManuallyDrop; use std::sync::Arc; @@ -39,7 +49,6 @@ 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 @@ -50,6 +59,49 @@ pub enum UserEventWrapper { #[cfg(feature = "a11y")] /// A11y was enabled A11yEnabled, + /// CLipboard Message + StartDnd { + /// internal dnd + internal: bool, + /// the surface the dnd is started from + source_surface: Option, + /// the icon if any + /// This is actually an Element + icon_surface: Option>, + /// the content of the dnd + content: Box, + /// the actions of the dnd + actions: DndAction, + }, + /// Dnd Event + Dnd(DndEvent), +} + +unsafe impl Send for UserEventWrapper {} +unsafe impl Sync for UserEventWrapper {} + +impl std::fmt::Debug for UserEventWrapper { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + UserEventWrapper::Message(m) => write!(f, "Message({:?})", m), + #[cfg(feature = "a11y")] + UserEventWrapper::A11y(a) => write!(f, "A11y({:?})", a), + #[cfg(feature = "a11y")] + UserEventWrapper::A11yEnabled => write!(f, "A11yEnabled"), + UserEventWrapper::StartDnd { + internal, + source_surface: _, + icon_surface, + content: _, + actions, + } => write!( + f, + "StartDnd {{ internal: {:?}, icon_surface: {}, actions: {:?} }}", + internal, icon_surface.is_some(), actions + ), + UserEventWrapper::Dnd(_) => write!(f, "Dnd"), + } + } } #[cfg(feature = "a11y")] @@ -143,6 +195,7 @@ where E: Executor + 'static, C: Compositor + 'static, A::Theme: StyleSheet, + ::Message: Sync, { use futures::task; use futures::Future; @@ -305,6 +358,7 @@ async fn run_instance( E: Executor + 'static, C: Compositor + 'static, A::Theme: StyleSheet, + A::Message: Send + Sync + 'static, { use winit::event; use winit::event_loop::ControlFlow; @@ -313,7 +367,8 @@ async fn run_instance( let mut viewport_version = state.viewport_version(); let physical_size = state.physical_size(); - let mut clipboard = Clipboard::connect(&window); + let mut clipboard = + Clipboard::connect(&window, crate::proxy::Proxy::new(proxy.clone())); let mut cache = user_interface::Cache::default(); let mut surface = compositor.create_surface( window.clone(), @@ -444,6 +499,111 @@ async fn run_instance( } #[cfg(feature = "a11y")] UserEventWrapper::A11yEnabled => a11y_enabled = true, + UserEventWrapper::StartDnd { + internal, + source_surface: _, // not needed if there is only one window + icon_surface, + content, + actions, + } => { + let mut renderer = compositor.create_renderer(); + let icon_surface = icon_surface + .map(|i| { + let i: Box = i; + i + }) + .and_then(|i| { + i.downcast::, + core::widget::tree::State, + )>>() + .ok() + }) + .map(|e| { + let e = Arc::into_inner(*e).unwrap(); + let (mut e, widget_state) = e; + let lim = core::layout::Limits::new( + Size::new(1., 1.), + Size::new( + state.viewport().physical_width() + as f32, + state.viewport().physical_height() + as f32, + ), + ); + + let mut tree = core::widget::Tree { + id: e.as_widget().id(), + tag: e.as_widget().tag(), + state: widget_state, + children: e.as_widget().children(), + }; + + let size = e + .as_widget() + .layout(&mut tree, &renderer, &lim); + e.as_widget_mut().diff(&mut tree); + + let size = lim.resolve( + Length::Shrink, + Length::Shrink, + size.size(), + ); + let mut surface = compositor.create_surface( + window.clone(), + size.width.ceil() as u32, + size.height.ceil() as u32, + ); + let viewport = Viewport::with_logical_size( + size, + state.viewport().scale_factor(), + ); + + let mut ui = UserInterface::build( + e, + size, + user_interface::Cache::default(), + &mut renderer, + ); + _ = ui.draw( + &mut renderer, + state.theme(), + &renderer::Style { + icon_color: state.icon_color(), + text_color: state.text_color(), + scale_factor: state.scale_factor(), + }, + Default::default(), + ); + let bytes = compositor.screenshot( + &mut renderer, + &mut surface, + &viewport, + state.background_color(), + &debug.overlay(), + ); + Icon::Buffer { + data: Arc::new(bytes), + width: viewport.physical_width(), + height: viewport.physical_height(), + transparent: false, + } + }); + + clipboard.start_dnd_winit( + internal, + DndSurface(Arc::new(Box::new(window.clone()))), + icon_surface, + content, + actions, + ); + } + UserEventWrapper::Dnd(e) => events.push(Event::Dnd(e)), }; } event::Event::WindowEvent { @@ -818,7 +978,7 @@ pub fn update( Proxy>, UserEventWrapper, >, - clipboard: &mut Clipboard, + clipboard: &mut Clipboard, should_exit: &mut bool, proxy: &mut winit::event_loop::EventLoopProxy>, debug: &mut Debug, @@ -876,7 +1036,7 @@ pub fn run_command( Proxy>, UserEventWrapper, >, - clipboard: &mut Clipboard, + clipboard: &mut Clipboard, should_exit: &mut bool, proxy: &mut winit::event_loop::EventLoopProxy>, debug: &mut Debug, @@ -1137,6 +1297,40 @@ pub fn run_command( .expect("Send message to event loop"); } command::Action::PlatformSpecific(_) => todo!(), + command::Action::Dnd(a) => match a { + iced_runtime::dnd::DndAction::RegisterDndDestination { + surface, + rectangles, + } => { + clipboard.register_dnd_destination(surface, rectangles); + } + iced_runtime::dnd::DndAction::StartDnd { + internal, + source_surface, + icon_surface, + content, + actions, + } => clipboard.start_dnd( + internal, + source_surface, + icon_surface, + content, + actions, + ), + iced_runtime::dnd::DndAction::EndDnd => { + clipboard.end_dnd(); + } + iced_runtime::dnd::DndAction::PeekDnd(m, to_msg) => { + let data = clipboard.peek_dnd(m); + let message = to_msg(data); + proxy + .send_event(UserEventWrapper::Message(message)) + .expect("Send message to event loop"); + } + iced_runtime::dnd::DndAction::SetAction(a) => { + clipboard.set_action(a); + } + }, } } } diff --git a/winit/src/clipboard.rs b/winit/src/clipboard.rs index 019384f583..3183f0ea5d 100644 --- a/winit/src/clipboard.rs +++ b/winit/src/clipboard.rs @@ -1,34 +1,52 @@ //! Access the clipboard. -use window_clipboard::mime::{self, ClipboardStoreData}; +use std::{any::Any, borrow::Cow}; + +use crate::futures::futures::Sink; +use dnd::{DndAction, DndDestinationRectangle, DndSurface, Icon}; +use iced_style::core::clipboard::DndSource; +use window_clipboard::{ + dnd::DndProvider, + mime::{self, ClipboardData, ClipboardStoreData}, +}; + +use crate::{application::UserEventWrapper, Proxy}; /// A buffer for short-term storage and transfer within and between /// applications. #[allow(missing_debug_implementations)] -pub struct Clipboard { - state: State, +pub struct Clipboard { + state: State, } -enum State { - Connected(window_clipboard::Clipboard), +enum State { + Connected(window_clipboard::Clipboard, Proxy>), Unavailable, } -impl Clipboard { +impl Clipboard { /// Creates a new [`Clipboard`] for the given window. - pub fn connect(window: &winit::window::Window) -> Clipboard { + pub fn connect( + window: &winit::window::Window, + proxy: Proxy>, + ) -> Clipboard { #[allow(unsafe_code)] let state = unsafe { window_clipboard::Clipboard::connect(window) } .ok() - .map(State::Connected) + .map(|c| (c, proxy.clone())) + .map(|c| State::Connected(c.0, c.1)) .unwrap_or(State::Unavailable); + if let State::Connected(clipboard, _) = &state { + clipboard.init_dnd(Box::new(proxy)); + } + Clipboard { state } } /// Creates a new [`Clipboard`] that isn't associated with a window. /// This clipboard will never contain a copied value. - pub fn unconnected() -> Clipboard { + pub fn unconnected() -> Clipboard { Clipboard { state: State::Unavailable, } @@ -37,7 +55,7 @@ impl Clipboard { /// Reads the current content of the [`Clipboard`] as text. pub fn read(&self) -> Option { match &self.state { - State::Connected(clipboard) => clipboard.read().ok(), + State::Connected(clipboard, _) => clipboard.read().ok(), State::Unavailable => None, } } @@ -45,7 +63,7 @@ impl Clipboard { /// Writes the given text contents to the [`Clipboard`]. pub fn write(&mut self, contents: String) { match &mut self.state { - State::Connected(clipboard) => match clipboard.write(contents) { + State::Connected(clipboard, _) => match clipboard.write(contents) { Ok(()) => {} Err(error) => { log::warn!("error writing to clipboard: {error}"); @@ -54,9 +72,32 @@ impl Clipboard { State::Unavailable => {} } } + + // + pub(crate) fn start_dnd_winit( + &self, + internal: bool, + source_surface: DndSurface, + icon_surface: Option, + content: Box, + actions: DndAction, + ) { + match &self.state { + State::Connected(clipboard, _) => { + _ = clipboard.start_dnd( + internal, + source_surface, + icon_surface, + content, + actions, + ) + } + State::Unavailable => {} + } + } } -impl crate::core::Clipboard for Clipboard { +impl crate::core::Clipboard for Clipboard { fn read(&self) -> Option { self.read() } @@ -67,7 +108,7 @@ impl crate::core::Clipboard for Clipboard { fn read_primary(&self) -> Option { match &self.state { - State::Connected(clipboard) => { + State::Connected(clipboard, _) => { clipboard.read_primary().and_then(|res| res.ok()) } State::Unavailable => None, @@ -76,7 +117,7 @@ impl crate::core::Clipboard for Clipboard { fn write_primary(&mut self, contents: String) { match &mut self.state { - State::Connected(clipboard) => { + State::Connected(clipboard, _) => { _ = clipboard.write_primary(contents) } State::Unavailable => {} @@ -85,7 +126,7 @@ impl crate::core::Clipboard for Clipboard { fn read_data(&self, mimes: Vec) -> Option<(Vec, String)> { match &self.state { - State::Connected(clipboard) => { + State::Connected(clipboard, _) => { clipboard.read_raw(mimes).and_then(|res| res.ok()) } State::Unavailable => None, @@ -99,7 +140,9 @@ impl crate::core::Clipboard for Clipboard { >, ) { match &mut self.state { - State::Connected(clipboard) => _ = clipboard.write_data(contents), + State::Connected(clipboard, _) => { + _ = clipboard.write_data(contents) + } State::Unavailable => {} } } @@ -109,7 +152,7 @@ impl crate::core::Clipboard for Clipboard { mimes: Vec, ) -> Option<(Vec, String)> { match &self.state { - State::Connected(clipboard) => { + State::Connected(clipboard, _) => { clipboard.read_primary_raw(mimes).and_then(|res| res.ok()) } State::Unavailable => None, @@ -123,10 +166,69 @@ impl crate::core::Clipboard for Clipboard { >, ) { match &mut self.state { - State::Connected(clipboard) => { + State::Connected(clipboard, _) => { _ = clipboard.write_primary_data(contents) } State::Unavailable => {} } } + + fn start_dnd( + &self, + internal: bool, + source_surface: Option, + icon_surface: Option>, + content: Box, + actions: DndAction, + ) { + match &self.state { + State::Connected(_, tx) => { + tx.raw.send_event(UserEventWrapper::StartDnd { + internal, + source_surface, + icon_surface, + content, + actions, + }); + } + State::Unavailable => {} + } + } + + fn register_dnd_destination( + &self, + surface: DndSurface, + rectangles: Vec, + ) { + match &self.state { + State::Connected(clipboard, _) => { + _ = clipboard.register_dnd_destination(surface, rectangles) + } + State::Unavailable => {} + } + } + + fn end_dnd(&self) { + match &self.state { + State::Connected(clipboard, _) => _ = clipboard.end_dnd(), + State::Unavailable => {} + } + } + + fn peek_dnd(&self, mime: String) -> Option<(Vec, String)> { + match &self.state { + State::Connected(clipboard, _) => clipboard + .peek_offer::(Some(Cow::Owned(mime))) + .ok() + .map(|res| (res.0, res.1)), + State::Unavailable => None, + } + } + + fn set_action(&self, action: DndAction) { + match &self.state { + State::Connected(clipboard, _) => _ = clipboard.set_action(action), + State::Unavailable => {} + } + } } diff --git a/winit/src/lib.rs b/winit/src/lib.rs index 948576a28a..622e9396a2 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -22,7 +22,6 @@ missing_debug_implementations, missing_docs, unused_results, - unsafe_code, rustdoc::broken_intra_doc_links )] #![cfg_attr(docsrs, feature(doc_auto_cfg))] diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index a07ae9934b..fb6da4074c 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -12,6 +12,7 @@ use crate::core::renderer; use crate::core::widget::operation; use crate::core::widget::Operation; use crate::core::window; +use crate::core::Clipboard as CoreClipboard; use crate::core::Size; use crate::futures::futures::channel::mpsc; use crate::futures::futures::{task, Future, StreamExt}; @@ -26,12 +27,17 @@ use crate::runtime::user_interface::{self, UserInterface}; use crate::runtime::Debug; use crate::style::application::StyleSheet; use crate::{Clipboard, Error, Proxy, Settings}; -use crate::core::Clipboard as CoreClipboard; +use dnd::DndAction; +use dnd::DndSurface; +use dnd::Icon; +use iced_graphics::Viewport; use iced_runtime::futures::futures::FutureExt; use iced_style::Theme; pub use state::State; use window_clipboard::mime::ClipboardStoreData; +use winit::raw_window_handle::HasWindowHandle; +use std::any::Any; use std::collections::HashMap; use std::mem::ManuallyDrop; use std::sync::Arc; @@ -127,6 +133,7 @@ where A: Application + 'static, E: Executor + 'static, C: Compositor + 'static, + A::Message: Send + Sync + 'static, A::Theme: StyleSheet, { use winit::event_loop::EventLoopBuilder; @@ -350,6 +357,7 @@ async fn run_instance( A: Application + 'static, E: Executor + 'static, C: Compositor + 'static, + A::Message: Send + Sync + 'static, A::Theme: StyleSheet, { use winit::event; @@ -363,7 +371,8 @@ async fn run_instance( main_window.raw.set_visible(true); } - let mut clipboard = Clipboard::connect(&main_window.raw); + let mut clipboard = + Clipboard::connect(&main_window.raw, Proxy::new(proxy.clone())); #[cfg(feature = "a11y")] let (window_a11y_id, adapter, mut a11y_enabled) = { @@ -446,6 +455,8 @@ async fn run_instance( debug.startup_finished(); + let mut cur_dnd_surface: Option = None; + 'main: while let Some(event) = event_receiver.next().await { match event { Event::WindowCreated { @@ -971,6 +982,192 @@ async fn run_instance( UserEventWrapper::A11yEnabled => { a11y_enabled = true } + UserEventWrapper::StartDnd { + internal, + source_surface, + icon_surface, + content, + actions, + } => { + let Some(window_id) = + source_surface.and_then(|source| { + match source { + core::clipboard::DndSource::Surface( + s, + ) => Some(s), + core::clipboard::DndSource::Widget( + w, + ) => { + // search windows for widget + user_interfaces.iter().find_map( + |(id, ui)| { + if ui. + find(&w).is_some() + { + Some(*id) + } else { + None + } + }, + ) + }, + } + }) + else { + continue; + }; + + let Some(window) = + window_manager.get_mut(window_id) + else { + continue; + }; + let state = &window.state; + + let icon_surface = icon_surface + .map(|i| { + let i: Box = i; + i + }) + .and_then(|i| { + i.downcast::, + >>( + ) + .ok() + }) + .map(|e| { + let size = e.as_widget().size(); + let lim = core::layout::Limits::new( + Size::new(1., 1.), + Size::new( + state + .viewport() + .physical_width() + as f32, + state + .viewport() + .physical_height() + as f32, + ), + ); + let size = lim.resolve( + size.width, + size.height, + Size::ZERO, + ); + let mut surface = compositor + .create_surface( + window.raw.clone(), + size.width.ceil() as u32, + size.height.ceil() as u32, + ); + let viewport = + Viewport::with_logical_size( + size, + state.viewport().scale_factor(), + ); + let mut renderer = + compositor.create_renderer(); + + let e = Arc::into_inner(*e).unwrap(); + let mut ui = UserInterface::build( + e, + size, + user_interface::Cache::default(), + &mut renderer, + ); + _ = ui.draw( + &mut renderer, + state.theme(), + &renderer::Style { + icon_color: state.icon_color(), + text_color: state.text_color(), + scale_factor: state + .scale_factor(), + }, + Default::default(), + ); + let bytes = compositor.screenshot( + &mut renderer, + &mut surface, + &viewport, + state.background_color(), + &debug.overlay(), + ); + Icon::Buffer { + data: Arc::new(bytes), + width: viewport.physical_width(), + height: viewport.physical_height(), + transparent: false, + } + }); + + clipboard.start_dnd_winit( + internal, + DndSurface(Arc::new(Box::new( + window.raw.clone(), + ))), + icon_surface, + content, + actions, + ); + } + UserEventWrapper::Dnd(e) => match &e { + dnd::DndEvent::Offer( + _, + dnd::OfferEvent::Leave, + ) => { + events.push(( + cur_dnd_surface, + core::Event::Dnd(e), + )); + cur_dnd_surface = None; + } + dnd::DndEvent::Offer( + _, + dnd::OfferEvent::Enter { surface, .. }, + ) => { + let window_handle = + surface.0.window_handle().ok(); + let window_id = window_manager + .iter_mut() + .find_map(|(id, window)| { + if window + .raw + .window_handle() + .ok() + .zip(window_handle) + .map(|(a, b)| a == b) + .unwrap_or_default() + { + Some(id) + } else { + None + } + }); + + cur_dnd_surface = window_id; + events.push(( + cur_dnd_surface, + core::Event::Dnd(e), + )); + } + dnd::DndEvent::Offer(..) => { + events.push(( + cur_dnd_surface, + core::Event::Dnd(e), + )); + } + dnd::DndEvent::Source(_) => { + events.push((None, core::Event::Dnd(e))) + } + }, }; } event::Event::WindowEvent { @@ -1059,7 +1256,7 @@ fn update( Proxy>, UserEventWrapper, >, - clipboard: &mut Clipboard, + clipboard: &mut Clipboard, control_sender: &mut mpsc::UnboundedSender, proxy: &mut winit::event_loop::EventLoopProxy>, debug: &mut Debug, @@ -1068,6 +1265,7 @@ fn update( ui_caches: &mut HashMap, ) where C: Compositor + 'static, + A::Message: Send + Sync + 'static, A::Theme: StyleSheet, { for message in messages.drain(..) { @@ -1108,7 +1306,7 @@ fn run_command( Proxy>, UserEventWrapper, >, - clipboard: &mut Clipboard, + clipboard: &mut Clipboard, control_sender: &mut mpsc::UnboundedSender, proxy: &mut winit::event_loop::EventLoopProxy>, @@ -1119,6 +1317,7 @@ fn run_command( A: Application, E: Executor, C: Compositor + 'static, + A::Message: Send + Sync + 'static, A::Theme: StyleSheet, { use crate::runtime::clipboard; @@ -1442,6 +1641,40 @@ fn run_command( command::Action::PlatformSpecific(_) => { tracing::warn!("Platform specific commands are not supported yet in multi-window winit mode."); } + command::Action::Dnd(a) => match a { + iced_runtime::dnd::DndAction::RegisterDndDestination { + surface, + rectangles, + } => { + clipboard.register_dnd_destination(surface, rectangles); + } + iced_runtime::dnd::DndAction::StartDnd { + internal, + source_surface, + icon_surface, + content, + actions, + } => clipboard.start_dnd( + internal, + source_surface, + icon_surface, + content, + actions, + ), + iced_runtime::dnd::DndAction::EndDnd => { + clipboard.end_dnd(); + } + iced_runtime::dnd::DndAction::PeekDnd(m, to_msg) => { + let data = clipboard.peek_dnd(m); + let message = to_msg(data); + proxy + .send_event(UserEventWrapper::Message(message)) + .expect("Send message to event loop"); + } + iced_runtime::dnd::DndAction::SetAction(a) => { + clipboard.set_action(a); + } + }, } } } diff --git a/winit/src/proxy.rs b/winit/src/proxy.rs index 1d6c48bb25..19f0502a26 100644 --- a/winit/src/proxy.rs +++ b/winit/src/proxy.rs @@ -1,14 +1,19 @@ -use crate::futures::futures::{ - channel::mpsc, - task::{Context, Poll}, - Sink, +use dnd::{DndEvent, DndSurface}; + +use crate::{ + application::UserEventWrapper, + futures::futures::{ + channel::mpsc, + task::{Context, Poll}, + Sink, + }, }; use std::pin::Pin; /// An event loop proxy that implements `Sink`. #[derive(Debug)] pub struct Proxy { - raw: winit::event_loop::EventLoopProxy, + pub(crate) raw: winit::event_loop::EventLoopProxy, } impl Clone for Proxy { @@ -59,3 +64,19 @@ impl Sink for Proxy { Poll::Ready(Ok(())) } } + +impl dnd::Sender for Proxy> { + fn send( + &self, + event: DndEvent, + ) -> Result<(), std::sync::mpsc::SendError>> { + self.raw + .send_event(UserEventWrapper::Dnd(event)) + .map_err(|_err| { + std::sync::mpsc::SendError(DndEvent::Offer( + None, + dnd::OfferEvent::Leave, + )) + }) + } +} From 232c2292cfe7b37b71f5246f3facfa5ec064b651 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 29 Mar 2024 16:36:27 -0400 Subject: [PATCH 107/178] fix: doc --- runtime/src/dnd.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/src/dnd.rs b/runtime/src/dnd.rs index 87d7a98411..8acf9efb51 100644 --- a/runtime/src/dnd.rs +++ b/runtime/src/dnd.rs @@ -73,7 +73,7 @@ impl std::fmt::Debug for DndAction { } impl DndAction { - /// Maps the output of a system [`Action`] using the provided closure. + /// Maps the output of a [`DndAction`] using the provided closure. pub fn map( self, f: impl Fn(T) -> A + 'static + MaybeSend + Sync, From bc2d15471a7f3ee2cfc8aaaca90c6987a2d70fd0 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 29 Mar 2024 20:07:52 -0400 Subject: [PATCH 108/178] fix: color format & multi-window --- src/multi_window/application.rs | 2 +- winit/src/application.rs | 6 ++++- winit/src/multi_window.rs | 43 +++++++++++++++++++++++---------- 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/src/multi_window/application.rs b/src/multi_window/application.rs index ac625281c6..bb78ed5ba1 100644 --- a/src/multi_window/application.rs +++ b/src/multi_window/application.rs @@ -73,7 +73,7 @@ pub trait Application: Sized { type Executor: Executor; /// The type of __messages__ your [`Application`] will produce. - type Message: std::fmt::Debug + Send; + type Message: std::fmt::Debug + Send + Sync + 'static; /// The theme of your [`Application`]. type Theme: Default + StyleSheet; diff --git a/winit/src/application.rs b/winit/src/application.rs index 76fb85d47a..8c2c0a61c5 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -580,13 +580,17 @@ async fn run_instance( }, Default::default(), ); - let bytes = compositor.screenshot( + let mut bytes = compositor.screenshot( &mut renderer, &mut surface, &viewport, state.background_color(), &debug.overlay(), ); + for pix in bytes.chunks_exact_mut(4) { + // rgba -> argb little endian + pix.swap(0, 2); + } Icon::Buffer { data: Arc::new(bytes), width: viewport.physical_width(), diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index fb6da4074c..88ec01618f 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -27,11 +27,11 @@ use crate::runtime::user_interface::{self, UserInterface}; use crate::runtime::Debug; use crate::style::application::StyleSheet; use crate::{Clipboard, Error, Proxy, Settings}; -use dnd::DndAction; use dnd::DndSurface; use dnd::Icon; use iced_graphics::Viewport; use iced_runtime::futures::futures::FutureExt; +use iced_style::core::Length; use iced_style::Theme; pub use state::State; use window_clipboard::mime::ClipboardStoreData; @@ -1023,26 +1023,30 @@ async fn run_instance( continue; }; let state = &window.state; - let icon_surface = icon_surface .map(|i| { let i: Box = i; i }) .and_then(|i| { - i.downcast::, - >>( + core::widget::tree::State, + )>>( ) .ok() }) .map(|e| { - let size = e.as_widget().size(); + let mut renderer = + compositor.create_renderer(); + + let e = Arc::into_inner(*e).unwrap(); + let (mut e, widget_state) = e; let lim = core::layout::Limits::new( Size::new(1., 1.), Size::new( @@ -1056,10 +1060,23 @@ async fn run_instance( as f32, ), ); + + let mut tree = core::widget::Tree { + id: e.as_widget().id(), + tag: e.as_widget().tag(), + state: widget_state, + children: e.as_widget().children(), + }; + + let size = e + .as_widget() + .layout(&mut tree, &renderer, &lim); + e.as_widget_mut().diff(&mut tree); + let size = lim.resolve( - size.width, - size.height, - Size::ZERO, + Length::Shrink, + Length::Shrink, + size.size(), ); let mut surface = compositor .create_surface( @@ -1072,10 +1089,6 @@ async fn run_instance( size, state.viewport().scale_factor(), ); - let mut renderer = - compositor.create_renderer(); - - let e = Arc::into_inner(*e).unwrap(); let mut ui = UserInterface::build( e, size, @@ -1093,13 +1106,17 @@ async fn run_instance( }, Default::default(), ); - let bytes = compositor.screenshot( + let mut bytes = compositor.screenshot( &mut renderer, &mut surface, &viewport, state.background_color(), &debug.overlay(), ); + for pix in bytes.chunks_exact_mut(4) { + // rgba -> argb little endian + pix.swap(0, 2); + } Icon::Buffer { data: Arc::new(bytes), width: viewport.physical_width(), From a441d81cef71d207afe0160706546f8a51f8aa1e Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 1 Apr 2024 14:30:11 -0400 Subject: [PATCH 109/178] feat: update advertised drag destinations after rebuilding an interface --- core/src/clipboard.rs | 48 +++++++++++++++++++++++- core/src/widget.rs | 10 +++++ runtime/src/user_interface.rs | 15 ++++++++ widget/src/column.rs | 17 +++++++++ widget/src/container.rs | 17 +++++++++ widget/src/mouse_area.rs | 17 +++++++++ widget/src/row.rs | 17 +++++++++ widget/src/scrollable.rs | 31 +++++++++++++++ winit/src/application.rs | 19 +++++++++- winit/src/multi_window.rs | 42 ++++++++++++++------- winit/src/multi_window/window_manager.rs | 2 + 11 files changed, 220 insertions(+), 15 deletions(-) diff --git a/core/src/clipboard.rs b/core/src/clipboard.rs index c97fa6e09f..d6bd8e896c 100644 --- a/core/src/clipboard.rs +++ b/core/src/clipboard.rs @@ -1,6 +1,6 @@ //! Access the clipboard. -use std::{any::Any, borrow::Cow, sync::Arc}; +use std::{any::Any, sync::Arc}; use dnd::{DndAction, DndDestinationRectangle, DndSurface}; use mime::{self, AllowedMimeTypes, AsMimeTypes, ClipboardStoreData}; @@ -165,3 +165,49 @@ pub enum DndSource { /// A surface is the source of the DnD operation. Surface(window::Id), } + +/// A list of DnD destination rectangles. +#[derive(Debug, Clone)] +pub struct DndDestinationRectangles { + /// The rectangle of the DnD destination. + rectangles: Vec, +} + +impl DndDestinationRectangles { + /// Creates a new [`DestinationRectangle`]. + pub fn new() -> Self { + Self { + rectangles: Vec::new(), + } + } + + /// Creates a new [`DestinationRectangle`] with the given capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self { + rectangles: Vec::with_capacity(capacity), + } + } + + /// Pushes a new rectangle to the list of DnD destination rectangles. + pub fn push(&mut self, rectangle: DndDestinationRectangle) { + self.rectangles.push(rectangle); + } + + /// Appends the list of DnD destination rectangles to the current list. + pub fn append(&mut self, other: &mut Vec) { + self.rectangles.append(other); + } + + /// Returns the list of DnD destination rectangles. + /// This consumes the [`DestinationRectangles`]. + pub fn into_rectangles(mut self) -> Vec { + self.rectangles.reverse(); + self.rectangles + } +} + +impl AsRef<[DndDestinationRectangle]> for DndDestinationRectangles { + fn as_ref(&self) -> &[DndDestinationRectangle] { + &self.rectangles + } +} diff --git a/core/src/widget.rs b/core/src/widget.rs index 136eeae471..fdbadb97e2 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -167,4 +167,14 @@ where /// Sets the id of the widget /// This may be called while diffing the widget tree fn set_id(&mut self, _id: Id) {} + + /// Adds the drag destination rectangles of the widget. + /// Runs after the layout phase for each widget in the tree. + fn drag_destinations( + &self, + _state: &Tree, + _layout: Layout<'_>, + _dnd_rectangles: &mut crate::clipboard::DndDestinationRectangles, + ) { + } } diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index 5999c94db0..8bfb1e1dd5 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -1,4 +1,5 @@ //! Implement your own event loop to drive a user interface. +use iced_core::clipboard::DndDestinationRectangles; use iced_core::widget::{Operation, OperationOutputWrapper}; use crate::core::event::{self, Event}; @@ -625,6 +626,20 @@ where pub fn find(&self, id: &widget::Id) -> Option<&widget::Tree> { self.state.find(id) } + + /// Get the destination rectangles for the user interface. + pub fn dnd_rectangles( + &self, + prev_capacity: usize, + ) -> DndDestinationRectangles { + let ret = DndDestinationRectangles::with_capacity(prev_capacity); + self.root.as_widget().drag_destinations( + &self.state, + Layout::new(&self.base), + &mut ret.clone(), + ); + ret + } } /// Reusable data of a specific [`UserInterface`]. diff --git a/widget/src/column.rs b/widget/src/column.rs index f16caac998..560a04eb85 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -284,6 +284,23 @@ where }), ) } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles, + ) { + for ((e, layout), state) in self + .children + .iter() + .zip(layout.children()) + .zip(state.children.iter()) + { + e.as_widget() + .drag_destinations(state, layout, dnd_rectangles); + } + } } impl<'a, Message, Theme, Renderer> From> diff --git a/widget/src/container.rs b/widget/src/container.rs index fdd0389567..7be86d8693 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -309,6 +309,23 @@ where cursor, ) } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles, + ) { + if let Some((layout, state)) = + layout.children().zip(state.children.iter()).next() + { + self.content.as_widget().drag_destinations( + state, + layout, + dnd_rectangles, + ); + } + } } impl<'a, Message, Theme, Renderer> From> diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index 5b93d88eb1..fa694dd307 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -244,6 +244,23 @@ where renderer, ) } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles, + ) { + if let Some((layout, state)) = + layout.children().zip(state.children.iter()).next() + { + self.content.as_widget().drag_destinations( + state, + layout, + dnd_rectangles, + ); + } + } } impl<'a, Message, Theme, Renderer> From> diff --git a/widget/src/row.rs b/widget/src/row.rs index 9970c047ac..0be2734580 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -273,6 +273,23 @@ where }), ) } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles, + ) { + for ((e, layout), state) in self + .children + .iter() + .zip(layout.children()) + .zip(state.children.iter()) + { + e.as_widget() + .drag_destinations(state, layout, dnd_rectangles); + } + } } impl<'a, Message, Theme, Renderer> From> diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 5916e40c75..e20c95ba22 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; +use iced_style::core::clipboard::DndDestinationRectangles; #[cfg(feature = "a11y")] use std::borrow::Cow; @@ -588,6 +589,36 @@ where } } } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles, + ) { + if let Some((c_layout, c_state)) = + layout.children().zip(state.children.iter()).next() + { + let mut my_dnd_rectangles = DndDestinationRectangles::new(); + self.content.as_widget().drag_destinations( + c_state, + layout, + &mut my_dnd_rectangles, + ); + let mut my_dnd_rectangles = my_dnd_rectangles.into_rectangles(); + + let bounds = layout.bounds(); + let content_bounds = c_layout.bounds(); + let state = state.state.downcast_ref::(); + for r in &mut my_dnd_rectangles { + let translation = + state.translation(self.direction, bounds, content_bounds); + r.rectangle.x -= translation.x as f64; + r.rectangle.y -= translation.y as f64; + } + dnd_rectangles.append(&mut my_dnd_rectangles); + } + } } impl<'a, Message, Theme, Renderer> diff --git a/winit/src/application.rs b/winit/src/application.rs index 8c2c0a61c5..583685a1e1 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -23,7 +23,6 @@ use crate::conversion; use crate::core; use crate::core::mouse; use crate::core::renderer; -use crate::core::renderer::Renderer; use crate::core::time::Instant; use crate::core::widget::operation; use crate::core::window; @@ -411,6 +410,8 @@ async fn run_instance( &mut debug, )); + let mut prev_dnd_rectangles_count = 0; + // Creates closure for handling the window drag resize state with winit. let mut drag_resize_window_func = drag_resize::event_func( &window, @@ -893,6 +894,22 @@ async fn run_instance( &mut debug, )); + let dnd_rectangles = user_interface + .dnd_rectangles(prev_dnd_rectangles_count); + let new_dnd_rectangles_count = + dnd_rectangles.as_ref().len(); + + if new_dnd_rectangles_count > 0 + || prev_dnd_rectangles_count > 0 + { + clipboard.register_dnd_destination( + DndSurface(Arc::new(Box::new(window.clone()))), + dnd_rectangles.into_rectangles(), + ); + } + + prev_dnd_rectangles_count = new_dnd_rectangles_count; + if should_exit { break; } diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 88ec01618f..306526696b 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -18,7 +18,6 @@ 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}; @@ -32,7 +31,6 @@ use dnd::Icon; use iced_graphics::Viewport; use iced_runtime::futures::futures::FutureExt; use iced_style::core::Length; -use iced_style::Theme; pub use state::State; use window_clipboard::mime::ClipboardStoreData; use winit::raw_window_handle::HasWindowHandle; @@ -429,6 +427,7 @@ async fn run_instance( window::Id::MAIN, user_interface::Cache::default(), )]), + &mut clipboard, )); run_command( @@ -861,6 +860,7 @@ async fn run_instance( &mut debug, &mut window_manager, cached_interfaces, + &mut clipboard, )); } @@ -1604,6 +1604,7 @@ fn run_command( debug, window_manager, std::mem::take(ui_caches), + clipboard, ); while let Some(mut operation) = current_operation.take() { @@ -1702,6 +1703,7 @@ pub fn build_user_interfaces<'a, A: Application, C: Compositor>( debug: &mut Debug, window_manager: &mut WindowManager, mut cached_user_interfaces: HashMap, + clipboard: &mut Clipboard, ) -> HashMap> where A::Theme: StyleSheet, @@ -1711,18 +1713,32 @@ where .drain() .filter_map(|(id, cache)| { let window = window_manager.get_mut(id)?; - - Some(( + let interface = build_user_interface( + application, + cache, + &mut window.renderer, + window.state.logical_size(), + debug, id, - build_user_interface( - application, - cache, - &mut window.renderer, - window.state.logical_size(), - debug, - id, - ), - )) + ); + + let dnd_rectangles = interface + .dnd_rectangles(window.prev_dnd_destination_rectangles_count); + let new_dnd_rectangles_count = dnd_rectangles.as_ref().len(); + + if new_dnd_rectangles_count > 0 + || window.prev_dnd_destination_rectangles_count > 0 + { + clipboard.register_dnd_destination( + DndSurface(Arc::new(Box::new(window.raw.clone()))), + dnd_rectangles.into_rectangles(), + ); + } + + window.prev_dnd_destination_rectangles_count = + new_dnd_rectangles_count; + + Some((id, interface)) }) .collect() } diff --git a/winit/src/multi_window/window_manager.rs b/winit/src/multi_window/window_manager.rs index 8b070da174..0a67a01580 100644 --- a/winit/src/multi_window/window_manager.rs +++ b/winit/src/multi_window/window_manager.rs @@ -69,6 +69,7 @@ where surface, renderer, mouse_interaction: mouse::Interaction::Idle, + prev_dnd_destination_rectangles_count: 0, }, ); @@ -142,6 +143,7 @@ where ) -> bool, >, >, + pub prev_dnd_destination_rectangles_count: usize, pub mouse_interaction: mouse::Interaction, pub surface: C::Surface, pub renderer: A::Renderer, From 015a3253bfd85495f938bb4ced06854d5cd46207 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 1 Apr 2024 14:36:20 -0400 Subject: [PATCH 110/178] fix: docs --- core/src/clipboard.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/clipboard.rs b/core/src/clipboard.rs index d6bd8e896c..206e6e1e78 100644 --- a/core/src/clipboard.rs +++ b/core/src/clipboard.rs @@ -174,14 +174,14 @@ pub struct DndDestinationRectangles { } impl DndDestinationRectangles { - /// Creates a new [`DestinationRectangle`]. + /// Creates a new [`DndDestinationRectangles`]. pub fn new() -> Self { Self { rectangles: Vec::new(), } } - /// Creates a new [`DestinationRectangle`] with the given capacity. + /// Creates a new [`DndDestinationRectangles`] with the given capacity. pub fn with_capacity(capacity: usize) -> Self { Self { rectangles: Vec::with_capacity(capacity), @@ -199,7 +199,7 @@ impl DndDestinationRectangles { } /// Returns the list of DnD destination rectangles. - /// This consumes the [`DestinationRectangles`]. + /// This consumes the [`DndDestinationRectangles`]. pub fn into_rectangles(mut self) -> Vec { self.rectangles.reverse(); self.rectangles From 5b9f77b4ac043213fd913fbc20cba18f36a680ab Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 1 Apr 2024 14:46:51 -0400 Subject: [PATCH 111/178] fix: pass correct state and layout for container widgets --- widget/src/container.rs | 14 +++++--------- widget/src/mouse_area.rs | 4 +--- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/widget/src/container.rs b/widget/src/container.rs index 7be86d8693..24e25396aa 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -316,15 +316,11 @@ where layout: Layout<'_>, dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles, ) { - if let Some((layout, state)) = - layout.children().zip(state.children.iter()).next() - { - self.content.as_widget().drag_destinations( - state, - layout, - dnd_rectangles, - ); - } + self.content.as_widget().drag_destinations( + state, + layout, + dnd_rectangles, + ); } } diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index fa694dd307..1f040b740b 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -251,9 +251,7 @@ where layout: Layout<'_>, dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles, ) { - if let Some((layout, state)) = - layout.children().zip(state.children.iter()).next() - { + if let Some(state) = state.children.iter().next() { self.content.as_widget().drag_destinations( state, layout, From 46ef2e2a2c1d133e4988133369a9cb9fa3bd1b19 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 1 Apr 2024 17:42:37 -0400 Subject: [PATCH 112/178] refactor: remove Sync bound for Message --- src/application.rs | 2 +- src/multi_window/application.rs | 2 +- winit/src/application.rs | 4 +--- winit/src/clipboard.rs | 2 +- winit/src/multi_window.rs | 8 ++++---- 5 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/application.rs b/src/application.rs index db04ef772d..629c874fdc 100644 --- a/src/application.rs +++ b/src/application.rs @@ -97,7 +97,7 @@ pub trait Application: Sized { type Executor: Executor; /// The type of __messages__ your [`Application`] will produce. - type Message: std::fmt::Debug + Send + Sync + 'static; + type Message: std::fmt::Debug + Send + 'static; /// The theme of your [`Application`]. type Theme: Default + StyleSheet; diff --git a/src/multi_window/application.rs b/src/multi_window/application.rs index bb78ed5ba1..56db2e20bf 100644 --- a/src/multi_window/application.rs +++ b/src/multi_window/application.rs @@ -73,7 +73,7 @@ pub trait Application: Sized { type Executor: Executor; /// The type of __messages__ your [`Application`] will produce. - type Message: std::fmt::Debug + Send + Sync + 'static; + type Message: std::fmt::Debug + Send + 'static; /// The theme of your [`Application`]. type Theme: Default + StyleSheet; diff --git a/winit/src/application.rs b/winit/src/application.rs index 583685a1e1..72528264a3 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -77,7 +77,6 @@ pub enum UserEventWrapper { } unsafe impl Send for UserEventWrapper {} -unsafe impl Sync for UserEventWrapper {} impl std::fmt::Debug for UserEventWrapper { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -194,7 +193,6 @@ where E: Executor + 'static, C: Compositor + 'static, A::Theme: StyleSheet, - ::Message: Sync, { use futures::task; use futures::Future; @@ -357,7 +355,7 @@ async fn run_instance( E: Executor + 'static, C: Compositor + 'static, A::Theme: StyleSheet, - A::Message: Send + Sync + 'static, + A::Message: Send + 'static, { use winit::event; use winit::event_loop::ControlFlow; diff --git a/winit/src/clipboard.rs b/winit/src/clipboard.rs index 3183f0ea5d..bf4a1e3050 100644 --- a/winit/src/clipboard.rs +++ b/winit/src/clipboard.rs @@ -24,7 +24,7 @@ enum State { Unavailable, } -impl Clipboard { +impl Clipboard { /// Creates a new [`Clipboard`] for the given window. pub fn connect( window: &winit::window::Window, diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 306526696b..4fcfaea836 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -131,7 +131,7 @@ where A: Application + 'static, E: Executor + 'static, C: Compositor + 'static, - A::Message: Send + Sync + 'static, + A::Message: Send + 'static, A::Theme: StyleSheet, { use winit::event_loop::EventLoopBuilder; @@ -355,7 +355,7 @@ async fn run_instance( A: Application + 'static, E: Executor + 'static, C: Compositor + 'static, - A::Message: Send + Sync + 'static, + A::Message: Send + 'static, A::Theme: StyleSheet, { use winit::event; @@ -1282,7 +1282,7 @@ fn update( ui_caches: &mut HashMap, ) where C: Compositor + 'static, - A::Message: Send + Sync + 'static, + A::Message: Send + 'static, A::Theme: StyleSheet, { for message in messages.drain(..) { @@ -1334,7 +1334,7 @@ fn run_command( A: Application, E: Executor, C: Compositor + 'static, - A::Message: Send + Sync + 'static, + A::Message: Send + 'static, A::Theme: StyleSheet, { use crate::runtime::clipboard; From 745d8467647209a9929fe631b2ba784d9cd39832 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Wed, 3 Apr 2024 08:05:03 -0700 Subject: [PATCH 113/178] sctk: Fix handling of DnD with subsurfaces (#122) Map subsurface to parent and add offset. --- sctk/src/application.rs | 9 ++++++++- sctk/src/sctk_event.rs | 14 ++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/sctk/src/application.rs b/sctk/src/application.rs index 333e6b3316..8a289173d1 100644 --- a/sctk/src/application.rs +++ b/sctk/src/application.rs @@ -2275,7 +2275,14 @@ where | SctkEvent::UpdateOutput { .. } | SctkEvent::RemovedOutput(_) => false, SctkEvent::ScaleFactorChanged { id, .. } => &id.id() == object_id, - SctkEvent::DndOffer { surface, .. } => &surface.id() == object_id, + SctkEvent::DndOffer { surface, .. } => { + let event_object_id = surface.id(); + &event_object_id == object_id + || state + .subsurfaces + .iter() + .any(|s| s.wl_surface.id() == event_object_id) + } SctkEvent::DataSource(_) => true, SctkEvent::SessionLocked => false, SctkEvent::SessionLockFinished => false, diff --git a/sctk/src/sctk_event.rs b/sctk/src/sctk_event.rs index 3d9a3c6da3..b7af4c841b 100755 --- a/sctk/src/sctk_event.rs +++ b/sctk/src/sctk_event.rs @@ -785,7 +785,7 @@ impl SctkEvent { id: _, inner_size: _, } => Default::default(), - SctkEvent::DndOffer { event, .. } => match event { + SctkEvent::DndOffer { event, surface } => match event { DndOfferEvent::Enter { mime_types, x, y } => { Some(iced_runtime::core::Event::PlatformSpecific( PlatformSpecific::Wayland(wayland::Event::DndOffer( @@ -796,9 +796,19 @@ impl SctkEvent { .collect() } DndOfferEvent::Motion { x, y } => { + let offset = if let Some((x_offset, y_offset, _)) = + subsurface_ids.get(&surface.id()) + { + (*x_offset, *y_offset) + } else { + (0, 0) + }; Some(iced_runtime::core::Event::PlatformSpecific( PlatformSpecific::Wayland(wayland::Event::DndOffer( - wayland::DndOfferEvent::Motion { x, y }, + wayland::DndOfferEvent::Motion { + x: x + offset.0 as f64, + y: y + offset.1 as f64, + }, )), )) .into_iter() From f944754e1e06022cce4fa46bcec41ed4ca6a46b5 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 4 Apr 2024 19:34:22 -0400 Subject: [PATCH 114/178] fixes for dnd --- core/src/element.rs | 20 ++++++++++++++++++++ runtime/src/user_interface.rs | 4 ++-- widget/src/container.rs | 20 +++++++++++++++----- widget/src/lazy.rs | 15 +++++++++++++++ widget/src/lazy/component.rs | 15 +++++++++++++++ widget/src/lazy/responsive.rs | 13 +++++++++++++ widget/src/scrollable.rs | 13 ++++++++----- winit/src/application.rs | 4 ++-- winit/src/multi_window.rs | 8 +++++--- 9 files changed, 95 insertions(+), 17 deletions(-) diff --git a/core/src/element.rs b/core/src/element.rs index 12047d660a..74a0f9e928 100644 --- a/core/src/element.rs +++ b/core/src/element.rs @@ -501,6 +501,15 @@ where fn set_id(&mut self, id: Id) { self.widget.set_id(id); } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + dnd_rectangles: &mut crate::clipboard::DndDestinationRectangles, + ) { + self.widget.drag_destinations(state, layout, dnd_rectangles); + } } struct Explain<'a, Message, Theme, Renderer: crate::Renderer> { @@ -653,5 +662,16 @@ where fn set_id(&mut self, id: Id) { self.element.widget.set_id(id); } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + dnd_rectangles: &mut crate::clipboard::DndDestinationRectangles, + ) { + self.element + .widget + .drag_destinations(state, layout, dnd_rectangles); + } // TODO maybe a11y_nodes } diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index 8bfb1e1dd5..50d84fe02c 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -632,11 +632,11 @@ where &self, prev_capacity: usize, ) -> DndDestinationRectangles { - let ret = DndDestinationRectangles::with_capacity(prev_capacity); + let mut ret = DndDestinationRectangles::with_capacity(prev_capacity); self.root.as_widget().drag_destinations( &self.state, Layout::new(&self.base), - &mut ret.clone(), + &mut ret, ); ret } diff --git a/widget/src/container.rs b/widget/src/container.rs index 24e25396aa..be221bc1d3 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -316,11 +316,21 @@ where layout: Layout<'_>, dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles, ) { - self.content.as_widget().drag_destinations( - state, - layout, - dnd_rectangles, - ); + if let Some(l) = layout.children().next() { + self.content.as_widget().drag_destinations( + state, + l, + dnd_rectangles, + ); + } + } + + fn id(&self) -> Option { + self.id.clone() + } + + fn set_id(&mut self, id: Id) { + self.id = Some(id); } } diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index bd35bdcd00..354e0e1600 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -308,6 +308,21 @@ where } None } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + dnd_rectangles: &mut core::clipboard::DndDestinationRectangles, + ) { + self.with_element(|element| { + element.as_widget().drag_destinations( + &state.children[0], + layout, + dnd_rectangles, + ); + }); + } } #[self_referencing] diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index 06ae31c9a3..0c2b9d6216 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -540,6 +540,21 @@ where } }) } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + dnd_rectangles: &mut core::clipboard::DndDestinationRectangles, + ) { + self.with_element(|element| { + element.as_widget().drag_destinations( + &state.children[0], + layout, + dnd_rectangles, + ) + }); + } } struct Overlay<'a, 'b, Message, Theme, Renderer, Event, S>( diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index 946fb159b6..009ee7533d 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -354,6 +354,19 @@ where .as_widget_mut() .set_id(_id); } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + dnd_rectangles: &mut core::clipboard::DndDestinationRectangles, + ) { + self.content.borrow().element.as_widget().drag_destinations( + state, + layout, + dnd_rectangles, + ); + } } impl<'a, Message, Theme, Renderer> diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index e20c95ba22..36b8826b39 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -592,12 +592,13 @@ where fn drag_destinations( &self, - state: &Tree, + tree: &Tree, layout: Layout<'_>, dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles, ) { + let my_state = tree.state.downcast_ref::(); if let Some((c_layout, c_state)) = - layout.children().zip(state.children.iter()).next() + layout.children().zip(tree.children.iter()).next() { let mut my_dnd_rectangles = DndDestinationRectangles::new(); self.content.as_widget().drag_destinations( @@ -609,10 +610,12 @@ where let bounds = layout.bounds(); let content_bounds = c_layout.bounds(); - let state = state.state.downcast_ref::(); for r in &mut my_dnd_rectangles { - let translation = - state.translation(self.direction, bounds, content_bounds); + let translation = my_state.translation( + self.direction, + bounds, + content_bounds, + ); r.rectangle.x -= translation.x as f64; r.rectangle.y -= translation.y as f64; } diff --git a/winit/src/application.rs b/winit/src/application.rs index 72528264a3..dc1979c7f4 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -583,7 +583,7 @@ async fn run_instance( &mut renderer, &mut surface, &viewport, - state.background_color(), + core::Color::TRANSPARENT, &debug.overlay(), ); for pix in bytes.chunks_exact_mut(4) { @@ -594,7 +594,7 @@ async fn run_instance( data: Arc::new(bytes), width: viewport.physical_width(), height: viewport.physical_height(), - transparent: false, + transparent: true, } }); diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 4fcfaea836..1c4e48255a 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -1014,14 +1014,17 @@ async fn run_instance( } }) else { + eprintln!("No source surface"); continue; }; let Some(window) = window_manager.get_mut(window_id) else { + eprintln!("No window"); continue; }; + let state = &window.state; let icon_surface = icon_surface .map(|i| { @@ -1110,7 +1113,7 @@ async fn run_instance( &mut renderer, &mut surface, &viewport, - state.background_color(), + core::Color::TRANSPARENT, &debug.overlay(), ); for pix in bytes.chunks_exact_mut(4) { @@ -1121,7 +1124,7 @@ async fn run_instance( data: Arc::new(bytes), width: viewport.physical_width(), height: viewport.physical_height(), - transparent: false, + transparent: true, } }); @@ -1725,7 +1728,6 @@ where let dnd_rectangles = interface .dnd_rectangles(window.prev_dnd_destination_rectangles_count); let new_dnd_rectangles_count = dnd_rectangles.as_ref().len(); - if new_dnd_rectangles_count > 0 || window.prev_dnd_destination_rectangles_count > 0 { From 52187b687876d17fe95441cc27ff42af0a7e70b4 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 4 Apr 2024 19:42:32 -0400 Subject: [PATCH 115/178] chore: update tag --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 42e9826f2a..4d404cb33b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -192,9 +192,9 @@ web-time = "0.2" # Newer wgpu commit that fixes Vulkan backend on Nvidia wgpu = { git = "https://github.com/gfx-rs/wgpu", rev = "20fda69" } winapi = "0.3" -window_clipboard = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd" } -dnd = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd" } -mime = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd" } +window_clipboard = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-2" } +dnd = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-2" } +mime = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-2" } # window_clipboard = { path = "../window_clipboard" } # dnd = { path = "../window_clipboard/dnd" } # mime = { path = "../window_clipboard/mime" } From d549c5db84a1d332dc0444526abedf012f37e43b Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 5 Apr 2024 09:43:49 -0600 Subject: [PATCH 116/178] Call unlock on session lock --- sctk/src/event_loop/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sctk/src/event_loop/mod.rs b/sctk/src/event_loop/mod.rs index 9ae5aff86e..477e147b17 100644 --- a/sctk/src/event_loop/mod.rs +++ b/sctk/src/event_loop/mod.rs @@ -1339,7 +1339,9 @@ where } } platform_specific::wayland::session_lock::Action::Unlock => { - self.state.session_lock.take(); + if let Some(session_lock) = self.state.session_lock.take() { + session_lock.unlock(); + } // Make sure server processes unlock before client exits let _ = self.state.connection.roundtrip(); sticky_exit_callback( From a77c6c1291f2ffadb2bdf15533bd0bd9ceaf4299 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 5 Apr 2024 13:57:55 -0600 Subject: [PATCH 117/178] Clean up after lock surfaces are destroyed --- sctk/src/application.rs | 14 ++++++++++++++ sctk/src/event_loop/mod.rs | 10 +++++++++- sctk/src/sctk_event.rs | 4 ++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/sctk/src/application.rs b/sctk/src/application.rs index 8a289173d1..d21fdea77e 100644 --- a/sctk/src/application.rs +++ b/sctk/src/application.rs @@ -787,6 +787,17 @@ where } } + SctkEvent::SessionLockSurfaceDone { surface } => { + if let Some(surface_id) = surface_ids.remove(&surface.id()) { + if kbd_surface_id == Some(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(surface.id(), surface_id); + } + } _ => {} } } @@ -2292,6 +2303,9 @@ where SctkEvent::SessionLockSurfaceConfigure { surface, .. } => { &surface.id() == object_id } + SctkEvent::SessionLockSurfaceDone { surface } => { + &surface.id() == object_id + } SctkEvent::SessionUnlocked => false, } } diff --git a/sctk/src/event_loop/mod.rs b/sctk/src/event_loop/mod.rs index 477e147b17..9774b3db92 100644 --- a/sctk/src/event_loop/mod.rs +++ b/sctk/src/event_loop/mod.rs @@ -1368,7 +1368,15 @@ where s.id == id }) { - self.state.lock_surfaces.remove(i); + let surface = self.state.lock_surfaces.remove(i); + sticky_exit_callback( + IcedSctkEvent::SctkEvent(SctkEvent::SessionLockSurfaceDone { + surface: surface.session_lock_surface.wl_surface().clone() + }), + &self.state, + &mut control_flow, + &mut callback, + ); } } } diff --git a/sctk/src/sctk_event.rs b/sctk/src/sctk_event.rs index b7af4c841b..95bdf2ba9c 100755 --- a/sctk/src/sctk_event.rs +++ b/sctk/src/sctk_event.rs @@ -206,6 +206,9 @@ pub enum SctkEvent { configure: SessionLockSurfaceConfigure, first: bool, }, + SessionLockSurfaceDone { + surface: WlSurface, + }, SessionUnlocked, } @@ -947,6 +950,7 @@ impl SctkEvent { } SctkEvent::SessionLockSurfaceCreated { .. } => vec![], SctkEvent::SessionLockSurfaceConfigure { .. } => vec![], + SctkEvent::SessionLockSurfaceDone { .. } => vec![], SctkEvent::SessionUnlocked => { Some(iced_runtime::core::Event::PlatformSpecific( PlatformSpecific::Wayland(wayland::Event::SessionLock( From 5680ad0aa75d6a89e660c890517d6978f2408e49 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 5 Apr 2024 12:14:46 -0400 Subject: [PATCH 118/178] fix(container): id and set_id should use content --- widget/src/container.rs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/widget/src/container.rs b/widget/src/container.rs index be221bc1d3..984ec06b3a 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -29,7 +29,6 @@ pub struct Container< Theme: StyleSheet, Renderer: crate::core::Renderer, { - id: Option, padding: Padding, width: Length, height: Length, @@ -55,7 +54,6 @@ where let size = content.as_widget().size_hint(); Container { - id: None, padding: Padding::ZERO, width: size.width.fluid(), height: size.height.fluid(), @@ -68,12 +66,6 @@ where } } - /// Sets the [`Id`] of the [`Container`]. - pub fn id(mut self, id: Id) -> Self { - self.id = Some(id); - self - } - /// Sets the [`Padding`] of the [`Container`]. pub fn padding>(mut self, padding: P) -> Self { self.padding = padding.into(); @@ -191,7 +183,7 @@ where operation: &mut dyn Operation>, ) { operation.container( - self.id.as_ref(), + self.content.as_widget().id().as_ref(), layout.bounds(), &mut |operation| { self.content.as_widget().operate( @@ -326,11 +318,11 @@ where } fn id(&self) -> Option { - self.id.clone() + self.content.as_widget().id().clone() } fn set_id(&mut self, id: Id) { - self.id = Some(id); + self.content.as_widget_mut().set_id(id); } } From da635373fb2235ed2c5d39e56cec9f302efb2439 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 5 Apr 2024 16:10:40 -0400 Subject: [PATCH 119/178] fix(scrollable): pass child layout when calculating drag destinations --- widget/src/scrollable.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 36b8826b39..211826e4db 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -603,7 +603,7 @@ where let mut my_dnd_rectangles = DndDestinationRectangles::new(); self.content.as_widget().drag_destinations( c_state, - layout, + c_layout, &mut my_dnd_rectangles, ); let mut my_dnd_rectangles = my_dnd_rectangles.into_rectangles(); From f689948cb9506b438cef51dc7bec49c80d3fe2ca Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 5 Apr 2024 17:23:41 -0400 Subject: [PATCH 120/178] fix(winit multi-window): handle exit_on_close request --- winit/src/multi_window.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 1c4e48255a..76cb2b8eef 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -1203,9 +1203,8 @@ async fn run_instance( if matches!( window_event, winit::event::WindowEvent::CloseRequested - ) && window.exit_on_close_request - { - let _ = window_manager.remove(id); + ) { + let w = window_manager.remove(id); let _ = user_interfaces.remove(&id); let _ = ui_caches.remove(&id); @@ -1214,7 +1213,9 @@ async fn run_instance( core::Event::Window(id, window::Event::Closed), )); - if window_manager.is_empty() { + if window_manager.is_empty() + && w.is_some_and(|w| w.exit_on_close_request) + { break 'main; } } else { @@ -1402,10 +1403,12 @@ fn run_command( .expect("Send control action"); } window::Action::Close(id) => { - let _ = window_manager.remove(id); + let w = window_manager.remove(id); let _ = ui_caches.remove(&id); - if window_manager.is_empty() { + if window_manager.is_empty() + && w.is_some_and(|w| w.exit_on_close_request) + { control_sender .start_send(Control::Exit) .expect("Send control action"); From ab89ac144dca06ae657f99dbc74e1ae0015b302c Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 9 Apr 2024 14:37:14 -0400 Subject: [PATCH 121/178] fix: translate offer positions in scrollable --- widget/src/scrollable.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 211826e4db..faa5032e5e 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -1,4 +1,5 @@ //! Navigate an endless amount of content with a scrollbar. +use dnd::DndEvent; use iced_runtime::core::widget::Id; use iced_style::core::clipboard::DndDestinationRectangles; #[cfg(feature = "a11y")] @@ -733,6 +734,20 @@ pub fn update( let translation = state.translation(direction, bounds, content_bounds); + if let Event::Dnd(DndEvent::Offer(_, e)) = &mut event { + match e { + dnd::OfferEvent::Enter { x, y, .. } => { + *x += f64::from(translation.x); + *y += f64::from(translation.y); + } + dnd::OfferEvent::Motion { x, y } => { + *x += f64::from(translation.x); + *y += f64::from(translation.y); + } + _ => {} + } + } + #[cfg(feature = "wayland")] if let Event::PlatformSpecific( iced_runtime::core::event::PlatformSpecific::Wayland(e), From 1add6cc89fa1b1af9c391c9e8fd7334c01ab7322 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 9 Apr 2024 14:40:20 -0400 Subject: [PATCH 122/178] chore: update tag --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4d404cb33b..f306061e97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -192,9 +192,9 @@ web-time = "0.2" # Newer wgpu commit that fixes Vulkan backend on Nvidia wgpu = { git = "https://github.com/gfx-rs/wgpu", rev = "20fda69" } winapi = "0.3" -window_clipboard = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-2" } -dnd = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-2" } -mime = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-2" } +window_clipboard = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-3" } +dnd = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-3" } +mime = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-3" } # window_clipboard = { path = "../window_clipboard" } # dnd = { path = "../window_clipboard/dnd" } # mime = { path = "../window_clipboard/mime" } From f149055d1d6b8ba68f12400a1cd593149e73a2a7 Mon Sep 17 00:00:00 2001 From: Mattias Eriksson Date: Wed, 10 Apr 2024 07:55:46 +0200 Subject: [PATCH 123/178] Add read_primary/write_primary --- runtime/src/clipboard.rs | 14 ++++++++++++++ src/lib.rs | 4 +++- winit/src/clipboard.rs | 26 ++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/runtime/src/clipboard.rs b/runtime/src/clipboard.rs index 1e78c61249..9978f4ac62 100644 --- a/runtime/src/clipboard.rs +++ b/runtime/src/clipboard.rs @@ -94,6 +94,20 @@ pub fn write(contents: String) -> Command { Command::single(command::Action::Clipboard(Action::Write(contents))) } +/// Read the current contents of primary. +pub fn read_primary( + f: impl Fn(Option) -> Message + 'static, +) -> Command { + Command::single(command::Action::Clipboard(Action::ReadPrimary(Box::new( + f, + )))) +} + +/// Write the given contents to primary. +pub fn write_primary(contents: String) -> Command { + Command::single(command::Action::Clipboard(Action::WritePrimary(contents))) +} + /// Read the current contents of the clipboard. pub fn read_data( f: impl Fn(Option) -> Message + 'static, diff --git a/src/lib.rs b/src/lib.rs index 90fdfdf4e4..5178165e8e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -224,7 +224,9 @@ pub use crate::core::{ pub mod clipboard { //! Access the clipboard. - pub use crate::runtime::clipboard::{read, write}; + pub use crate::runtime::clipboard::{ + read, read_primary, write, write_primary, + }; pub use dnd; pub use iced_core::clipboard::{read_data, read_primary_data}; pub use mime; diff --git a/winit/src/clipboard.rs b/winit/src/clipboard.rs index bf4a1e3050..d95a8957b0 100644 --- a/winit/src/clipboard.rs +++ b/winit/src/clipboard.rs @@ -73,6 +73,32 @@ impl Clipboard { } } + /// Reads the current content of the Primary as text. + pub fn read_primary(&self) -> Option { + match &self.state { + State::Connected(clipboard, _) => { + clipboard.read_primary().and_then(|res| res.ok()) + } + State::Unavailable => None, + } + } + + /// Writes the given text contents to the Primary. + pub fn write_primary(&mut self, contents: String) { + match &mut self.state { + State::Connected(clipboard, _) => { + match clipboard.write_primary(contents) { + Some(Ok(())) => {} + Some(Err(error)) => { + log::warn!("error writing to clipboard: {error}"); + } + None => {} //Primary not available + } + } + State::Unavailable => {} + } + } + // pub(crate) fn start_dnd_winit( &self, From b7e7021acfa747bc62c70301a58602bb57de9148 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 11 Apr 2024 00:31:57 -0400 Subject: [PATCH 124/178] fix(tiny-skia): non-simple border scaling the issue can be seen with sharp corners when using the screenshot portal with scaling --- Cargo.toml | 2 +- core/src/keyboard/modifiers.rs | 2 +- tiny_skia/src/backend.rs | 37 ++++++++++++++++++++++------------ 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f306061e97..5df1c50530 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -149,7 +149,7 @@ iced_sctk = { version = "0.1", path = "sctk" } iced_accessibility = { version = "0.1", path = "accessibility" } async-std = "1.0" -bitflags = "1.0" +bitflags = "2.5" bytemuck = { version = "1.0", features = ["derive"] } cosmic-text = { git = "https://github.com/pop-os/cosmic-text.git" } futures = "0.3" diff --git a/core/src/keyboard/modifiers.rs b/core/src/keyboard/modifiers.rs index bbdd827270..3670fdaa8e 100644 --- a/core/src/keyboard/modifiers.rs +++ b/core/src/keyboard/modifiers.rs @@ -2,7 +2,7 @@ use bitflags::bitflags; bitflags! { /// The current state of the keyboard modifiers. - #[derive(Default)] + #[derive(Default, Debug, PartialEq, Eq, Clone, Copy)] pub struct Modifiers: u32{ /// The "shift" key. const SHIFT = 0b100; diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index ce94b3d0c7..4d93e90f99 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -379,26 +379,32 @@ impl Backend { clip_mask, ); } else { + let transform = tiny_skia::Transform::from_translate( + translation.x, + translation.y, + ); + // Draw corners that have too small border radii as having no border radius, // but mask them with the rounded rectangle with the correct border radius. let mut temp_pixmap = tiny_skia::Pixmap::new( - bounds.width as u32, - bounds.height as u32, + physical_bounds.width as u32, + physical_bounds.height as u32, ) .unwrap(); let mut quad_mask = tiny_skia::Mask::new( - bounds.width as u32, - bounds.height as u32, + physical_bounds.width as u32, + physical_bounds.height as u32, ) .unwrap(); let zero_bounds = Rectangle { x: 0.0, y: 0.0, - width: bounds.width, - height: bounds.height, + width: physical_bounds.width, + height: physical_bounds.height, }; + let path = rounded_rectangle(zero_bounds, fill_border_radius); @@ -409,12 +415,17 @@ impl Backend { transform, ); let path_bounds = Rectangle { - x: border_width / 2.0, - y: border_width / 2.0, - width: bounds.width - border_width, - height: bounds.height - border_width, + x: (border_width / 2.0) * scale_factor, + y: (border_width / 2.0) * scale_factor, + width: physical_bounds.width + - border_width * scale_factor, + height: physical_bounds.height + - border_width * scale_factor, }; + for r in &mut border_radius { + *r /= scale_factor; + } let border_radius_path = rounded_rectangle(path_bounds, border_radius); @@ -428,7 +439,7 @@ impl Backend { ..tiny_skia::Paint::default() }, &tiny_skia::Stroke { - width: border_width, + width: border_width * scale_factor, ..tiny_skia::Stroke::default() }, transform, @@ -436,8 +447,8 @@ impl Backend { ); pixels.draw_pixmap( - bounds.x as i32, - bounds.y as i32, + (bounds.x / scale_factor) as i32, + (bounds.y / scale_factor) as i32, temp_pixmap.as_ref(), &tiny_skia::PixmapPaint::default(), transform, From c87978d8973feeac57f32647676f01d6a02aaf7a Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Sat, 13 Apr 2024 08:45:37 -0600 Subject: [PATCH 125/178] Update window_clipboard to pop-dnd-4 --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5df1c50530..39261f6832 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -192,9 +192,9 @@ web-time = "0.2" # Newer wgpu commit that fixes Vulkan backend on Nvidia wgpu = { git = "https://github.com/gfx-rs/wgpu", rev = "20fda69" } winapi = "0.3" -window_clipboard = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-3" } -dnd = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-3" } -mime = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-3" } +window_clipboard = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-4" } +dnd = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-4" } +mime = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-4" } # window_clipboard = { path = "../window_clipboard" } # dnd = { path = "../window_clipboard/dnd" } # mime = { path = "../window_clipboard/mime" } From 3fe3a619b04786bdb4b3b67fa354b96ef693f017 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 15 Apr 2024 13:06:58 -0400 Subject: [PATCH 126/178] slider & toggler roundness --- style/src/slider.rs | 2 ++ style/src/theme.rs | 5 ++++ style/src/toggler.rs | 8 ++++++- widget/src/slider.rs | 11 +++++++-- widget/src/toggler.rs | 45 ++++++++++++----------------------- widget/src/vertical_slider.rs | 3 ++- 6 files changed, 40 insertions(+), 34 deletions(-) diff --git a/style/src/slider.rs b/style/src/slider.rs index d1e427028c..07dc6be4cf 100644 --- a/style/src/slider.rs +++ b/style/src/slider.rs @@ -70,6 +70,8 @@ pub enum HandleShape { Rectangle { /// The width of the rectangle. width: u16, + /// The height of the rectangle. + height: u16, /// The border radius of the corners of the rectangle. border_radius: border::Radius, }, diff --git a/style/src/theme.rs b/style/src/theme.rs index 8f38fae819..de7c2c7b5a 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -22,6 +22,7 @@ use crate::text_editor; use crate::text_input; use crate::toggler; +use iced_core::border::Radius; use iced_core::{Background, Border, Color, Shadow, Vector}; use std::fmt; @@ -473,6 +474,7 @@ impl slider::StyleSheet for Theme { let handle = slider::Handle { shape: slider::HandleShape::Rectangle { + height: 8, width: 8, border_radius: 4.0.into(), }, @@ -734,6 +736,9 @@ impl toggler::StyleSheet for Theme { palette.background.base.color }, foreground_border: None, + border_radius: Radius::from(8.0), + handle_radius: Radius::from(8.0), + handle_margin: 2.0, } } Toggler::Custom(custom) => custom.active(self, is_active), diff --git a/style/src/toggler.rs b/style/src/toggler.rs index abc73f2a25..81c615fb2d 100644 --- a/style/src/toggler.rs +++ b/style/src/toggler.rs @@ -1,5 +1,5 @@ //! Change the appearance of a toggler. -use iced_core::Color; +use iced_core::{border::Radius, Color}; /// The appearance of a toggler. #[derive(Debug, Clone, Copy)] @@ -12,6 +12,12 @@ pub struct Appearance { pub foreground: Color, /// The [`Color`] of the foreground border of the toggler. pub foreground_border: Option, + /// The border radius of the toggler. + pub border_radius: Radius, + /// the radius of the handle of the toggler + pub handle_radius: Radius, + /// the space between the handle and the border of the toggler + pub handle_margin: f32, } /// A set of rules that dictate the style of a toggler. diff --git a/widget/src/slider.rs b/widget/src/slider.rs index ebdc4f7994..5951d25cf4 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -522,17 +522,24 @@ pub fn draw( (radius * 2.0, radius * 2.0, Radius::from(radius)) } HandleShape::Rectangle { + height, width, border_radius, } => { let width = (f32::from(width)) .max(2.0 * border_width) .min(bounds.width); - let height = bounds.height; + let height = (f32::from(height)) + .max(2.0 * border_width) + .min(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); + *r = (*r) + .min(height / 2.0) + .min(width / 2.0) + .max(*r * (width + border_width * 2.0) / width); } + (width, height, border_radius.into()) } }; diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index be0880d4dd..35a0e8537d 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -243,12 +243,7 @@ where layout::next_to_each_other( &limits, self.spacing, - |_| { - layout::Node::new(crate::core::Size::new( - 2.0 * self.size, - self.size, - )) - }, + |_| layout::Node::new(crate::core::Size::new(48., 24.)), |limits| { if let Some(label) = self.label.as_deref() { let state = tree @@ -329,13 +324,6 @@ where cursor: mouse::Cursor, viewport: &Rectangle, ) { - /// Makes sure that the border radius of the toggler looks good at every size. - const BORDER_RADIUS_RATIO: f32 = 32.0 / 13.0; - - /// The space ratio between the background Quad and the Toggler bounds, and - /// between the background Quad and foreground Quad. - const SPACE_RATIO: f32 = 0.05; - let mut children = layout.children(); let toggler_layout = children.next().unwrap(); @@ -361,23 +349,21 @@ where theme.active(&self.style, self.is_toggled) }; - let border_radius = bounds.height / BORDER_RADIUS_RATIO; - let space = SPACE_RATIO * bounds.height; + let space = style.handle_margin; let toggler_background_bounds = Rectangle { - x: bounds.x + space, - y: bounds.y + space, - width: bounds.width - (2.0 * space), - height: bounds.height - (2.0 * space), + x: bounds.x, + y: bounds.y, + width: bounds.width, + height: bounds.height, }; renderer.fill_quad( renderer::Quad { bounds: toggler_background_bounds, border: Border { - radius: border_radius.into(), - width: 1.0, - color: style.background_border.unwrap_or(style.background), + radius: style.border_radius, + ..Default::default() }, ..renderer::Quad::default() }, @@ -387,22 +373,21 @@ where let toggler_foreground_bounds = Rectangle { x: bounds.x + if self.is_toggled { - bounds.width - 2.0 * space - (bounds.height - (4.0 * space)) + bounds.width - space - (bounds.height - (2.0 * space)) } else { - 2.0 * space + space }, - y: bounds.y + (2.0 * space), - width: bounds.height - (4.0 * space), - height: bounds.height - (4.0 * space), + y: bounds.y + space, + width: bounds.height - (2.0 * space), + height: bounds.height - (2.0 * space), }; renderer.fill_quad( renderer::Quad { bounds: toggler_foreground_bounds, border: Border { - radius: border_radius.into(), - width: 1.0, - color: style.foreground_border.unwrap_or(style.foreground), + radius: style.handle_radius, + ..Default::default() }, ..renderer::Quad::default() }, diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index 7b5f44edae..88a45d91b5 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -366,9 +366,10 @@ pub fn draw( (radius * 2.0, radius * 2.0, radius.into()) } HandleShape::Rectangle { + height, width, border_radius, - } => (f32::from(width), bounds.width, border_radius), + } => (f32::from(width), f32::from(height), border_radius), }; let value = value.into() as f32; From feda333d1e3f0e181a5aee0a22cdd62bb2a2cca0 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Mon, 22 Apr 2024 14:39:36 -0700 Subject: [PATCH 127/178] Send `DataSource` events to all surfaces Previously these events are directed to the first surface, then removed from `sctk_events`. Which is definitely not right. --- sctk/src/application.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/sctk/src/application.rs b/sctk/src/application.rs index d21fdea77e..60c44bdac3 100644 --- a/sctk/src/application.rs +++ b/sctk/src/application.rs @@ -1002,7 +1002,10 @@ where while i < sctk_events.len() { let has_kbd_focus = kbd_surface_id.as_ref() == Some(object_id); - if event_is_for_surface( + if event_is_for_all_surfaces(&sctk_events[i]) { + filtered_sctk.push(sctk_events[i].clone()); + i += 1; + } else if event_is_for_surface( &sctk_events[i], object_id, state, @@ -2253,6 +2256,13 @@ where interfaces } +fn event_is_for_all_surfaces(evt: &SctkEvent) -> bool { + match evt { + SctkEvent::DataSource(_) => true, + _ => false, + } +} + // Determine if `SctkEvent` is for surface with given object id. fn event_is_for_surface<'a, A, C>( evt: &SctkEvent, From 900bad58c37c2ffce85ee73a8beabcd1ca63a3a7 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Wed, 24 Apr 2024 12:08:04 -0700 Subject: [PATCH 128/178] sctk: Use empty input region for subsurfaces This seems to work, and is a better way to deal with subsurface input if there aren't any problems. This way, input events simply go to the parent surface, so we don't have to deal with various edge cases related to that. (Though for compositor-side issues, we still need to fix those for other clients.) This helps with an issue with drag-and-drop and subsurfaces on Smithay, and a different issue on Kwin (in KDE 5.27, at least). --- sctk/src/subsurface_widget.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sctk/src/subsurface_widget.rs b/sctk/src/subsurface_widget.rs index cc10832aba..e39a19f74a 100644 --- a/sctk/src/subsurface_widget.rs +++ b/sctk/src/subsurface_widget.rs @@ -321,17 +321,25 @@ impl SubsurfaceState { let wl_surface = self .wl_compositor .create_surface(&self.qh, SurfaceData::new(None, 1)); + + // Use empty input region so parent surface gets pointer events + let region = self.wl_compositor.create_region(&self.qh, ()); + wl_surface.set_input_region(Some(®ion)); + region.destroy(); + 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, From 4c756fe29b3578b2606a6e451302932d3c6ef5f8 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 24 Apr 2024 15:58:28 -0400 Subject: [PATCH 129/178] fix(scrollable): filter scroll events in the wrong direction --- widget/src/scrollable.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index faa5032e5e..d66b316d8e 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -800,6 +800,13 @@ pub fn update( } mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y), }; + if matches!(direction, Direction::Vertical(_)) + && delta.y.abs() < 0.1 + || matches!(direction, Direction::Horizontal(_)) + && delta.x.abs() < 0.1 + { + return event::Status::Ignored; + } state.scroll(delta, direction, bounds, content_bounds); From 46b43746dbed3aea595d7c42892500c810ef606d Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 25 Apr 2024 15:25:48 -0400 Subject: [PATCH 130/178] fix(tiny_skia): damage --- tiny_skia/src/window/compositor.rs | 41 +++++++++++++++--------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 21ccf6200e..37b03cb054 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -183,31 +183,30 @@ pub fn present>( }) .unwrap_or_else(|| vec![Rectangle::with_size(viewport.logical_size())]); - if damage.is_empty() { - return Ok(()); - } - surface.primitive_stack.push_front(primitives.to_vec()); - surface.background_color = background_color; - let damage = damage::group(damage, scale_factor, physical_size); + if !damage.is_empty() { + surface.background_color = background_color; - let mut pixels = tiny_skia::PixmapMut::from_bytes( - bytemuck::cast_slice_mut(&mut buffer), - physical_size.width, - physical_size.height, - ) - .expect("Create pixel map"); + let damage = damage::group(damage, scale_factor, physical_size); - backend.draw( - &mut pixels, - &mut surface.clip_mask, - primitives, - viewport, - &damage, - background_color, - overlay, - ); + let mut pixels = tiny_skia::PixmapMut::from_bytes( + bytemuck::cast_slice_mut(&mut buffer), + physical_size.width, + physical_size.height, + ) + .expect("Create pixel map"); + + backend.draw( + &mut pixels, + &mut surface.clip_mask, + primitives, + viewport, + &damage, + background_color, + overlay, + ); + } buffer.present().map_err(|_| compositor::SurfaceError::Lost) } From 38e9fd6a402d2460c6ec555d54a150972d89f941 Mon Sep 17 00:00:00 2001 From: Dominic Gerhauser Date: Sat, 13 Apr 2024 12:58:11 +0200 Subject: [PATCH 131/178] feat(mouseare): mouse enter and exit --- widget/src/mouse_area.rs | 60 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index 1f040b740b..5b0e3d36b8 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -30,6 +30,8 @@ pub struct MouseArea< on_right_release: Option, on_middle_press: Option, on_middle_release: Option, + on_mouse_enter: Option, + on_mouse_exit: Option, } impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { @@ -81,13 +83,33 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { self.on_middle_release = Some(message); self } + #[must_use] + /// The message to emit on mouse enter. + pub fn on_mouse_enter(mut self, message: Message) -> Self { + self.on_mouse_enter = Some(message); + self + } + #[must_use] + /// The message to emit on mouse exit. + pub fn on_mouse_exit(mut self, message: Message) -> Self { + self.on_mouse_exit = Some(message); + self + } } /// Local state of the [`MouseArea`]. -#[derive(Default)] struct State { // TODO: Support on_mouse_enter and on_mouse_exit drag_initiated: Option, + is_out_of_bounds: bool, +} +impl Default for State { + fn default() -> Self { + Self { + drag_initiated: Default::default(), + is_out_of_bounds: true, + } + } } impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { @@ -104,6 +126,8 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { on_right_release: None, on_middle_press: None, on_middle_release: None, + on_mouse_enter: None, + on_mouse_exit: None, } } } @@ -231,7 +255,6 @@ where viewport, ); } - fn overlay<'b>( &'b mut self, tree: &'b mut Tree, @@ -244,7 +267,6 @@ where renderer, ) } - fn drag_destinations( &self, state: &Tree, @@ -286,6 +308,23 @@ fn update( state: &mut State, ) -> event::Status { if !cursor.is_over(layout.bounds()) { + if !state.is_out_of_bounds { + if widget + .on_mouse_enter + .as_ref() + .or(widget.on_mouse_exit.as_ref()) + .is_some() + { + if let Event::Mouse(mouse::Event::CursorMoved { .. }) = event { + state.is_out_of_bounds = true; + if let Some(message) = widget.on_mouse_exit.as_ref() { + shell.publish(message.clone()); + } + return event::Status::Captured; + } + } + } + return event::Status::Ignored; } @@ -353,6 +392,21 @@ fn update( return event::Status::Captured; } } + if let Some(message) = widget + .on_mouse_enter + .as_ref() + .or(widget.on_mouse_exit.as_ref()) + { + if let Event::Mouse(mouse::Event::CursorMoved { .. }) = event { + if state.is_out_of_bounds { + state.is_out_of_bounds = false; + if widget.on_mouse_enter.is_some() { + shell.publish(message.clone()); + } + 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)) From 364c90c00ea6d449ef9a9bf179bd6176248415a7 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 22 Apr 2024 23:37:28 -0400 Subject: [PATCH 132/178] fix(sctk): nested popup parent --- sctk/src/event_loop/state.rs | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/sctk/src/event_loop/state.rs b/sctk/src/event_loop/state.rs index 8ee524e5f3..62ae9ae471 100644 --- a/sctk/src/event_loop/state.rs +++ b/sctk/src/event_loop/state.rs @@ -514,11 +514,13 @@ where let (toplevel, popup) = match &parent { SctkSurface::LayerSurface(parent) => { - let parent_layer_surface = self + let Some(parent_layer_surface) = self .layer_surfaces .iter() .find(|w| w.surface.wl_surface() == parent) - .unwrap(); + else { + return Err(PopupCreationError::ParentMissing); + }; let popup = Popup::from_surface( None, &positioner, @@ -531,11 +533,13 @@ where (parent_layer_surface.surface.wl_surface(), popup) } SctkSurface::Window(parent) => { - let parent_window = self + let Some(parent_window) = self .windows .iter() .find(|w| w.window.wl_surface() == parent) - .unwrap(); + else { + return Err(PopupCreationError::ParentMissing); + }; ( parent_window.window.wl_surface(), Popup::from_surface( @@ -549,17 +553,12 @@ where ) } 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(); + let Some(parent_xdg) = self.popups.iter().find_map(|p| { + (p.popup.wl_surface() == parent) + .then(|| p.popup.xdg_surface()) + }) else { + return Err(PopupCreationError::ParentMissing); + }; ( &toplevel, From deae3d4c4ebf51004a5b948ea07f8771cfa20072 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Thu, 25 Apr 2024 15:02:04 -0700 Subject: [PATCH 133/178] sctk: Add support for drag-and-drop surface offsets This adds an offset `Vector` as an argument to `on_drag`, and allows passing an offset to `start_drag`. Some applications using drag and drop want the top left corner of the drag surface (as happens without an offset). But others want the drag surface to be offset based on where the cursor is on the widget when starting the drag. This can just be `-1 * offset`, but may be scaled if the drag surface is a different size from the original widget. --- .../platform_specific/wayland/data_device.rs | 4 ++-- runtime/src/dnd.rs | 2 +- sctk/src/commands/data_device.rs | 4 +++- sctk/src/event_loop/mod.rs | 5 ++++- widget/src/dnd_source.rs | 19 ++++++++++++++----- 5 files changed, 24 insertions(+), 10 deletions(-) diff --git a/runtime/src/command/platform_specific/wayland/data_device.rs b/runtime/src/command/platform_specific/wayland/data_device.rs index 5a85eefcaf..1ad5003b40 100644 --- a/runtime/src/command/platform_specific/wayland/data_device.rs +++ b/runtime/src/command/platform_specific/wayland/data_device.rs @@ -1,4 +1,4 @@ -use iced_core::window::Id; +use iced_core::{window::Id, Vector}; use iced_futures::MaybeSend; use sctk::reexports::client::protocol::wl_data_device_manager::DndAction; use std::{any::Any, fmt, marker::PhantomData}; @@ -52,7 +52,7 @@ pub enum ActionInner { /// 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, + icon_id: Option<(DndIcon, Vector)>, /// The data to send. data: Box, }, diff --git a/runtime/src/dnd.rs b/runtime/src/dnd.rs index 8acf9efb51..393bc29c4e 100644 --- a/runtime/src/dnd.rs +++ b/runtime/src/dnd.rs @@ -3,7 +3,7 @@ use std::any::Any; use dnd::{DndDestinationRectangle, DndSurface}; -use iced_core::clipboard::DndSource; +use iced_core::{clipboard::DndSource, Vector}; use iced_futures::MaybeSend; use window_clipboard::mime::{AllowedMimeTypes, AsMimeTypes}; diff --git a/sctk/src/commands/data_device.rs b/sctk/src/commands/data_device.rs index b009dca473..4bc0c92ba9 100644 --- a/sctk/src/commands/data_device.rs +++ b/sctk/src/commands/data_device.rs @@ -15,6 +15,8 @@ use iced_runtime::{ }; use sctk::reexports::client::protocol::wl_data_device_manager::DndAction; +use crate::core::Vector; + /// 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( @@ -38,7 +40,7 @@ pub fn start_drag( mime_types: Vec, actions: DndAction, origin_id: window::Id, - icon_id: Option, + icon_id: Option<(DndIcon, Vector)>, data: Box, ) -> Command { Command::single(command::Action::PlatformSpecific( diff --git a/sctk/src/event_loop/mod.rs b/sctk/src/event_loop/mod.rs index 9774b3db92..efc8c5de17 100644 --- a/sctk/src/event_loop/mod.rs +++ b/sctk/src/event_loop/mod.rs @@ -1163,12 +1163,15 @@ where 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_surface = if let Some((icon_id, offset)) = 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); + if offset != crate::core::Vector::ZERO { + wl_surface.offset(offset.x as i32, offset.y as i32); + } source.start_drag(device, &origin, Some(&wl_surface), serial); sticky_exit_callback( IcedSctkEvent::DndSurfaceCreated( diff --git a/widget/src/dnd_source.rs b/widget/src/dnd_source.rs index f25f97a8ec..23a949bcec 100644 --- a/widget/src/dnd_source.rs +++ b/widget/src/dnd_source.rs @@ -4,7 +4,7 @@ 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, + Point, Rectangle, Shell, Size, Vector, Widget, }; use crate::core::widget::{ @@ -16,7 +16,7 @@ use crate::core::widget::{ pub struct DndSource<'a, Message, Theme, Renderer> { content: Element<'a, Message, Theme, Renderer>, - on_drag: Option Message + 'a>>, + on_drag: Option Message + 'a>>, on_cancelled: Option, @@ -40,7 +40,7 @@ impl<'a, Message, Widget, Renderer> DndSource<'a, Message, Widget, Renderer> { #[must_use] pub fn on_drag(mut self, f: F) -> Self where - F: Fn(Size) -> Message + 'a, + F: Fn(Size, Vector) -> Message + 'a, { self.on_drag = Some(Box::new(f)); self @@ -343,7 +343,12 @@ where if let (Some(on_drag), Some(_)) = (self.on_drag.as_ref(), state.left_pressed_position.take()) { - shell.publish(on_drag(layout.bounds().size())); + let mut offset = cursor_position; + let offset = Vector::new( + cursor_position.x - layout.bounds().x, + cursor_position.y - layout.bounds().y, + ); + shell.publish(on_drag(layout.bounds().size(), offset)); state.is_dragging = true; return event::Status::Captured; }; @@ -362,7 +367,11 @@ where if distance > self.drag_threshold { state.left_pressed_position = None; state.is_dragging = true; - shell.publish(on_drag(layout.bounds().size())); + let offset = Vector::new( + cursor_position.x - layout.bounds().x, + cursor_position.y - layout.bounds().y, + ); + shell.publish(on_drag(layout.bounds().size(), offset)); return event::Status::Captured; } } From 08fc97c7209e4acc34b05fdc43c6fb64eb4fa4d2 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Tue, 30 Apr 2024 12:18:06 -0600 Subject: [PATCH 134/178] Adjust to line ending needing to be specified as part of cosmic_text::BufferLine --- graphics/src/geometry/text.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/graphics/src/geometry/text.rs b/graphics/src/geometry/text.rs index 6c02b909b7..6861009f91 100644 --- a/graphics/src/geometry/text.rs +++ b/graphics/src/geometry/text.rs @@ -43,6 +43,7 @@ impl Text { let mut buffer = cosmic_text::BufferLine::new( &self.content, + cosmic_text::LineEnding::default(), cosmic_text::AttrsList::new(text::to_attributes(self.font)), text::to_shaping(self.shaping), ); From 1869fe32ecf13caf36568c2ce8fc2acad33b33f0 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 1 May 2024 12:07:33 -0400 Subject: [PATCH 135/178] fix: clean up dnd surfaces when a window is removed --- Cargo.toml | 6 +++--- sctk/Cargo.toml | 2 +- winit/src/multi_window.rs | 26 ++++++++++++++++++++++++-- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 39261f6832..a6487210f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -192,9 +192,9 @@ web-time = "0.2" # Newer wgpu commit that fixes Vulkan backend on Nvidia wgpu = { git = "https://github.com/gfx-rs/wgpu", rev = "20fda69" } winapi = "0.3" -window_clipboard = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-4" } -dnd = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-4" } -mime = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-4" } +window_clipboard = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-5" } +dnd = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-5" } +mime = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-5" } # window_clipboard = { path = "../window_clipboard" } # dnd = { path = "../window_clipboard/dnd" } # mime = { path = "../window_clipboard/mime" } diff --git a/sctk/Cargo.toml b/sctk/Cargo.toml index 6c01eeeebc..31036a83c2 100644 --- a/sctk/Cargo.toml +++ b/sctk/Cargo.toml @@ -22,7 +22,7 @@ window_clipboard.workspace = true raw-window-handle = "0.6" enum-repr = "0.2" futures = "0.3" -wayland-backend = {version = "0.3.1", features = ["client_system"]} +wayland-backend = { version = "0.3.1", features = ["client_system"] } float-cmp = "0.9" xkbcommon-dl = "0.4.1" xkbcommon = { version = "0.7", features = ["wayland"] } diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 76cb2b8eef..0065fc14fd 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -731,10 +731,18 @@ async fn run_instance( winit::event::WindowEvent::CloseRequested ) && window.exit_on_close_request { - let _ = window_manager.remove(id); + let w = window_manager.remove(id); let _ = user_interfaces.remove(&id); let _ = ui_caches.remove(&id); - + // XXX Empty rectangle list un-registers the window + if let Some(w) = w { + clipboard.register_dnd_destination( + DndSurface(Arc::new(Box::new( + w.raw.clone(), + ))), + Vec::new(), + ); + } events.push(( None, core::Event::Window(id, window::Event::Closed), @@ -1207,6 +1215,14 @@ async fn run_instance( let w = window_manager.remove(id); let _ = user_interfaces.remove(&id); let _ = ui_caches.remove(&id); + if let Some(w) = w.as_ref() { + clipboard.register_dnd_destination( + DndSurface(Arc::new(Box::new( + w.raw.clone(), + ))), + Vec::new(), + ); + } events.push(( None, @@ -1405,6 +1421,12 @@ fn run_command( window::Action::Close(id) => { let w = window_manager.remove(id); let _ = ui_caches.remove(&id); + if let Some(w) = w.as_ref() { + clipboard.register_dnd_destination( + DndSurface(Arc::new(Box::new(w.raw.clone()))), + Vec::new(), + ); + } if window_manager.is_empty() && w.is_some_and(|w| w.exit_on_close_request) From 927a85251c6dc56cb827711f842a8b87590f2ad4 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 1 May 2024 15:01:49 -0400 Subject: [PATCH 136/178] fix: update widnow-clipboard tag --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a6487210f3..ba2a742626 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -192,9 +192,9 @@ web-time = "0.2" # Newer wgpu commit that fixes Vulkan backend on Nvidia wgpu = { git = "https://github.com/gfx-rs/wgpu", rev = "20fda69" } winapi = "0.3" -window_clipboard = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-5" } -dnd = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-5" } -mime = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-5" } +window_clipboard = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-6" } +dnd = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-6" } +mime = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-6" } # window_clipboard = { path = "../window_clipboard" } # dnd = { path = "../window_clipboard/dnd" } # mime = { path = "../window_clipboard/mime" } From 317458efe420034c7d22faf2351867fdd3d9ac3f Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Fri, 26 Apr 2024 14:22:18 -0700 Subject: [PATCH 137/178] sctk: Add touch support --- sctk/src/application.rs | 34 +++++++- sctk/src/event_loop/mod.rs | 1 + sctk/src/event_loop/state.rs | 5 +- sctk/src/handlers/seat/seat.rs | 26 ++++-- sctk/src/handlers/seat/touch.rs | 142 +++++++++++++++++++++++++++++++- sctk/src/sctk_event.rs | 18 +++- 6 files changed, 216 insertions(+), 10 deletions(-) diff --git a/sctk/src/application.rs b/sctk/src/application.rs index 60c44bdac3..3265b43a04 100644 --- a/sctk/src/application.rs +++ b/sctk/src/application.rs @@ -49,7 +49,7 @@ use iced_runtime::{ wayland::{data_device::DndIcon, popup}, }, }, - core::{mouse::Interaction, Color, Point, Size}, + core::{mouse::Interaction, touch, Color, Point, Size}, multi_window::Program, system, user_interface, window::Id as SurfaceId, @@ -488,6 +488,30 @@ where } } }, + SctkEvent::TouchEvent { variant, surface, .. } => { + let mut offset = (0., 0.); + let (state, _native_id) = match surface_ids + .get(&surface.id()) + .and_then(|id| states.get_mut(&id.inner()).map(|state| (state, id))) + { + Some(s) => s, + None => { + if let Some((x_offset, y_offset, id)) = subsurface_ids.get(&surface.id()) { + offset = (f64::from(*x_offset), f64::from(*y_offset)); + states.get_mut(&id.inner()).map(|state| (state, id)).unwrap() + } else { + continue + } + }, + }; + let position = match variant { + touch::Event::FingerPressed { position, .. } => position, + touch::Event::FingerLifted { position, .. } => position, + touch::Event::FingerMoved { position, .. } => position, + touch::Event::FingerLost { position, .. } => position, + }; + state.set_cursor_position(Some(LogicalPosition { x: position.x as f64 + offset.0, y: position.y as f64 + offset.1 })); + }, SctkEvent::WindowEvent { variant, id: wl_surface } => match variant { crate::sctk_event::WindowEventVariant::Created(id, native_id) => { surface_ids.insert(id, SurfaceIdWrapper::Window(native_id)); @@ -2289,6 +2313,14 @@ where KeyboardEventVariant::Leave(id) => &id.id() == object_id, _ => has_kbd_focus, }, + SctkEvent::TouchEvent { surface, .. } => { + let event_object_id = surface.id(); + &event_object_id == object_id + || state + .subsurfaces + .iter() + .any(|s| s.wl_surface.id() == event_object_id) + } SctkEvent::WindowEvent { id, .. } => &id.id() == object_id, SctkEvent::LayerSurfaceEvent { id, .. } => &id.id() == object_id, SctkEvent::PopupEvent { id, .. } => &id.id() == object_id, diff --git a/sctk/src/event_loop/mod.rs b/sctk/src/event_loop/mod.rs index efc8c5de17..23555b28ea 100644 --- a/sctk/src/event_loop/mod.rs +++ b/sctk/src/event_loop/mod.rs @@ -203,6 +203,7 @@ where lock_surfaces: Vec::new(), dnd_source: None, _kbd_focus: None, + touch_points: HashMap::new(), sctk_events: Vec::new(), frame_events: Vec::new(), pending_user_events: Vec::new(), diff --git a/sctk/src/event_loop/state.rs b/sctk/src/event_loop/state.rs index 62ae9ae471..59e84a8f84 100644 --- a/sctk/src/event_loop/state.rs +++ b/sctk/src/event_loop/state.rs @@ -1,4 +1,5 @@ use std::{ + collections::HashMap, fmt::{Debug, Formatter}, num::NonZeroU32, }; @@ -26,6 +27,7 @@ use iced_runtime::{ window::SctkWindowSettings, }, }, + core::{touch, Point}, keyboard::Modifiers, window, }; @@ -92,7 +94,7 @@ pub(crate) struct SctkSeat { 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) touch: Option, pub(crate) _modifiers: Modifiers, pub(crate) data_device: DataDevice, pub(crate) icon: Option, @@ -286,6 +288,7 @@ pub struct SctkState { pub(crate) lock_surfaces: Vec, pub(crate) dnd_source: Option>, pub(crate) _kbd_focus: Option, + pub(crate) touch_points: HashMap, /// 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, diff --git a/sctk/src/handlers/seat/seat.rs b/sctk/src/handlers/seat/seat.rs index cf28572a7d..97cca9a8a8 100644 --- a/sctk/src/handlers/seat/seat.rs +++ b/sctk/src/handlers/seat/seat.rs @@ -34,7 +34,7 @@ where seat, kbd: None, ptr: None, - _touch: None, + touch: None, data_device, _modifiers: Modifiers::default(), kbd_focus: None, @@ -59,7 +59,7 @@ where seat: seat.clone(), kbd: None, ptr: None, - _touch: None, + touch: None, data_device: self .data_device_manager_state .get_data_device(qh, &seat), @@ -121,7 +121,16 @@ where } } sctk::seat::Capability::Touch => { - // TODO touch + if let Some(touch) = self.seat_state.get_touch(qh, &seat).ok() { + self.sctk_events.push(SctkEvent::SeatEvent { + variant: SeatEventVariant::NewCapability( + capability, + touch.id(), + ), + id: seat.clone(), + }); + my_seat.touch.replace(touch); + } } _ => unimplemented!(), } @@ -165,8 +174,15 @@ where } } sctk::seat::Capability::Touch => { - // TODO touch - // my_seat.touch = self.seat_state.get_touch(qh, &seat).ok(); + if let Some(touch) = my_seat.touch.take() { + self.sctk_events.push(SctkEvent::SeatEvent { + variant: SeatEventVariant::RemoveCapability( + capability, + touch.id(), + ), + id: seat.clone(), + }); + } } _ => unimplemented!(), } diff --git a/sctk/src/handlers/seat/touch.rs b/sctk/src/handlers/seat/touch.rs index 70b786d12e..a7e797e365 100644 --- a/sctk/src/handlers/seat/touch.rs +++ b/sctk/src/handlers/seat/touch.rs @@ -1 +1,141 @@ -// TODO +// TODO handle multiple seats? + +use crate::{event_loop::state::SctkState, sctk_event::SctkEvent}; +use iced_runtime::core::{touch, Point}; +use sctk::{ + delegate_touch, + reexports::client::{ + protocol::{wl_surface::WlSurface, wl_touch::WlTouch}, + Connection, QueueHandle, + }, + seat::touch::TouchHandler, +}; +use std::fmt::Debug; + +impl TouchHandler for SctkState { + fn down( + &mut self, + _: &Connection, + _: &QueueHandle, + touch: &WlTouch, + _serial: u32, + _time: u32, + surface: WlSurface, + id: i32, + position: (f64, f64), + ) { + let Some(my_seat) = + self.seats.iter().find(|s| s.touch.as_ref() == Some(touch)) + else { + return; + }; + + let id = touch::Finger(id as u64); + let position = Point::new(position.0 as f32, position.1 as f32); + self.touch_points.insert(id, (surface.clone(), position)); + self.sctk_events.push(SctkEvent::TouchEvent { + variant: touch::Event::FingerPressed { id, position }, + touch_id: touch.clone(), + seat_id: my_seat.seat.clone(), + surface, + }); + } + + fn up( + &mut self, + _: &Connection, + _: &QueueHandle, + touch: &WlTouch, + _serial: u32, + _time: u32, + id: i32, + ) { + let Some(my_seat) = + self.seats.iter().find(|s| s.touch.as_ref() == Some(touch)) + else { + return; + }; + + let id = touch::Finger(id as u64); + if let Some((surface, position)) = self.touch_points.get(&id).cloned() { + self.sctk_events.push(SctkEvent::TouchEvent { + variant: touch::Event::FingerLifted { id, position }, + touch_id: touch.clone(), + seat_id: my_seat.seat.clone(), + surface, + }); + } + } + fn motion( + &mut self, + _: &Connection, + _: &QueueHandle, + touch: &WlTouch, + _time: u32, + id: i32, + position: (f64, f64), + ) { + let Some(my_seat) = + self.seats.iter().find(|s| s.touch.as_ref() == Some(touch)) + else { + return; + }; + + let id = touch::Finger(id as u64); + let position = Point::new(position.0 as f32, position.1 as f32); + if let Some((surface, position_ref)) = self.touch_points.get_mut(&id) { + *position_ref = position; + self.sctk_events.push(SctkEvent::TouchEvent { + variant: touch::Event::FingerMoved { id, position }, + touch_id: touch.clone(), + seat_id: my_seat.seat.clone(), + surface: surface.clone(), + }); + } + } + + fn shape( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlTouch, + _: i32, + _: f64, + _: f64, + ) { + } + + fn orientation( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlTouch, + _: i32, + _: f64, + ) { + } + + fn cancel( + &mut self, + _: &Connection, + _: &QueueHandle, + touch: &WlTouch, + ) { + let Some(my_seat) = + self.seats.iter().find(|s| s.touch.as_ref() == Some(touch)) + else { + return; + }; + + for (id, (surface, position)) in self.touch_points.drain() { + self.sctk_events.push(SctkEvent::TouchEvent { + variant: touch::Event::FingerLost { id, position }, + touch_id: touch.clone(), + seat_id: my_seat.seat.clone(), + surface, + }); + } + } +} + +delegate_touch!(@ SctkState); diff --git a/sctk/src/sctk_event.rs b/sctk/src/sctk_event.rs index 95bdf2ba9c..629763829f 100755 --- a/sctk/src/sctk_event.rs +++ b/sctk/src/sctk_event.rs @@ -14,7 +14,7 @@ use iced_futures::core::event::{ }; use iced_runtime::{ command::platform_specific::wayland::data_device::DndIcon, - core::{event::wayland, keyboard, mouse, window, Point}, + core::{event::wayland, keyboard, mouse, touch, window, Point}, keyboard::{key, Key, Location}, window::Id as SurfaceId, }; @@ -25,7 +25,7 @@ use sctk::{ protocol::{ wl_data_device_manager::DndAction, wl_keyboard::WlKeyboard, wl_output::WlOutput, wl_pointer::WlPointer, wl_seat::WlSeat, - wl_surface::WlSurface, + wl_surface::WlSurface, wl_touch::WlTouch, }, Proxy, }, @@ -146,6 +146,12 @@ pub enum SctkEvent { kbd_id: WlKeyboard, seat_id: WlSeat, }, + TouchEvent { + variant: touch::Event, + touch_id: WlTouch, + seat_id: WlSeat, + surface: WlSurface, + }, // TODO data device & touch // @@ -627,6 +633,14 @@ impl SctkEvent { )] } }, + SctkEvent::TouchEvent { + variant, + touch_id: _, + seat_id: _, + surface: _, + } => { + vec![iced_runtime::core::Event::Touch(variant)] + } SctkEvent::WindowEvent { variant, id: surface, From d756c9defccb7e2190fc7c3686346a3fd18c2092 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Fri, 26 Apr 2024 14:58:29 -0700 Subject: [PATCH 138/178] sctk: Support `start_drag` with drags started from touch events --- sctk/src/event_loop/mod.rs | 6 +++--- sctk/src/event_loop/state.rs | 1 + sctk/src/handlers/seat/seat.rs | 2 ++ sctk/src/handlers/seat/touch.rs | 12 ++++++++---- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/sctk/src/event_loop/mod.rs b/sctk/src/event_loop/mod.rs index 23555b28ea..bb6b2f1afa 100644 --- a/sctk/src/event_loop/mod.rs +++ b/sctk/src/event_loop/mod.rs @@ -1142,9 +1142,9 @@ where Some(s) => s, None => continue, }; - let serial = match seat.last_ptr_press { - Some(s) => s.2, - None => continue, + // Get last pointer press or touch down serial, whichever is newer + let Some(serial) = seat.last_ptr_press.map(|s| s.2).max(seat.last_touch_down.map(|s| s.2)) else { + continue; }; let origin = match self diff --git a/sctk/src/event_loop/state.rs b/sctk/src/event_loop/state.rs index 59e84a8f84..7737fd59f3 100644 --- a/sctk/src/event_loop/state.rs +++ b/sctk/src/event_loop/state.rs @@ -95,6 +95,7 @@ pub(crate) struct SctkSeat { pub(crate) ptr_focus: Option, pub(crate) last_ptr_press: Option<(u32, u32, u32)>, // (time, button, serial) pub(crate) touch: Option, + pub(crate) last_touch_down: Option<(u32, i32, u32)>, // (time, point, serial) pub(crate) _modifiers: Modifiers, pub(crate) data_device: DataDevice, pub(crate) icon: Option, diff --git a/sctk/src/handlers/seat/seat.rs b/sctk/src/handlers/seat/seat.rs index 97cca9a8a8..adc5e0e842 100644 --- a/sctk/src/handlers/seat/seat.rs +++ b/sctk/src/handlers/seat/seat.rs @@ -41,6 +41,7 @@ where ptr_focus: None, last_ptr_press: None, last_kbd_press: None, + last_touch_down: None, icon: None, }); } @@ -68,6 +69,7 @@ where ptr_focus: None, last_ptr_press: None, last_kbd_press: None, + last_touch_down: None, icon: None, }); self.seats.last_mut().unwrap() diff --git a/sctk/src/handlers/seat/touch.rs b/sctk/src/handlers/seat/touch.rs index a7e797e365..3c6920898b 100644 --- a/sctk/src/handlers/seat/touch.rs +++ b/sctk/src/handlers/seat/touch.rs @@ -18,18 +18,22 @@ impl TouchHandler for SctkState { _: &Connection, _: &QueueHandle, touch: &WlTouch, - _serial: u32, - _time: u32, + serial: u32, + time: u32, surface: WlSurface, id: i32, position: (f64, f64), ) { - let Some(my_seat) = - self.seats.iter().find(|s| s.touch.as_ref() == Some(touch)) + let Some(my_seat) = self + .seats + .iter_mut() + .find(|s| s.touch.as_ref() == Some(touch)) else { return; }; + my_seat.last_touch_down.replace((time, id, serial)); + let id = touch::Finger(id as u64); let position = Point::new(position.0 as f32, position.1 as f32); self.touch_points.insert(id, (surface.clone(), position)); From 81a483b96f560f25e0de4c34c0e9edbacefa19f2 Mon Sep 17 00:00:00 2001 From: Victoria Brekenfeld Date: Tue, 14 May 2024 18:12:14 +0200 Subject: [PATCH 139/178] iced_wgpu: Query wayland for the device to use, if possible --- wgpu/Cargo.toml | 9 +++ wgpu/src/window.rs | 2 + wgpu/src/window/compositor.rs | 79 +++++++++++++++----- wgpu/src/window/wayland.rs | 135 ++++++++++++++++++++++++++++++++++ 4 files changed, 208 insertions(+), 17 deletions(-) create mode 100644 wgpu/src/window/wayland.rs diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 1d3b57a710..14694cdc12 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -42,3 +42,12 @@ resvg.optional = true tracing.workspace = true tracing.optional = true + +[target.'cfg(unix)'.dependencies] +rustix = { version = "0.38" } +raw-window-handle.workspace = true +sctk.workspace = true +wayland-protocols.workspace = true +wayland-backend = { version = "0.3.3", features = ["client_system"] } +wayland-client = { version = "0.31.2" } +wayland-sys = { version = "0.31.1", features = ["dlopen"] } diff --git a/wgpu/src/window.rs b/wgpu/src/window.rs index 9545a14e5b..d51038667a 100644 --- a/wgpu/src/window.rs +++ b/wgpu/src/window.rs @@ -1,5 +1,7 @@ //! Display rendering results on windows. pub mod compositor; +#[cfg(unix)] +mod wayland; pub use compositor::Compositor; pub use wgpu::Surface; diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index efe5e4b724..dc641262a4 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -6,6 +6,9 @@ use crate::graphics::compositor; use crate::graphics::{Error, Viewport}; use crate::{Backend, Primitive, Renderer, Settings}; +#[cfg(unix)] +use super::wayland::get_wayland_device_ids; + /// A window graphics backend for iced powered by `wgpu`. #[allow(missing_debug_implementations)] pub struct Compositor { @@ -32,32 +35,74 @@ impl Compositor { log::info!("{settings:#?}"); + let available_adapters = + instance.enumerate_adapters(settings.internal_backend); + #[cfg(not(target_arch = "wasm32"))] if log::max_level() >= log::LevelFilter::Info { - let available_adapters: Vec<_> = instance - .enumerate_adapters(settings.internal_backend) - .iter() - .map(wgpu::Adapter::get_info) - .collect(); - log::info!("Available adapters: {available_adapters:#?}"); + log::info!( + "Available adapters: {:#?}", + available_adapters.iter().map(wgpu::Adapter::get_info) + ); } + #[cfg(unix)] + let ids = compatible_window.as_ref().and_then(get_wayland_device_ids); + #[allow(unsafe_code)] let compatible_surface = compatible_window .and_then(|window| instance.create_surface(window).ok()); - let adapter = instance - .request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::util::power_preference_from_env() - .unwrap_or(if settings.antialiasing.is_none() { - wgpu::PowerPreference::LowPower + let mut adapter = None; + #[cfg_attr(not(unix), allow(dead_code))] + if std::env::var_os("WGPU_ADAPTER_NAME").is_none() { + #[cfg(unix)] + if let Some((vendor_id, device_id)) = ids { + adapter = available_adapters + .into_iter() + .filter(|adapter| { + let info = adapter.get_info(); + info.device == device_id as u32 + && info.vendor == vendor_id as u32 + }) + .find(|adapter| { + if let Some(surface) = compatible_surface.as_ref() { + adapter.is_surface_supported(surface) + } else { + true + } + }); + } + } else if let Ok(name) = std::env::var("WGPU_ADAPTER_NAME") { + adapter = available_adapters + .into_iter() + .filter(|adapter| { + let info = adapter.get_info(); + info.name == name + }) + .find(|adapter| { + if let Some(surface) = compatible_surface.as_ref() { + adapter.is_surface_supported(surface) } else { - wgpu::PowerPreference::HighPerformance - }), - compatible_surface: compatible_surface.as_ref(), - force_fallback_adapter: false, - }) - .await?; + true + } + }); + } + + let adapter = adapter.or({ + instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::util::power_preference_from_env() + .unwrap_or(if settings.antialiasing.is_none() { + wgpu::PowerPreference::LowPower + } else { + wgpu::PowerPreference::HighPerformance + }), + compatible_surface: compatible_surface.as_ref(), + force_fallback_adapter: false, + }) + .await + })?; log::info!("Selected: {:#?}", adapter.get_info()); diff --git a/wgpu/src/window/wayland.rs b/wgpu/src/window/wayland.rs new file mode 100644 index 0000000000..a07d2180b6 --- /dev/null +++ b/wgpu/src/window/wayland.rs @@ -0,0 +1,135 @@ +use crate::graphics::compositor::Window; +use raw_window_handle::{RawDisplayHandle, WaylandDisplayHandle}; +use rustix::fs::{major, minor}; +use sctk::{ + dmabuf::{DmabufFeedback, DmabufHandler, DmabufState}, + registry::{ProvidesRegistryState, RegistryState}, + registry_handlers, +}; +use std::{fs::File, io::Read, path::PathBuf}; +use wayland_client::{ + backend::Backend, globals::registry_queue_init, protocol::wl_buffer, + Connection, QueueHandle, +}; +use wayland_protocols::wp::linux_dmabuf::zv1::client::{ + zwp_linux_buffer_params_v1, zwp_linux_dmabuf_feedback_v1, +}; + +struct AppData { + registry_state: RegistryState, + dmabuf_state: DmabufState, + feedback: Option, +} + +impl DmabufHandler for AppData { + fn dmabuf_state(&mut self) -> &mut DmabufState { + &mut self.dmabuf_state + } + + fn dmabuf_feedback( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _proxy: &zwp_linux_dmabuf_feedback_v1::ZwpLinuxDmabufFeedbackV1, + feedback: DmabufFeedback, + ) { + self.feedback = Some(feedback); + } + + fn created( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _params: &zwp_linux_buffer_params_v1::ZwpLinuxBufferParamsV1, + _buffer: wl_buffer::WlBuffer, + ) { + } + + fn failed( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _params: &zwp_linux_buffer_params_v1::ZwpLinuxBufferParamsV1, + ) { + } + + fn released( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _buffer: &wl_buffer::WlBuffer, + ) { + } +} + +impl ProvidesRegistryState for AppData { + fn registry(&mut self) -> &mut RegistryState { + &mut self.registry_state + } + registry_handlers![,]; +} + +pub fn get_wayland_device_ids(window: &W) -> Option<(u16, u16)> { + let conn = match window.display_handle().map(|handle| handle.as_raw()) { + #[allow(unsafe_code)] + Ok(RawDisplayHandle::Wayland(WaylandDisplayHandle { + display, .. + })) => Connection::from_backend(unsafe { + Backend::from_foreign_display(display.as_ptr() as *mut _) + }), + _ => { + return None; + } + }; + + let (globals, mut event_queue) = registry_queue_init(&conn).unwrap(); + let qh = event_queue.handle(); + + let mut app_data = AppData { + registry_state: RegistryState::new(&globals), + dmabuf_state: DmabufState::new(&globals, &qh), + feedback: None, + }; + + match app_data.dmabuf_state.version() { + Some(4..) => { + let _ = app_data.dmabuf_state.get_default_feedback(&qh).unwrap(); + + let feedback = loop { + let _ = event_queue.blocking_dispatch(&mut app_data).ok()?; + if let Some(feedback) = app_data.feedback.as_ref() { + break feedback; + } + }; + + let dev = feedback.main_device(); + let path = PathBuf::from(format!( + "/sys/dev/char/{}:{}/device/drm", + major(dev), + minor(dev) + )); + let vendor = { + let path = path.join("vendor"); + let mut file = File::open(&path).ok()?; + let mut contents = String::new(); + let _ = file.read_to_string(&mut contents).ok()?; + u16::from_str_radix(contents.trim_start_matches("0x"), 16) + .ok()? + }; + let device = { + let path = path.join("device"); + let mut file = File::open(&path).ok()?; + let mut contents = String::new(); + let _ = file.read_to_string(&mut contents).ok()?; + u16::from_str_radix(contents.trim_start_matches("0x"), 16) + .ok()? + }; + + Some((vendor, device)) + } + _ => None, + } +} + +sctk::delegate_dmabuf!(AppData); +sctk::delegate_registry!(AppData); From 7c58b8eda9885083e019a5ea14edf4652740e154 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 15 May 2024 13:11:52 -0400 Subject: [PATCH 140/178] fix(image): guess the image format before decoding --- graphics/src/image.rs | 4 +++- src/window/icon.rs | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/graphics/src/image.rs b/graphics/src/image.rs index d89caacecb..2a4ed2bb3b 100644 --- a/graphics/src/image.rs +++ b/graphics/src/image.rs @@ -9,7 +9,9 @@ pub use ::image as image_rs; pub fn load(handle: &Handle) -> image_rs::ImageResult { match handle.data() { Data::Path(path) => { - let image = ::image::open(path)?; + let image = ::image::io::Reader::open(&path)? + .with_guessed_format()? + .decode()?; let operation = std::fs::File::open(path) .ok() diff --git a/src/window/icon.rs b/src/window/icon.rs index ef71c22834..6c0d1ca154 100644 --- a/src/window/icon.rs +++ b/src/window/icon.rs @@ -13,7 +13,10 @@ use std::path::Path; /// This will return an error in case the file is missing at run-time. You may prefer [`from_file_data`] instead. #[cfg(feature = "image")] pub fn from_file>(icon_path: P) -> Result { - let icon = image::io::Reader::open(icon_path)?.decode()?.to_rgba8(); + let icon = ::image::io::Reader::open(icon_path)? + .with_guessed_format()? + .decode()? + .to_rgba8(); Ok(icon::from_rgba(icon.to_vec(), icon.width(), icon.height())?) } From 84d788c96d8463ce9810fa203d5c1b0e83b4a3ce Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Thu, 30 May 2024 18:37:20 +0200 Subject: [PATCH 141/178] fix: emit Event::Resized to fix nav bar in cosmic-settings --- sctk/src/sctk_event.rs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/sctk/src/sctk_event.rs b/sctk/src/sctk_event.rs index 629763829f..c7a1f211c6 100755 --- a/sctk/src/sctk_event.rs +++ b/sctk/src/sctk_event.rs @@ -675,23 +675,17 @@ impl SctkEvent { Default::default() } WindowEventVariant::Configure(configure, surface, _) => { - if configure.is_resizing() { + if let (Some(new_width), Some(new_height)) = + configure.new_size + { 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(), + width: new_width.get(), + height: new_height.get(), }, ) }) From 09116a85e27890df6dbad1906444a0b3e4e8ba14 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 29 May 2024 16:24:53 -0400 Subject: [PATCH 142/178] fix: better handling of state tree This persists widget state associated with widgets assigned custom IDs even when the tree structure changes, but resets state if the custom ID is not found. --- core/src/lib.rs | 2 +- core/src/widget/tree.rs | 161 +++++++++++++++++++++++++++++----- runtime/src/user_interface.rs | 10 +++ 3 files changed, 150 insertions(+), 23 deletions(-) diff --git a/core/src/lib.rs b/core/src/lib.rs index bab7a3c259..25d962902f 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -9,7 +9,7 @@ #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] -#![forbid(unsafe_code)] +// #![forbid(unsafe_code)] #![deny( missing_debug_implementations, missing_docs, diff --git a/core/src/widget/tree.rs b/core/src/widget/tree.rs index d9e53acbc2..d1eb23fe38 100644 --- a/core/src/widget/tree.rs +++ b/core/src/widget/tree.rs @@ -2,10 +2,16 @@ use crate::id::{Id, Internal}; use crate::Widget; use std::any::{self, Any}; -use std::borrow::{Borrow, BorrowMut}; +use std::borrow::{Borrow, BorrowMut, Cow}; use std::collections::HashMap; -use std::fmt; use std::hash::Hash; +use std::iter::zip; +use std::{fmt, mem}; + +thread_local! { + /// A map of named widget states. +pub static NAMED: std::cell::RefCell, (State, Vec<(usize, Tree)>)>> = std::cell::RefCell::new(HashMap::new()); +} /// A persistent state widget tree. /// @@ -53,6 +59,81 @@ impl Tree { } } + /// Takes all named widgets from the tree. + pub fn take_all_named( + &mut self, + ) -> HashMap, (State, Vec<(usize, Tree)>)> { + let mut named = HashMap::new(); + struct Visit { + parent: Cow<'static, str>, + index: usize, + visited: bool, + } + // tree traversal to find all named widgets + // and keep their state and children + let mut stack = vec![(self, None)]; + while let Some((tree, visit)) = stack.pop() { + if let Some(Id(Internal::Custom(_, n))) = tree.id.clone() { + let state = mem::replace(&mut tree.state, State::None); + let children_count = tree.children.len(); + let children = + tree.children.iter_mut().rev().enumerate().map(|(i, c)| { + if matches!(c.id, Some(Id(Internal::Custom(_, _)))) { + (c, None) + } else { + ( + c, + Some(Visit { + index: i, + parent: n.clone(), + visited: false, + }), + ) + } + }); + _ = named.insert( + n.clone(), + (state, Vec::with_capacity(children_count)), + ); + stack.extend(children); + } else if let Some(visit) = visit { + if visit.visited { + named.get_mut(&visit.parent).unwrap().1.push(( + visit.index, + mem::replace( + tree, + Tree { + id: tree.id.clone(), + tag: tree.tag, + ..Tree::empty() + }, + ), + )); + } else { + let ptr = tree as *mut Tree; + + stack.push(( + // TODO remove this unsafe block + #[allow(unsafe_code)] + // SAFETY: when the reference is finally accessed, all the children references will have been processed first. + unsafe { + ptr.as_mut().unwrap() + }, + Some(Visit { + visited: true, + ..visit + }), + )); + stack.extend(tree.children.iter_mut().map(|c| (c, None))); + } + } else { + stack.extend(tree.children.iter_mut().map(|s| (s, None))); + } + } + + named + } + /// Finds a widget state in the tree by its id. pub fn find<'a>(&'a self, id: &Id) -> Option<&'a Tree> { if self.id == Some(id.clone()) { @@ -84,24 +165,50 @@ impl Tree { { 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; - } + let mut needs_reset = false; + let tag_match = self.tag == borrowed.tag(); + if let Some(Id(Internal::Custom(_, n))) = borrowed.id() { + if let Some((mut state, children)) = + NAMED.with_borrow_mut(|named| named.remove(&n)) + { + std::mem::swap(&mut self.state, &mut state); + let mut widget_children = borrowed.children(); + if !tag_match || self.children.len() != widget_children.len() { + self.children = borrowed.children(); } else { - borrowed.set_id(id); + for (old_i, mut old) in children { + let Some(new) = widget_children.get_mut(old_i) else { + continue; + }; + let Some(my_state) = self.children.get_mut(old_i) + else { + continue; + }; + debug_assert!(old.tag == my_state.tag); + debug_assert!(old.id == new.id); + + mem::swap(my_state, &mut old); + } } + } else { + needs_reset = true; + } + } else if tag_match { + if let Some(id) = self.id.clone() { + borrowed.set_id(id); + } + if self.children.len() != borrowed.children().len() { + self.children = borrowed.children(); } - borrowed.diff(self) } else { - *self = Self::new(new); + needs_reset = true; + } + if needs_reset { + *self = Self::new(borrowed); + let borrowed = new.borrow_mut(); + borrowed.diff(self); + } else { + borrowed.diff(self); } } @@ -164,7 +271,11 @@ impl Tree { ); let mut child_state_i = 0; - for (new, new_id) in new_children.iter_mut().zip(new_ids.iter()) { + let mut new_trees: Vec<(Tree, usize)> = + Vec::with_capacity(new_children.len()); + for (i, (new, new_id)) in + new_children.iter_mut().zip(new_ids.iter()).enumerate() + { 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()) @@ -173,7 +284,12 @@ impl Tree { } }) { c - } else if child_state_i < id_list.len() { + } else if child_state_i < id_list.len() + && !matches!( + id_list[child_state_i].id, + Some(Id(Internal::Custom(_, _))) + ) + { let c = &mut id_list[child_state_i]; if len_changed { c.id.clone_from(new_id); @@ -181,16 +297,17 @@ impl Tree { child_state_i += 1; c } else { + let mut my_new_state = new_state(new); + diff(&mut my_new_state, new); + new_trees.push((my_new_state, i)); continue; }; diff(child_state, new); } - if self.children.len() < new_children.len() { - self.children.extend( - new_children[self.children.len()..].iter().map(new_state), - ); + for (new_tree, i) in new_trees { + self.children.insert(i, new_tree); } } } diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index 50d84fe02c..3dc67465c3 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -1,5 +1,7 @@ //! Implement your own event loop to drive a user interface. + use iced_core::clipboard::DndDestinationRectangles; +use iced_core::widget::tree::NAMED; use iced_core::widget::{Operation, OperationOutputWrapper}; use crate::core::event::{self, Event}; @@ -98,6 +100,10 @@ where let mut root = root.into(); let Cache { mut state } = cache; + NAMED.with_borrow_mut(|named| { + *named = state.take_all_named(); + }); + state.diff(root.as_widget_mut()); let base = root.as_widget().layout( @@ -106,6 +112,10 @@ where &layout::Limits::new(Size::ZERO, bounds), ); + NAMED.with_borrow_mut(|named| { + named.clear(); + }); + UserInterface { root, base, From 2dcae59258c9cee6862c7fb2193066a1cb1e30c2 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 30 May 2024 17:04:59 -0400 Subject: [PATCH 143/178] fix: avoid with_borrow_mut --- core/src/widget/tree.rs | 2 +- runtime/src/user_interface.rs | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/core/src/widget/tree.rs b/core/src/widget/tree.rs index d1eb23fe38..cf2019292c 100644 --- a/core/src/widget/tree.rs +++ b/core/src/widget/tree.rs @@ -169,7 +169,7 @@ impl Tree { let tag_match = self.tag == borrowed.tag(); if let Some(Id(Internal::Custom(_, n))) = borrowed.id() { if let Some((mut state, children)) = - NAMED.with_borrow_mut(|named| named.remove(&n)) + NAMED.with(|named| named.borrow_mut().remove(&n)) { std::mem::swap(&mut self.state, &mut state); let mut widget_children = borrowed.children(); diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index 3dc67465c3..830318dbc8 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -100,8 +100,9 @@ where let mut root = root.into(); let Cache { mut state } = cache; - NAMED.with_borrow_mut(|named| { - *named = state.take_all_named(); + NAMED.with(|named| { + let mut guard = named.borrow_mut(); + *guard = state.take_all_named(); }); state.diff(root.as_widget_mut()); @@ -112,8 +113,8 @@ where &layout::Limits::new(Size::ZERO, bounds), ); - NAMED.with_borrow_mut(|named| { - named.clear(); + NAMED.with(|named| { + named.borrow_mut().clear(); }); UserInterface { From dc1fea81148911c2daf4e73bbc3cc8e16d30cb47 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 30 May 2024 21:53:01 -0400 Subject: [PATCH 144/178] fix(core): replace debug_assert in diff --- core/src/widget/tree.rs | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/core/src/widget/tree.rs b/core/src/widget/tree.rs index cf2019292c..db1744eb50 100644 --- a/core/src/widget/tree.rs +++ b/core/src/widget/tree.rs @@ -172,20 +172,35 @@ impl Tree { NAMED.with(|named| named.borrow_mut().remove(&n)) { std::mem::swap(&mut self.state, &mut state); - let mut widget_children = borrowed.children(); + let widget_children = borrowed.children(); if !tag_match || self.children.len() != widget_children.len() { self.children = borrowed.children(); } else { for (old_i, mut old) in children { - let Some(new) = widget_children.get_mut(old_i) else { - continue; - }; let Some(my_state) = self.children.get_mut(old_i) else { continue; }; - debug_assert!(old.tag == my_state.tag); - debug_assert!(old.id == new.id); + if my_state.tag != old.tag || { + !match (&old.id, &my_state.id) { + ( + Some(Id(Internal::Custom(_, ref old_name))), + Some(Id(Internal::Custom(_, ref my_name))), + ) => old_name == my_name, + ( + Some(Id(Internal::Set(a))), + Some(Id(Internal::Set(b))), + ) => a.len() == b.len(), + ( + Some(Id(Internal::Unique(_))), + Some(Id(Internal::Unique(_))), + ) => true, + (None, None) => true, + _ => false, + } + } { + continue; + } mem::swap(my_state, &mut old); } From 34973a56d512d11c1c3c343e2cb5c4c979d15e9c Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 31 May 2024 10:39:27 -0400 Subject: [PATCH 145/178] fix: update read and write methods so they don't recurse --- winit/src/clipboard.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/winit/src/clipboard.rs b/winit/src/clipboard.rs index d95a8957b0..e8fe6a2af2 100644 --- a/winit/src/clipboard.rs +++ b/winit/src/clipboard.rs @@ -125,11 +125,17 @@ impl Clipboard { impl crate::core::Clipboard for Clipboard { fn read(&self) -> Option { - self.read() + match &self.state { + State::Connected(clipboard, _) => clipboard.read().ok(), + State::Unavailable => None, + } } fn write(&mut self, contents: String) { - self.write(contents); + match &mut self.state { + State::Connected(clipboard, _) => _ = clipboard.write(contents), + State::Unavailable => {} + } } fn read_primary(&self) -> Option { From ce62217627a8c0a06a2bcc6934f56b1105dedd72 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 31 May 2024 18:56:16 -0400 Subject: [PATCH 146/178] fix: refactor dnd impl to support responsive widget --- core/src/element.rs | 14 +++++--- core/src/widget.rs | 1 + core/src/widget/operation.rs | 19 ++++++++++ core/src/widget/operation/search_id.rs | 42 ++++++++++++++++++++++ core/src/widget/tree.rs | 34 ++++++++++++++++-- runtime/src/user_interface.rs | 2 ++ widget/src/column.rs | 9 +++-- widget/src/container.rs | 2 ++ widget/src/lazy.rs | 2 ++ widget/src/lazy/component.rs | 2 ++ widget/src/lazy/responsive.rs | 17 +++++++-- widget/src/mouse_area.rs | 2 ++ widget/src/row.rs | 9 +++-- widget/src/scrollable.rs | 2 ++ winit/src/application.rs | 2 +- winit/src/multi_window.rs | 48 ++++++++++++++++++++------ 16 files changed, 181 insertions(+), 26 deletions(-) create mode 100644 core/src/widget/operation/search_id.rs diff --git a/core/src/element.rs b/core/src/element.rs index 74a0f9e928..563d7be071 100644 --- a/core/src/element.rs +++ b/core/src/element.rs @@ -506,9 +506,11 @@ where &self, state: &Tree, layout: Layout<'_>, + renderer: &Renderer, dnd_rectangles: &mut crate::clipboard::DndDestinationRectangles, ) { - self.widget.drag_destinations(state, layout, dnd_rectangles); + self.widget + .drag_destinations(state, layout, renderer, dnd_rectangles); } } @@ -667,11 +669,15 @@ where &self, state: &Tree, layout: Layout<'_>, + renderer: &Renderer, dnd_rectangles: &mut crate::clipboard::DndDestinationRectangles, ) { - self.element - .widget - .drag_destinations(state, layout, dnd_rectangles); + self.element.widget.drag_destinations( + state, + layout, + renderer, + dnd_rectangles, + ); } // TODO maybe a11y_nodes } diff --git a/core/src/widget.rs b/core/src/widget.rs index fdbadb97e2..a9f6a58e48 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -174,6 +174,7 @@ where &self, _state: &Tree, _layout: Layout<'_>, + _renderer: &Renderer, _dnd_rectangles: &mut crate::clipboard::DndDestinationRectangles, ) { } diff --git a/core/src/widget/operation.rs b/core/src/widget/operation.rs index ab4e74b74e..158bdf324f 100644 --- a/core/src/widget/operation.rs +++ b/core/src/widget/operation.rs @@ -1,6 +1,7 @@ //! Query or update internal widget state. pub mod focusable; pub mod scrollable; +pub mod search_id; pub mod text_input; pub use focusable::Focusable; @@ -129,6 +130,20 @@ impl Operation> for OperationWrapper { OperationWrapper::Wrapper(c) => c.as_ref().finish(), } } + + fn custom(&mut self, _state: &mut dyn Any, _id: Option<&Id>) { + match self { + OperationWrapper::Message(operation) => { + operation.custom(_state, _id); + } + OperationWrapper::Id(operation) => { + operation.custom(_state, _id); + } + OperationWrapper::Wrapper(operation) => { + operation.custom(_state, _id); + } + } + } } #[allow(missing_debug_implementations)] @@ -174,6 +189,10 @@ impl<'a, T, B> Operation for MapOperation<'a, B> { fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { self.operation.text_input(state, id) } + + fn custom(&mut self, state: &mut dyn Any, id: Option<&Id>) { + self.operation.custom(state, id); + } } /// A piece of logic that can traverse the widget tree of an application in diff --git a/core/src/widget/operation/search_id.rs b/core/src/widget/operation/search_id.rs new file mode 100644 index 0000000000..b6e330f779 --- /dev/null +++ b/core/src/widget/operation/search_id.rs @@ -0,0 +1,42 @@ +//! Search for widgets with the target Id. + +use super::Operation; +use crate::{id::Id, widget::operation::Outcome, Rectangle}; + +/// Produces an [`Operation`] that searches for the Id +pub fn search_id(target: Id) -> impl Operation { + struct Find { + found: bool, + target: Id, + } + + impl Operation for Find { + fn custom(&mut self, _state: &mut dyn std::any::Any, id: Option<&Id>) { + if Some(&self.target) == id { + self.found = true; + } + } + + fn container( + &mut self, + _id: Option<&Id>, + _bounds: Rectangle, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self); + } + + fn finish(&self) -> Outcome { + if self.found { + Outcome::Some(self.target.clone()) + } else { + Outcome::None + } + } + } + + Find { + found: false, + target, + } +} diff --git a/core/src/widget/tree.rs b/core/src/widget/tree.rs index db1744eb50..f1b37114b2 100644 --- a/core/src/widget/tree.rs +++ b/core/src/widget/tree.rs @@ -5,7 +5,6 @@ use std::any::{self, Any}; use std::borrow::{Borrow, BorrowMut, Cow}; use std::collections::HashMap; use std::hash::Hash; -use std::iter::zip; use std::{fmt, mem}; thread_local! { @@ -168,8 +167,37 @@ impl Tree { let mut needs_reset = false; let tag_match = self.tag == borrowed.tag(); if let Some(Id(Internal::Custom(_, n))) = borrowed.id() { - if let Some((mut state, children)) = - NAMED.with(|named| named.borrow_mut().remove(&n)) + if let Some((mut state, children)) = NAMED + .with(|named| named.borrow_mut().remove(&n)) + .or_else(|| { + //check self.id + if let Some(Id(Internal::Custom(_, ref name))) = self.id { + if name == &n { + Some(( + mem::replace(&mut self.state, State::None), + self.children + .iter_mut() + .map(|s| { + // take the data + mem::replace( + s, + Tree { + id: s.id.clone(), + tag: s.tag, + ..Tree::empty() + }, + ) + }) + .enumerate() + .collect(), + )) + } else { + None + } + } else { + None + } + }) { std::mem::swap(&mut self.state, &mut state); let widget_children = borrowed.children(); diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index 830318dbc8..672a4f4445 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -642,11 +642,13 @@ where pub fn dnd_rectangles( &self, prev_capacity: usize, + renderer: &Renderer, ) -> DndDestinationRectangles { let mut ret = DndDestinationRectangles::with_capacity(prev_capacity); self.root.as_widget().drag_destinations( &self.state, Layout::new(&self.base), + renderer, &mut ret, ); ret diff --git a/widget/src/column.rs b/widget/src/column.rs index 560a04eb85..0980439648 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -289,6 +289,7 @@ where &self, state: &Tree, layout: Layout<'_>, + renderer: &Renderer, dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles, ) { for ((e, layout), state) in self @@ -297,8 +298,12 @@ where .zip(layout.children()) .zip(state.children.iter()) { - e.as_widget() - .drag_destinations(state, layout, dnd_rectangles); + e.as_widget().drag_destinations( + state, + layout, + renderer, + dnd_rectangles, + ); } } } diff --git a/widget/src/container.rs b/widget/src/container.rs index 984ec06b3a..abd245fcae 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -306,12 +306,14 @@ where &self, state: &Tree, layout: Layout<'_>, + renderer: &Renderer, dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles, ) { if let Some(l) = layout.children().next() { self.content.as_widget().drag_destinations( state, l, + renderer, dnd_rectangles, ); } diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index 354e0e1600..a9b3316185 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -313,12 +313,14 @@ where &self, state: &Tree, layout: Layout<'_>, + renderer: &Renderer, dnd_rectangles: &mut core::clipboard::DndDestinationRectangles, ) { self.with_element(|element| { element.as_widget().drag_destinations( &state.children[0], layout, + renderer, dnd_rectangles, ); }); diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index 0c2b9d6216..75cf7b717c 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -545,12 +545,14 @@ where &self, state: &Tree, layout: Layout<'_>, + renderer: &Renderer, dnd_rectangles: &mut core::clipboard::DndDestinationRectangles, ) { self.with_element(|element| { element.as_widget().drag_destinations( &state.children[0], layout, + renderer, dnd_rectangles, ) }); diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index 009ee7533d..b13a53f85b 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -359,13 +359,24 @@ where &self, state: &Tree, layout: Layout<'_>, + renderer: &Renderer, dnd_rectangles: &mut core::clipboard::DndDestinationRectangles, ) { - self.content.borrow().element.as_widget().drag_destinations( - state, + let ret = self.content.borrow_mut().resolve( + &mut state.state.downcast_ref::().tree.borrow_mut(), + renderer, layout, - dnd_rectangles, + &self.view, + |tree, r, layout, element| { + element.as_widget().drag_destinations( + tree, + layout, + r, + dnd_rectangles, + ); + }, ); + ret } } diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index 5b0e3d36b8..0e45ffe8e5 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -271,12 +271,14 @@ where &self, state: &Tree, layout: Layout<'_>, + renderer: &Renderer, dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles, ) { if let Some(state) = state.children.iter().next() { self.content.as_widget().drag_destinations( state, layout, + renderer, dnd_rectangles, ); } diff --git a/widget/src/row.rs b/widget/src/row.rs index 0be2734580..e6884d8e2e 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -278,6 +278,7 @@ where &self, state: &Tree, layout: Layout<'_>, + renderer: &Renderer, dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles, ) { for ((e, layout), state) in self @@ -286,8 +287,12 @@ where .zip(layout.children()) .zip(state.children.iter()) { - e.as_widget() - .drag_destinations(state, layout, dnd_rectangles); + e.as_widget().drag_destinations( + state, + layout, + renderer, + dnd_rectangles, + ); } } } diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index d66b316d8e..72a25a635d 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -595,6 +595,7 @@ where &self, tree: &Tree, layout: Layout<'_>, + renderer: &Renderer, dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles, ) { let my_state = tree.state.downcast_ref::(); @@ -605,6 +606,7 @@ where self.content.as_widget().drag_destinations( c_state, c_layout, + renderer, &mut my_dnd_rectangles, ); let mut my_dnd_rectangles = my_dnd_rectangles.into_rectangles(); diff --git a/winit/src/application.rs b/winit/src/application.rs index dc1979c7f4..a8a898a655 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -893,7 +893,7 @@ async fn run_instance( )); let dnd_rectangles = user_interface - .dnd_rectangles(prev_dnd_rectangles_count); + .dnd_rectangles(prev_dnd_rectangles_count, &renderer); let new_dnd_rectangles_count = dnd_rectangles.as_ref().len(); diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 0065fc14fd..dfad04d42f 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -1006,16 +1006,40 @@ async fn run_instance( core::clipboard::DndSource::Widget( w, ) => { - // search windows for widget - user_interfaces.iter().find_map( - |(id, ui)| { - if ui. - find(&w).is_some() - { - Some(*id) - } else { - None + // search windows for widget with operation + user_interfaces.iter_mut().find_map( + |(ui_id, ui)| { + let mut current_operation = + Some(Box::new(OperationWrapper::Id(Box::new( + operation::search_id::search_id(w.clone()), + )))); + let Some(ui_renderer) = window_manager.get_mut(ui_id.clone()).map(|w| &w.renderer) else { + return None; + }; + while let Some(mut operation) = current_operation.take() + { + ui + .operate(&ui_renderer, operation.as_mut()); + + match operation.finish() { + operation::Outcome::None => { + } + operation::Outcome::Some(message) => { + match message { + operation::OperationOutputWrapper::Message(_) => { + unimplemented!(); + } + operation::OperationOutputWrapper::Id(_) => { + return Some(ui_id.clone()); + }, } + } + operation::Outcome::Chain(next) => { + current_operation = Some(Box::new(OperationWrapper::Wrapper(next))); + } + } + } + None }, ) }, @@ -1750,8 +1774,10 @@ where id, ); - let dnd_rectangles = interface - .dnd_rectangles(window.prev_dnd_destination_rectangles_count); + let dnd_rectangles = interface.dnd_rectangles( + window.prev_dnd_destination_rectangles_count, + &window.renderer, + ); let new_dnd_rectangles_count = dnd_rectangles.as_ref().len(); if new_dnd_rectangles_count > 0 || window.prev_dnd_destination_rectangles_count > 0 From 9ce8c6becc655be8448bbf069421ae2ddab1a18c Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 31 May 2024 18:59:39 -0400 Subject: [PATCH 147/178] cargo fmt --- winit/src/multi_window.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index dfad04d42f..0eaa7daa55 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -1020,7 +1020,6 @@ async fn run_instance( { ui .operate(&ui_renderer, operation.as_mut()); - match operation.finish() { operation::Outcome::None => { } From 323f80f197e2efe6057ce246dd68a2c7337026ec Mon Sep 17 00:00:00 2001 From: Victoria Brekenfeld Date: Tue, 28 May 2024 22:51:12 +0200 Subject: [PATCH 148/178] wgpu: fix nvidia gpu powering up in hybrid setups --- wgpu/src/window/compositor.rs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index dc641262a4..6ee3d90a4d 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -28,6 +28,23 @@ impl Compositor { settings: Settings, compatible_window: Option, ) -> Option { + #[cfg(unix)] + let ids = compatible_window.as_ref().and_then(get_wayland_device_ids); + + // HACK: + // 1. If we specifically didn't select an nvidia gpu + // 2. and nobody set an adapter name, + // 3. and the user didn't request the high power pref + // => don't load the nvidia icd, as it might power on the gpu in hybrid setups causing severe delays + #[cfg(unix)] + if !matches!(ids, Some((0x10de, _))) + && std::env::var_os("WGPU_ADAPTER_NAME").is_none() + && std::env::var("WGPU_POWER_PREF").as_deref() != Ok("high") + { + std::env::set_var("VK_LOADER_DRIVERS_DISABLE", "nvidia*"); + } + + // only load the instance after setting environment variables, this initializes the vulkan loader let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { backends: settings.internal_backend, ..Default::default() @@ -46,9 +63,6 @@ impl Compositor { ); } - #[cfg(unix)] - let ids = compatible_window.as_ref().and_then(get_wayland_device_ids); - #[allow(unsafe_code)] let compatible_surface = compatible_window .and_then(|window| instance.create_surface(window).ok()); From ef247e2b2d3ce5911d8d70faa27ae99952298fef Mon Sep 17 00:00:00 2001 From: Victoria Brekenfeld Date: Mon, 3 Jun 2024 17:14:33 +0200 Subject: [PATCH 149/178] wgpu: Fix querying adapter, even if we already have one --- wgpu/src/window/compositor.rs | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 6ee3d90a4d..757ea3b88d 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -103,20 +103,24 @@ impl Compositor { }); } - let adapter = adapter.or({ - instance - .request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::util::power_preference_from_env() - .unwrap_or(if settings.antialiasing.is_none() { - wgpu::PowerPreference::LowPower - } else { - wgpu::PowerPreference::HighPerformance - }), - compatible_surface: compatible_surface.as_ref(), - force_fallback_adapter: false, - }) - .await - })?; + let adapter = + match adapter { + Some(adapter) => adapter, + None => instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: + wgpu::util::power_preference_from_env().unwrap_or( + if settings.antialiasing.is_none() { + wgpu::PowerPreference::LowPower + } else { + wgpu::PowerPreference::HighPerformance + }, + ), + compatible_surface: compatible_surface.as_ref(), + force_fallback_adapter: false, + }) + .await?, + }; log::info!("Selected: {:#?}", adapter.get_info()); From c47171dabe3e88286fdb8bde236ef0c038e9d8fe Mon Sep 17 00:00:00 2001 From: Victoria Brekenfeld Date: Mon, 3 Jun 2024 17:15:08 +0200 Subject: [PATCH 150/178] wgpu: Fix wayland device id conversion --- wgpu/src/window/wayland.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/wgpu/src/window/wayland.rs b/wgpu/src/window/wayland.rs index a07d2180b6..a77b1150cd 100644 --- a/wgpu/src/window/wayland.rs +++ b/wgpu/src/window/wayland.rs @@ -104,7 +104,7 @@ pub fn get_wayland_device_ids(window: &W) -> Option<(u16, u16)> { let dev = feedback.main_device(); let path = PathBuf::from(format!( - "/sys/dev/char/{}:{}/device/drm", + "/sys/dev/char/{}:{}/device", major(dev), minor(dev) )); @@ -113,16 +113,22 @@ pub fn get_wayland_device_ids(window: &W) -> Option<(u16, u16)> { let mut file = File::open(&path).ok()?; let mut contents = String::new(); let _ = file.read_to_string(&mut contents).ok()?; - u16::from_str_radix(contents.trim_start_matches("0x"), 16) - .ok()? + u16::from_str_radix( + contents.trim().trim_start_matches("0x"), + 16, + ) + .ok()? }; let device = { let path = path.join("device"); let mut file = File::open(&path).ok()?; let mut contents = String::new(); let _ = file.read_to_string(&mut contents).ok()?; - u16::from_str_radix(contents.trim_start_matches("0x"), 16) - .ok()? + u16::from_str_radix( + contents.trim().trim_start_matches("0x"), + 16, + ) + .ok()? }; Some((vendor, device)) From f942212aae4ff6b0546b6d28969fc7a5e97b4fdd Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Mon, 10 Jun 2024 08:23:52 -0600 Subject: [PATCH 151/178] Adapt to new cosmic-text --- graphics/src/geometry/text.rs | 1 + graphics/src/text/editor.rs | 50 +++++++++++++++++++++-------------- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/graphics/src/geometry/text.rs b/graphics/src/geometry/text.rs index 6861009f91..cf688d4556 100644 --- a/graphics/src/geometry/text.rs +++ b/graphics/src/geometry/text.rs @@ -54,6 +54,7 @@ impl Text { f32::MAX, cosmic_text::Wrap::None, None, + 8, ); let translation_x = match self.horizontal_alignment { diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 4b12fa8330..43ba95c643 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -147,10 +147,8 @@ impl editor::Editor for Editor { Some(Rectangle { x, width, - y: (visual_line as i32 - + visual_lines_offset) - as f32 - * line_height, + y: (visual_line as f32) * line_height + + visual_lines_offset, height: line_height, }) } else { @@ -235,8 +233,7 @@ impl editor::Editor for Editor { Cursor::Caret(Point::new( offset, - (visual_lines_offset + visual_line as i32) as f32 - * line_height, + visual_line as f32 * line_height + visual_lines_offset, )) } } @@ -596,8 +593,9 @@ impl editor::Editor for Editor { let internal = self.internal(); let last_visible_line = internal.editor.with_buffer(|buffer| { + let metrics = buffer.metrics(); let scroll = buffer.scroll(); - let mut window = scroll.layout + buffer.visible_lines(); + let mut window = scroll.vertical + buffer.size().1; buffer .lines @@ -605,14 +603,20 @@ impl editor::Editor for Editor { .enumerate() .skip(scroll.line) .find_map(|(i, line)| { - let visible_lines = line + let layout = line .layout_opt() .as_ref() - .expect("Line layout should be cached") - .len() as i32; + .expect("Line layout should be cached"); + + let mut layout_height = 0.0; + for layout_line in layout.iter() { + layout_height += layout_line + .line_height_opt + .unwrap_or(metrics.line_height); + } - if window > visible_lines { - window -= visible_lines; + if window > layout_height { + window -= layout_height; None } else { Some(i) @@ -790,22 +794,28 @@ fn highlight_line( }) } -fn visual_lines_offset(line: usize, buffer: &cosmic_text::Buffer) -> i32 { +fn visual_lines_offset(line: usize, buffer: &cosmic_text::Buffer) -> f32 { + let metrics = buffer.metrics(); let scroll = buffer.scroll(); - let visual_lines_before_start: usize = buffer + + let mut height_before_start = 0.0; + buffer .lines .iter() .skip(scroll.line) .take(line) .map(|line| { - line.layout_opt() + let layout = line + .layout_opt() .as_ref() - .expect("Line layout should be cached") - .len() - }) - .sum(); + .expect("Line layout should be cached"); + for layout_line in layout.iter() { + height_before_start += + layout_line.line_height_opt.unwrap_or(metrics.line_height); + } + }); - visual_lines_before_start as i32 - scroll.layout + height_before_start - scroll.vertical } fn motion_to_action(motion: Motion) -> cosmic_text::Action { From 12415d8e280b542dead2480d4ce65d30d509be4c Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 11 Jun 2024 11:43:20 +0200 Subject: [PATCH 152/178] fix: unset VK_LOADER_DRIVERS_DISABLE after enumeration Allows applications to be launched on the NVIDIA GPU with Vulkan support --- wgpu/src/window/compositor.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 757ea3b88d..db702830c1 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -55,6 +55,8 @@ impl Compositor { let available_adapters = instance.enumerate_adapters(settings.internal_backend); + std::env::remove_var("VK_LOADER_DRIVERS_DISABLE"); + #[cfg(not(target_arch = "wasm32"))] if log::max_level() >= log::LevelFilter::Info { log::info!( From 7be016e8b1aaf46bba40f12a1bb16678fb14bbba Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Wed, 12 Jun 2024 08:03:11 -0600 Subject: [PATCH 153/178] Adapt to cosmic-text undefined width change --- graphics/src/geometry/text.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphics/src/geometry/text.rs b/graphics/src/geometry/text.rs index cf688d4556..9cadf71763 100644 --- a/graphics/src/geometry/text.rs +++ b/graphics/src/geometry/text.rs @@ -51,7 +51,7 @@ impl Text { let layout = buffer.layout( font_system.raw(), self.size.0, - f32::MAX, + None, cosmic_text::Wrap::None, None, 8, From 5409352708cf551c9d339f6b8b1c00cc9b4aa747 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Wed, 12 Jun 2024 09:16:12 -0600 Subject: [PATCH 154/178] Update for cosmic-text undefined buffer size --- graphics/src/text.rs | 7 ++++--- graphics/src/text/cache.rs | 4 ++-- graphics/src/text/editor.rs | 7 ++++--- graphics/src/text/paragraph.rs | 8 ++++---- tiny_skia/src/text.rs | 7 +++++-- wgpu/src/text.rs | 7 +++++-- 6 files changed, 24 insertions(+), 16 deletions(-) diff --git a/graphics/src/text.rs b/graphics/src/text.rs index 7c4b5e31fe..f3ad3eb916 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -98,11 +98,12 @@ pub fn measure(buffer: &cosmic_text::Buffer) -> Size { (run.line_w.max(width), total_lines + 1) }); - let (max_width, max_height) = buffer.size(); + let (max_width_opt, max_height_opt) = buffer.size(); Size::new( - width.min(max_width), - (total_lines as f32 * buffer.metrics().line_height).min(max_height), + width.min(max_width_opt.unwrap_or(f32::MAX)), + (total_lines as f32 * buffer.metrics().line_height) + .min(max_height_opt.unwrap_or(f32::MAX)), ) } diff --git a/graphics/src/text/cache.rs b/graphics/src/text/cache.rs index 7fb335678d..337295c7c8 100644 --- a/graphics/src/text/cache.rs +++ b/graphics/src/text/cache.rs @@ -52,8 +52,8 @@ impl Cache { buffer.set_size( font_system, - key.bounds.width, - key.bounds.height.max(key.line_height), + Some(key.bounds.width), + Some(key.bounds.height.max(key.line_height)), ); buffer.set_text( font_system, diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 43ba95c643..9f933788b9 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -559,8 +559,8 @@ impl editor::Editor for Editor { internal.editor.with_buffer_mut(|buffer| { buffer.set_size( font_system.raw(), - new_bounds.width, - new_bounds.height, + Some(new_bounds.width), + Some(new_bounds.height), ) }); @@ -595,7 +595,8 @@ impl editor::Editor for Editor { let last_visible_line = internal.editor.with_buffer(|buffer| { let metrics = buffer.metrics(); let scroll = buffer.scroll(); - let mut window = scroll.vertical + buffer.size().1; + let mut window = + scroll.vertical + buffer.size().1.unwrap_or(f32::MAX); buffer .lines diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs index 5d0275427a..00a8a52c17 100644 --- a/graphics/src/text/paragraph.rs +++ b/graphics/src/text/paragraph.rs @@ -77,8 +77,8 @@ impl core::text::Paragraph for Paragraph { buffer.set_size( font_system.raw(), - text.bounds.width, - text.bounds.height, + Some(text.bounds.width), + Some(text.bounds.height), ); buffer.set_text( @@ -116,8 +116,8 @@ impl core::text::Paragraph for Paragraph { internal.buffer.set_size( font_system.raw(), - new_bounds.width, - new_bounds.height, + Some(new_bounds.width), + Some(new_bounds.height), ); internal.bounds = new_bounds; diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index d87715261f..1e1801186f 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -161,13 +161,16 @@ impl Pipeline { ) { let mut font_system = font_system().write().expect("Write font system"); - let (width, height) = buffer.size(); + let (width_opt, height_opt) = buffer.size(); draw( font_system.raw(), &mut self.glyph_cache, buffer, - Rectangle::new(position, Size::new(width, height)), + Rectangle::new( + position, + Size::new(width_opt.unwrap_or(0.0), height_opt.unwrap_or(0.0)), + ), color, alignment::Horizontal::Left, alignment::Vertical::Top, diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index dca09cb8a8..c02b6ea70f 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -193,13 +193,16 @@ impl Pipeline { return None; }; - let (width, height) = buffer.size(); + let (width_opt, height_opt) = buffer.size(); ( buffer.as_ref(), Rectangle::new( text.position, - Size::new(width, height), + Size::new( + width_opt.unwrap_or(0.0), + height_opt.unwrap_or(0.0), + ), ), alignment::Horizontal::Left, alignment::Vertical::Top, From 31709bb2f622a7f1596d819da6840ca6777cf5f4 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Fri, 7 Jun 2024 15:28:46 -0700 Subject: [PATCH 155/178] sctk: Update `sctk`, `wayland-protocols` --- Cargo.toml | 4 ++-- sctk/src/event_loop/state.rs | 8 +++++--- sctk/src/handlers/compositor.rs | 25 +++++++++++++++++++++++-- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ba2a742626..aaf74f67be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -171,7 +171,7 @@ qrcode = { version = "0.12", default-features = false } raw-window-handle = "0.6" resvg = "0.37" rustc-hash = "1.0" -sctk = { package = "smithay-client-toolkit", git = "https://github.com/smithay/client-toolkit", rev = "3bed072" } +sctk = { package = "smithay-client-toolkit", version = "0.19.1" } smol = "1.0" smol_str = "0.2" softbuffer = { git = "https://github.com/pop-os/softbuffer", tag = "cosmic-4.0" } @@ -185,7 +185,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"] } +wayland-protocols = { version = "0.32.1", features = ["staging"] } web-sys = "0.3" web-time = "0.2" # wgpu = "0.19" diff --git a/sctk/src/event_loop/state.rs b/sctk/src/event_loop/state.rs index 7737fd59f3..0782ab3ae8 100644 --- a/sctk/src/event_loop/state.rs +++ b/sctk/src/event_loop/state.rs @@ -498,9 +498,11 @@ where settings.positioner.anchor_rect.width, settings.positioner.anchor_rect.height, ); - positioner.set_constraint_adjustment( - settings.positioner.constraint_adjustment, - ); + if let Ok(constraint_adjustment) = + settings.positioner.constraint_adjustment.try_into() + { + positioner.set_constraint_adjustment(constraint_adjustment); + } positioner.set_gravity(settings.positioner.gravity); positioner.set_offset( settings.positioner.offset.0, diff --git a/sctk/src/handlers/compositor.rs b/sctk/src/handlers/compositor.rs index 8c46af974e..dd5e3b429f 100644 --- a/sctk/src/handlers/compositor.rs +++ b/sctk/src/handlers/compositor.rs @@ -2,7 +2,10 @@ use sctk::{ compositor::CompositorHandler, delegate_compositor, - reexports::client::{protocol::wl_surface, Connection, QueueHandle}, + reexports::client::{ + protocol::{wl_output, wl_surface}, + Connection, QueueHandle, + }, }; use std::fmt::Debug; @@ -35,11 +38,29 @@ impl CompositorHandler for SctkState { _conn: &Connection, _qh: &QueueHandle, _surface: &wl_surface::WlSurface, - _new_transform: sctk::reexports::client::protocol::wl_output::Transform, + _new_transform: wl_output::Transform, ) { // TODO // this is not required } + + fn surface_enter( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &wl_surface::WlSurface, + _: &wl_output::WlOutput, + ) { + } + + fn surface_leave( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &wl_surface::WlSurface, + _: &wl_output::WlOutput, + ) { + } } delegate_compositor!(@ SctkState); From 3f39d5c2a695bd6667460b2a7c9b6e71685c3519 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Fri, 7 Jun 2024 18:56:32 -0700 Subject: [PATCH 156/178] sctk: Add alpha setting to `Subsurface` widget --- sctk/src/event_loop/mod.rs | 6 ++++++ sctk/src/subsurface_widget.rs | 31 +++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/sctk/src/event_loop/mod.rs b/sctk/src/event_loop/mod.rs index bb6b2f1afa..88df063b1d 100644 --- a/sctk/src/event_loop/mod.rs +++ b/sctk/src/event_loop/mod.rs @@ -325,6 +325,11 @@ where .registry_state .bind_one(&self.state.queue_handle, 2..=4, GlobalData) .ok(); + let wp_alpha_modifier = self + .state + .registry_state + .bind_one(&self.state.queue_handle, 1..=1, ()) + .ok(); if let Ok(wl_subcompositor) = wl_subcompositor { if let Ok(wp_viewporter) = wp_viewporter { callback( @@ -334,6 +339,7 @@ where wp_viewporter, wl_shm, wp_dmabuf, + wp_alpha_modifier, qh: self.state.queue_handle.clone(), buffers: HashMap::new(), }), diff --git a/sctk/src/subsurface_widget.rs b/sctk/src/subsurface_widget.rs index e39a19f74a..d50c28bc04 100644 --- a/sctk/src/subsurface_widget.rs +++ b/sctk/src/subsurface_widget.rs @@ -26,6 +26,7 @@ use sctk::{ compositor::SurfaceData, globals::GlobalData, reexports::client::{ + delegate_noop, protocol::{ wl_buffer::{self, WlBuffer}, wl_compositor::WlCompositor, @@ -40,6 +41,10 @@ use sctk::{ }; use wayland_backend::client::ObjectId; use wayland_protocols::wp::{ + alpha_modifier::v1::client::{ + wp_alpha_modifier_surface_v1::WpAlphaModifierSurfaceV1, + wp_alpha_modifier_v1::WpAlphaModifierV1, + }, linux_dmabuf::zv1::client::{ zwp_linux_buffer_params_v1::{self, ZwpLinuxBufferParamsV1}, zwp_linux_dmabuf_v1::{self, ZwpLinuxDmabufV1}, @@ -312,6 +317,7 @@ pub struct SubsurfaceState { pub wp_viewporter: WpViewporter, pub wl_shm: WlShm, pub wp_dmabuf: Option, + pub wp_alpha_modifier: Option, pub qh: QueueHandle>, pub(crate) buffers: HashMap>, } @@ -340,10 +346,16 @@ impl SubsurfaceState { sctk::globals::GlobalData, ); + let wp_alpha_modifier_surface = + self.wp_alpha_modifier.as_ref().map(|wp_alpha_modifier| { + wp_alpha_modifier.get_surface(&wl_surface, &self.qh, ()) + }); + SubsurfaceInstance { wl_surface, wl_subsurface, wp_viewport, + wp_alpha_modifier_surface, wl_buffer: None, bounds: None, } @@ -443,6 +455,7 @@ pub(crate) struct SubsurfaceInstance { pub(crate) wl_surface: WlSurface, wl_subsurface: WlSubsurface, wp_viewport: WpViewport, + wp_alpha_modifier_surface: Option, wl_buffer: Option, bounds: Option>, } @@ -509,6 +522,12 @@ impl SubsurfaceInstance { self.wl_surface.commit(); } + if let Some(wp_alpha_modifier_surface) = &self.wp_alpha_modifier_surface + { + let alpha = (info.alpha.clamp(0.0, 1.0) * u32::MAX as f32) as u32; + wp_alpha_modifier_surface.set_multiplier(alpha); + } + subsurface_ids.insert( self.wl_surface.id(), (info.bounds.x as i32, info.bounds.y as i32, parent_id), @@ -533,6 +552,7 @@ impl Drop for SubsurfaceInstance { pub(crate) struct SubsurfaceInfo { pub buffer: SubsurfaceBuffer, pub bounds: Rectangle, + pub alpha: f32, } thread_local! { @@ -550,6 +570,7 @@ pub struct Subsurface<'a> { width: Length, height: Length, content_fit: ContentFit, + alpha: f32, } impl<'a, Message, Theme, Renderer> Widget @@ -603,6 +624,7 @@ where subsurfaces.borrow_mut().push(SubsurfaceInfo { buffer: self.buffer.clone(), bounds: layout.bounds(), + alpha: self.alpha, }) }); } @@ -621,6 +643,7 @@ impl<'a> Subsurface<'a> { width: Length::Shrink, height: Length::Shrink, content_fit: ContentFit::Contain, + alpha: 1., } } @@ -638,6 +661,11 @@ impl<'a> Subsurface<'a> { self.content_fit = content_fit; self } + + pub fn alpha(mut self, alpha: f32) -> Self { + self.alpha = alpha; + self + } } impl<'a, Message, Theme, Renderer> From> @@ -650,3 +678,6 @@ where Self::new(subsurface) } } + +delegate_noop!(@ SctkState: ignore WpAlphaModifierV1); +delegate_noop!(@ SctkState: ignore WpAlphaModifierSurfaceV1); From 3b70fe34a4656cede87be655f188c5c3c59a417b Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Fri, 7 Jun 2024 19:58:20 -0700 Subject: [PATCH 157/178] sctk: Unmap subsurfaces instead of immediately destroying them Destroying a surface is immediate, rather than synchronized with commits. This fixes a flickering behavior with drag and drop in cosmic-workspaces. --- sctk/src/event_loop/mod.rs | 1 + sctk/src/subsurface_widget.rs | 21 ++++++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/sctk/src/event_loop/mod.rs b/sctk/src/event_loop/mod.rs index 88df063b1d..27f356a593 100644 --- a/sctk/src/event_loop/mod.rs +++ b/sctk/src/event_loop/mod.rs @@ -342,6 +342,7 @@ where wp_alpha_modifier, qh: self.state.queue_handle.clone(), buffers: HashMap::new(), + unmapped_subsurfaces: Vec::new(), }), &self.state, &mut control_flow, diff --git a/sctk/src/subsurface_widget.rs b/sctk/src/subsurface_widget.rs index d50c28bc04..77d63d34ab 100644 --- a/sctk/src/subsurface_widget.rs +++ b/sctk/src/subsurface_widget.rs @@ -320,6 +320,7 @@ pub struct SubsurfaceState { pub wp_alpha_modifier: Option, pub qh: QueueHandle>, pub(crate) buffers: HashMap>, + pub unmapped_subsurfaces: Vec, } impl SubsurfaceState { @@ -370,6 +371,13 @@ impl SubsurfaceState { subsurfaces: &mut Vec, view_subsurfaces: &[SubsurfaceInfo], ) { + // Subsurfaces aren't destroyed immediately to sync removal with parent + // surface commit. Since `destroy` is immediate. + // + // They should be safe to destroy by the next time `update_subsurfaces` + // is run. + self.unmapped_subsurfaces.clear(); + // Remove cached `wl_buffers` for any `BufferSource`s that no longer exist. self.buffers.retain(|k, v| { let retain = k.0.strong_count() > 0; @@ -380,9 +388,11 @@ impl SubsurfaceState { }); // If view requested fewer subsurfaces than there currently are, - // destroy excess. - if view_subsurfaces.len() < subsurfaces.len() { - subsurfaces.truncate(view_subsurfaces.len()); + // unmap excess. + while view_subsurfaces.len() < subsurfaces.len() { + let subsurface = subsurfaces.pop().unwrap(); + subsurface.unmap(); + self.unmapped_subsurfaces.push(subsurface); } // Create new subsurfaces if there aren't enough. while subsurfaces.len() < view_subsurfaces.len() { @@ -536,6 +546,11 @@ impl SubsurfaceInstance { self.wl_buffer = Some(buffer); self.bounds = Some(info.bounds); } + + pub fn unmap(&self) { + self.wl_surface.attach(None, 0, 0); + self.wl_surface.commit(); + } } impl Drop for SubsurfaceInstance { From 9a366ddad282728682d95cc90aa55af2a283b147 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Tue, 11 Jun 2024 18:09:16 -0700 Subject: [PATCH 158/178] Update `window_clipboard` --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index aaf74f67be..246d0f6357 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -192,9 +192,9 @@ web-time = "0.2" # Newer wgpu commit that fixes Vulkan backend on Nvidia wgpu = { git = "https://github.com/gfx-rs/wgpu", rev = "20fda69" } winapi = "0.3" -window_clipboard = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-6" } -dnd = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-6" } -mime = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-6" } +window_clipboard = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-7" } +dnd = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-7" } +mime = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-7" } # window_clipboard = { path = "../window_clipboard" } # dnd = { path = "../window_clipboard/dnd" } # mime = { path = "../window_clipboard/mime" } From 296b32267d5b7d8f16a1efcef249931694fcd19e Mon Sep 17 00:00:00 2001 From: Bjorn Ove Hay Andersen Date: Thu, 13 Jun 2024 12:41:29 +0200 Subject: [PATCH 159/178] iced_wgpu: don't query Wayland on macos --- wgpu/Cargo.toml | 2 +- wgpu/src/window.rs | 2 +- wgpu/src/window/compositor.rs | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 14694cdc12..5b5e7c6e69 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -43,7 +43,7 @@ resvg.optional = true tracing.workspace = true tracing.optional = true -[target.'cfg(unix)'.dependencies] +[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] rustix = { version = "0.38" } raw-window-handle.workspace = true sctk.workspace = true diff --git a/wgpu/src/window.rs b/wgpu/src/window.rs index d51038667a..baf8a05458 100644 --- a/wgpu/src/window.rs +++ b/wgpu/src/window.rs @@ -1,6 +1,6 @@ //! Display rendering results on windows. pub mod compositor; -#[cfg(unix)] +#[cfg(all(unix, not(target_os = "macos")))] mod wayland; pub use compositor::Compositor; diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index db702830c1..15919db5e7 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -6,7 +6,7 @@ use crate::graphics::compositor; use crate::graphics::{Error, Viewport}; use crate::{Backend, Primitive, Renderer, Settings}; -#[cfg(unix)] +#[cfg(all(unix, not(target_os = "macos")))] use super::wayland::get_wayland_device_ids; /// A window graphics backend for iced powered by `wgpu`. @@ -28,7 +28,7 @@ impl Compositor { settings: Settings, compatible_window: Option, ) -> Option { - #[cfg(unix)] + #[cfg(all(unix, not(target_os = "macos")))] let ids = compatible_window.as_ref().and_then(get_wayland_device_ids); // HACK: @@ -36,7 +36,7 @@ impl Compositor { // 2. and nobody set an adapter name, // 3. and the user didn't request the high power pref // => don't load the nvidia icd, as it might power on the gpu in hybrid setups causing severe delays - #[cfg(unix)] + #[cfg(all(unix, not(target_os = "macos")))] if !matches!(ids, Some((0x10de, _))) && std::env::var_os("WGPU_ADAPTER_NAME").is_none() && std::env::var("WGPU_POWER_PREF").as_deref() != Ok("high") @@ -72,7 +72,7 @@ impl Compositor { let mut adapter = None; #[cfg_attr(not(unix), allow(dead_code))] if std::env::var_os("WGPU_ADAPTER_NAME").is_none() { - #[cfg(unix)] + #[cfg(all(unix, not(target_os = "macos")))] if let Some((vendor_id, device_id)) = ids { adapter = available_adapters .into_iter() From b2921d54569db9efbe5d142b1055008de0de5140 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 19 Jun 2024 12:53:04 -0400 Subject: [PATCH 160/178] fix: only try to connect to clipboard if on linux --- Cargo.toml | 12 ++++++------ examples/todos/Cargo.toml | 12 ++++++++++-- winit/src/clipboard.rs | 1 + 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 246d0f6357..fbc6057a8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -192,10 +192,10 @@ web-time = "0.2" # Newer wgpu commit that fixes Vulkan backend on Nvidia wgpu = { git = "https://github.com/gfx-rs/wgpu", rev = "20fda69" } winapi = "0.3" -window_clipboard = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-7" } -dnd = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-7" } -mime = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-7" } -# window_clipboard = { path = "../window_clipboard" } -# dnd = { path = "../window_clipboard/dnd" } -# mime = { path = "../window_clipboard/mime" } +window_clipboard = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-8" } +dnd = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-8" } +mime = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-8" } +# window_clipboard = { path = "../../window_clipboard" } +# dnd = { path = "../../window_clipboard/dnd" } +# mime = { path = "../../window_clipboard/mime" } winit = { git = "https://github.com/pop-os/winit.git", branch = "winit-0.29" } diff --git a/examples/todos/Cargo.toml b/examples/todos/Cargo.toml index 6ce3bbcb49..6728c1395e 100644 --- a/examples/todos/Cargo.toml +++ b/examples/todos/Cargo.toml @@ -30,6 +30,14 @@ wasm-timer.workspace = true [package.metadata.deb] assets = [ - ["target/release-opt/todos", "usr/bin/iced-todos", "755"], - ["iced-todos.desktop", "usr/share/applications/", "644"], + [ + "target/release-opt/todos", + "usr/bin/iced-todos", + "755", + ], + [ + "iced-todos.desktop", + "usr/share/applications/", + "644", + ], ] diff --git a/winit/src/clipboard.rs b/winit/src/clipboard.rs index e8fe6a2af2..171ad5cdd1 100644 --- a/winit/src/clipboard.rs +++ b/winit/src/clipboard.rs @@ -37,6 +37,7 @@ impl Clipboard { .map(|c| State::Connected(c.0, c.1)) .unwrap_or(State::Unavailable); + #[cfg(target_os = "linux")] if let State::Connected(clipboard, _) = &state { clipboard.init_dnd(Box::new(proxy)); } From d0ba49851cb0b8829e760b75c9aef36d81bdb340 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 20 Jun 2024 11:52:20 -0400 Subject: [PATCH 161/178] fix: enable the tokio feature for accesskit_unix --- accessibility/Cargo.toml | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/accessibility/Cargo.toml b/accessibility/Cargo.toml index 59965df28f..0423ff97e3 100644 --- a/accessibility/Cargo.toml +++ b/accessibility/Cargo.toml @@ -8,12 +8,16 @@ edition = "2021" [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} +accesskit_unix = { git = "https://github.com/wash2/accesskit.git", branch = "winit-0.29", optional = true, default-features = false, features = [ + "tokio", +] } +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 = "../../../accesskit/common/", version = "0.12.2" } +# accesskit_unix = { path = "../../../accesskit/platforms/unix/", version = "0.7.1", optional = true, default-features = false, features = [ +# "tokio", +# ] } +# accesskit_windows = { path = "../../../accesskit/platforms/windows/", version = "0.16.0", optional = true } +# accesskit_macos = { path = "../../../accesskit/platforms/macos/", version = "0.11.0", optional = true } +# accesskit_winit = { path = "../../../accesskit/platforms/winit/", version = "0.18.1", optional = true } From e707391d60f716d953864f656fc453f3cfe88114 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 27 Jun 2024 19:56:53 -0400 Subject: [PATCH 162/178] refactor(sctk): convert window actions --- .../platform_specific/wayland/window.rs | 88 +++++++++++++++++++ sctk/src/application.rs | 10 ++- 2 files changed, 95 insertions(+), 3 deletions(-) diff --git a/runtime/src/command/platform_specific/wayland/window.rs b/runtime/src/command/platform_specific/wayland/window.rs index 81f24c30fb..0be812a66a 100644 --- a/runtime/src/command/platform_specific/wayland/window.rs +++ b/runtime/src/command/platform_specific/wayland/window.rs @@ -3,11 +3,14 @@ use std::marker::PhantomData; use iced_core::layout::Limits; use iced_core::window::Mode; +use iced_core::Size; use iced_futures::MaybeSend; use sctk::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge; use iced_core::window::Id; +use crate::window; + /// window settings #[derive(Debug, Clone)] pub struct SctkWindowSettings { @@ -309,3 +312,88 @@ impl fmt::Debug for Action { } } } + +/// error type for unsupported actions +#[derive(Debug, Clone, thiserror::Error)] +pub enum Error { + /// Not supported + #[error("Not supported")] + NotSupported, +} + +impl TryFrom> for Action { + type Error = Error; + + fn try_from(value: window::Action) -> Result { + match value { + window::Action::Spawn(id, settings) => { + let min = settings.min_size.unwrap_or(Size::new(1., 1.)); + let max = settings.max_size.unwrap_or(Size::INFINITY); + let builder = SctkWindowSettings { + window_id: id, + app_id: Some(settings.platform_specific.application_id), + title: None, + parent: None, + autosize: false, + size_limits: Limits::NONE + .min_width(min.width) + .min_height(min.height) + .max_width(max.width) + .max_height(max.height), + size: ( + settings.size.width.round() as u32, + settings.size.height.round() as u32, + ), + resizable: settings + .resizable + .then_some(settings.resize_border as f64), + client_decorations: settings.decorations, + transparent: settings.transparent, + xdg_activation_token: None, + }; + Ok(Action::Window { + builder, + _phantom: PhantomData, + }) + } + window::Action::Close(id) => Ok(Action::Destroy(id)), + window::Action::Resize(id, size) => Ok(Action::Size { + id, + width: size.width.round() as u32, + height: size.height.round() as u32, + }), + window::Action::Drag(id) => Ok(Action::InteractiveMove { id }), + window::Action::FetchSize(_, _) + | window::Action::FetchMaximized(_, _) + | window::Action::Move(_, _) + | window::Action::FetchMode(_, _) + | window::Action::ToggleMaximize(_) + | window::Action::ToggleDecorations(_) + | window::Action::RequestUserAttention(_, _) + | window::Action::GainFocus(_) + | window::Action::ChangeLevel(_, _) + | window::Action::ShowWindowMenu(_) + | window::Action::FetchId(_, _) + | window::Action::ChangeIcon(_, _) + | window::Action::Screenshot(_, _) + | window::Action::FetchMinimized(_, _) => Err(Error::NotSupported), + window::Action::Maximize(id, maximized) => { + if maximized { + Ok(Action::Maximize { id }) + } else { + Ok(Action::UnsetMaximize { id }) + } + } + window::Action::Minimize(id, bool) => { + if bool { + Ok(Action::Minimize { id }) + } else { + Err(Error::NotSupported) + } + } + window::Action::ChangeMode(id, mode) => { + Ok(Action::Mode(id, mode.into())) + } + } + } +} diff --git a/sctk/src/application.rs b/sctk/src/application.rs index 3265b43a04..dc66c363cf 100644 --- a/sctk/src/application.rs +++ b/sctk/src/application.rs @@ -46,7 +46,7 @@ use iced_runtime::{ self, platform_specific::{ self, - wayland::{data_device::DndIcon, popup}, + wayland::{data_device::DndIcon, popup, window}, }, }, core::{mouse::Interaction, touch, Color, Point, Size}, @@ -2073,9 +2073,12 @@ where proxy.send_event(Event::Message(message)); }, }, - command::Action::Window(..) => { - unimplemented!("Use platform specific events instead") + command::Action::Window(action) => { + if let Ok(a) = action.try_into() { + return handle_actions(application, cache, state, renderer, command::Action::PlatformSpecific(platform_specific::Action::Wayland(command::platform_specific::wayland::Action::Window(a))), runtime, proxy, debug, _graphics_info, auto_size_surfaces, clipboard); + } } + command::Action::Window(action) => {} command::Action::System(action) => match action { system::Action::QueryInformation(_tag) => { #[cfg(feature = "system")] @@ -2230,6 +2233,7 @@ where }; None } + pub fn build_user_interfaces<'a, A, C>( application: &'a A, renderer: &mut A::Renderer, From 4c609e776a7721bddb6d50b60096f4d1f655491c Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 28 Jun 2024 11:21:00 -0400 Subject: [PATCH 163/178] fix: settings.decorations enables SSD --- runtime/src/command/platform_specific/wayland/window.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/src/command/platform_specific/wayland/window.rs b/runtime/src/command/platform_specific/wayland/window.rs index 0be812a66a..e1c94ffc48 100644 --- a/runtime/src/command/platform_specific/wayland/window.rs +++ b/runtime/src/command/platform_specific/wayland/window.rs @@ -347,7 +347,7 @@ impl TryFrom> for Action { resizable: settings .resizable .then_some(settings.resize_border as f64), - client_decorations: settings.decorations, + client_decorations: !settings.decorations, transparent: settings.transparent, xdg_activation_token: None, }; From 6f83a8d16447b2590cb081f3e0f294678cc6caaf Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 5 Jul 2024 13:45:42 -0400 Subject: [PATCH 164/178] fix(core): state order and handling of new trees --- core/src/widget/tree.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/src/widget/tree.rs b/core/src/widget/tree.rs index f1b37114b2..078a64d164 100644 --- a/core/src/widget/tree.rs +++ b/core/src/widget/tree.rs @@ -76,7 +76,7 @@ impl Tree { let state = mem::replace(&mut tree.state, State::None); let children_count = tree.children.len(); let children = - tree.children.iter_mut().rev().enumerate().map(|(i, c)| { + tree.children.iter_mut().enumerate().rev().map(|(i, c)| { if matches!(c.id, Some(Id(Internal::Custom(_, _)))) { (c, None) } else { @@ -350,7 +350,11 @@ impl Tree { } for (new_tree, i) in new_trees { - self.children.insert(i, new_tree); + if self.children.len() > i { + self.children[i] = new_tree; + } else { + self.children.push(new_tree); + } } } } From 85b077d7874ca128457207bd89e9c7d1761be714 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Mon, 8 Jul 2024 20:41:54 -0600 Subject: [PATCH 165/178] Make text wrap configurable --- core/src/text.rs | 19 +++++++++++++++++++ core/src/widget/text.rs | 14 +++++++++++++- graphics/src/text.rs | 12 +++++++++++- graphics/src/text/paragraph.rs | 8 +++++++- widget/src/checkbox.rs | 14 ++++++++++++++ widget/src/overlay/menu.rs | 12 ++++++++++++ widget/src/pick_list.rs | 28 ++++++++++++++++++++++++++-- widget/src/radio.rs | 9 +++++++++ widget/src/text_input/text_input.rs | 3 +++ widget/src/toggler.rs | 9 +++++++++ 10 files changed, 123 insertions(+), 5 deletions(-) diff --git a/core/src/text.rs b/core/src/text.rs index 1598ef67b1..10d0c36252 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -40,6 +40,9 @@ pub struct Text<'a, Font> { /// The [`Shaping`] strategy of the [`Text`]. pub shaping: Shaping, + + /// The [`Wrap`] mode of the [`Text`]. + pub wrap: Wrap, } /// The shaping strategy of some text. @@ -66,6 +69,22 @@ pub enum Shaping { Advanced, } +/// The wrap mode of some text. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub enum Wrap { + /// No wraping + None, + /// Wraps at a glyph level + Glyph, + /// Wraps at a word level + Word, + /// Wraps at the word level, or fallback to glyph level if a word can't fit on a line by itself + /// + /// This is the default + #[default] + WordOrGlyph, +} + /// The height of a line of text in a paragraph. #[derive(Debug, Clone, Copy, PartialEq)] pub enum LineHeight { diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index 4c83053f7e..c8c73bf8bc 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -11,7 +11,7 @@ use crate::{ use std::borrow::Cow; -pub use text::{LineHeight, Shaping}; +pub use text::{LineHeight, Shaping, Wrap}; /// A paragraph of text. #[allow(missing_debug_implementations)] @@ -30,6 +30,7 @@ where vertical_alignment: alignment::Vertical, font: Option, shaping: Shaping, + wrap: Wrap, style: Theme::Style, } @@ -51,6 +52,7 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, shaping: Shaping::Advanced, + wrap: Default::default(), style: Default::default(), } } @@ -116,6 +118,12 @@ where self.shaping = shaping; self } + + /// Sets the [`Wrap`] mode of the [`Text`]. + pub fn wrap(mut self, wrap: Wrap) -> Self { + self.wrap = wrap; + self + } } /// The internal state of a [`Text`] widget. @@ -162,6 +170,7 @@ where self.horizontal_alignment, self.vertical_alignment, self.shaping, + self.wrap, ) } @@ -246,6 +255,7 @@ pub fn layout( horizontal_alignment: alignment::Horizontal, vertical_alignment: alignment::Vertical, shaping: Shaping, + wrap: Wrap, ) -> layout::Node where Renderer: text::Renderer, @@ -267,6 +277,7 @@ where horizontal_alignment, vertical_alignment, shaping, + wrap, }); paragraph.min_bounds() @@ -347,6 +358,7 @@ where font: self.font, style: self.style.clone(), shaping: self.shaping, + wrap: self.wrap, } } } diff --git a/graphics/src/text.rs b/graphics/src/text.rs index f3ad3eb916..b08b602f6c 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -10,7 +10,7 @@ pub use paragraph::Paragraph; pub use cosmic_text; use crate::core::font::{self, Font}; -use crate::core::text::Shaping; +use crate::core::text::{Shaping, Wrap}; use crate::core::{Color, Point, Rectangle, Size}; use once_cell::sync::OnceCell; @@ -171,6 +171,16 @@ pub fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping { } } +/// Converts some [`Wrap`] mode to a [`cosmic_text::Wrap`] strategy. +pub fn to_wrap(wrap: Wrap) -> cosmic_text::Wrap { + match wrap { + Wrap::None => cosmic_text::Wrap::None, + Wrap::Glyph => cosmic_text::Wrap::Glyph, + Wrap::Word => cosmic_text::Wrap::Word, + Wrap::WordOrGlyph => cosmic_text::Wrap::WordOrGlyph, + } +} + /// Converts some [`Color`] to a [`cosmic_text::Color`]. pub fn to_color(color: Color) -> cosmic_text::Color { let [r, g, b, a] = color.into_rgba8(); diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs index 00a8a52c17..ae298877b5 100644 --- a/graphics/src/text/paragraph.rs +++ b/graphics/src/text/paragraph.rs @@ -1,7 +1,7 @@ //! Draw paragraphs. use crate::core; use crate::core::alignment; -use crate::core::text::{Hit, LineHeight, Shaping, Text}; +use crate::core::text::{Hit, LineHeight, Shaping, Text, Wrap}; use crate::core::{Font, Pixels, Point, Size}; use crate::text; @@ -17,6 +17,7 @@ struct Internal { content: String, // TODO: Reuse from `buffer` (?) font: Font, shaping: Shaping, + wrap: Wrap, horizontal_alignment: alignment::Horizontal, vertical_alignment: alignment::Vertical, bounds: Size, @@ -81,6 +82,8 @@ impl core::text::Paragraph for Paragraph { Some(text.bounds.height), ); + buffer.set_wrap(font_system.raw(), text::to_wrap(text.wrap)); + buffer.set_text( font_system.raw(), text.content, @@ -97,6 +100,7 @@ impl core::text::Paragraph for Paragraph { horizontal_alignment: text.horizontal_alignment, vertical_alignment: text.vertical_alignment, shaping: text.shaping, + wrap: text.wrap, bounds: text.bounds, min_bounds, version: font_system.version(), @@ -141,6 +145,7 @@ impl core::text::Paragraph for Paragraph { horizontal_alignment: internal.horizontal_alignment, vertical_alignment: internal.vertical_alignment, shaping: internal.shaping, + wrap: internal.wrap, }); } } @@ -274,6 +279,7 @@ impl Default for Internal { content: String::new(), font: Font::default(), shaping: Shaping::default(), + wrap: Wrap::default(), horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, bounds: Size::ZERO, diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index c6f1fd26cc..cb2fb8cca4 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -63,6 +63,7 @@ pub struct Checkbox< text_size: Option, text_line_height: text::LineHeight, text_shaping: text::Shaping, + text_wrap: text::Wrap, font: Option, icon: Icon, style: ::Style, @@ -107,6 +108,7 @@ where text_size: None, text_line_height: text::LineHeight::default(), text_shaping: text::Shaping::Advanced, + text_wrap: text::Wrap::default(), font: None, icon: Icon { font: Renderer::ICON_FONT, @@ -114,6 +116,7 @@ where size: None, line_height: text::LineHeight::default(), shaping: text::Shaping::Advanced, + wrap: text::Wrap::default(), }, style: Default::default(), } @@ -158,6 +161,12 @@ where self } + /// Sets the [`text::Wrap`] mode of the [`Checkbox`]. + pub fn text_wrap(mut self, wrap: text::Wrap) -> Self { + self.text_wrap = wrap; + self + } + /// Sets the [`Renderer::Font`] of the text of the [`Checkbox`]. /// /// [`Renderer::Font`]: crate::core::text::Renderer @@ -258,6 +267,7 @@ where alignment::Horizontal::Left, alignment::Vertical::Top, self.text_shaping, + self.text_wrap, ) }, ) @@ -345,6 +355,7 @@ where size, line_height, shaping, + wrap, } = &self.icon; let size = size.unwrap_or(Pixels(bounds.height * 0.7)); @@ -359,6 +370,7 @@ where horizontal_alignment: alignment::Horizontal::Center, vertical_alignment: alignment::Vertical::Center, shaping: *shaping, + wrap: *wrap, }, bounds.center(), custom_style.icon_color, @@ -497,4 +509,6 @@ pub struct Icon { pub line_height: text::LineHeight, /// The shaping strategy of the icon. pub shaping: text::Shaping, + /// The wrap mode of the icon. + pub wrap: text::Wrap, } diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index e342468bac..9b4ff35955 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -39,6 +39,7 @@ pub struct Menu< text_size: Option, text_line_height: text::LineHeight, text_shaping: text::Shaping, + text_wrap: text::Wrap, font: Option, style: Theme::Style, } @@ -70,6 +71,7 @@ where text_size: None, text_line_height: text::LineHeight::default(), text_shaping: text::Shaping::Advanced, + text_wrap: text::Wrap::default(), font: None, style: Default::default(), } @@ -108,6 +110,12 @@ where self } + /// Sets the [`text::Wrap`] mode of the [`Menu`]. + pub fn text_wrap(mut self, wrap: text::Wrap) -> Self { + self.text_wrap = wrap; + self + } + /// Sets the font of the [`Menu`]. pub fn font(mut self, font: impl Into) -> Self { self.font = Some(font.into()); @@ -199,6 +207,7 @@ where text_size, text_line_height, text_shaping, + text_wrap, style, } = menu; @@ -211,6 +220,7 @@ where text_size, text_line_height, text_shaping, + text_wrap, padding, style: style.clone(), })); @@ -333,6 +343,7 @@ where text_size: Option, text_line_height: text::LineHeight, text_shaping: text::Shaping, + text_wrap: text::Wrap, font: Option, style: Theme::Style, } @@ -535,6 +546,7 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: self.text_shaping, + wrap: self.text_wrap, }, Point::new(bounds.x + self.padding.left, bounds.center_y()), if is_selected { diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index af2c3bebac..9f119959ad 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -45,6 +45,7 @@ pub struct PickList< text_size: Option, text_line_height: text::LineHeight, text_shaping: text::Shaping, + text_wrap: text::Wrap, font: Option, handle: Handle, style: Theme::Style, @@ -82,6 +83,7 @@ where text_size: None, text_line_height: text::LineHeight::default(), text_shaping: text::Shaping::Advanced, + text_wrap: text::Wrap::default(), font: None, handle: Handle::default(), style: Default::default(), @@ -127,6 +129,12 @@ where self } + /// Sets the [`text::Wrap`] mode of the [`PickList`]. + pub fn text_wrap(mut self, wrap: text::Wrap) -> Self { + self.text_wrap = wrap; + self + } + /// Sets the font of the [`PickList`]. pub fn font(mut self, font: impl Into) -> Self { self.font = Some(font.into()); @@ -192,6 +200,7 @@ where self.text_size, self.text_line_height, self.text_shaping, + self.text_wrap, self.font, self.placeholder.as_deref(), &self.options, @@ -252,6 +261,7 @@ where self.text_size, self.text_line_height, self.text_shaping, + self.text_wrap, font, self.placeholder.as_deref(), self.selected.as_ref(), @@ -276,6 +286,7 @@ where self.padding, self.text_size, self.text_shaping, + self.text_wrap, self.font.unwrap_or_else(|| renderer.default_font()), &self.options, &self.on_selected, @@ -377,6 +388,8 @@ pub struct Icon { pub line_height: text::LineHeight, /// The shaping strategy of the icon. pub shaping: text::Shaping, + /// The wrap mode of the icon. + pub wrap: text::Wrap, } /// Computes the layout of a [`PickList`]. @@ -389,6 +402,7 @@ pub fn layout( text_size: Option, text_line_height: text::LineHeight, text_shaping: text::Shaping, + text_wrap: text::Wrap, font: Option, placeholder: Option<&str>, options: &[T], @@ -416,6 +430,7 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: text_shaping, + wrap: text_wrap, }; for (option, paragraph) in options.iter().zip(state.options.iter_mut()) { @@ -579,6 +594,7 @@ pub fn overlay<'a, T, Message, Theme, Renderer>( padding: Padding, text_size: Option, text_shaping: text::Shaping, + text_wrap: text::Wrap, font: Renderer::Font, options: &'a [T], on_selected: &'a dyn Fn(T) -> Message, @@ -613,6 +629,7 @@ where .padding(padding) .font(font) .text_shaping(text_shaping) + .text_wrap(text_wrap) .style(style); if let Some(text_size) = text_size { @@ -635,6 +652,7 @@ pub fn draw<'a, T, Theme, Renderer>( text_size: Option, text_line_height: text::LineHeight, text_shaping: text::Shaping, + text_wrap: text::Wrap, font: Renderer::Font, placeholder: Option<&str>, selected: Option<&T>, @@ -673,6 +691,7 @@ pub fn draw<'a, T, Theme, Renderer>( *size, LineHeight::default(), text::Shaping::Advanced, + text::Wrap::default(), )), Handle::Static(Icon { font, @@ -680,7 +699,8 @@ pub fn draw<'a, T, Theme, Renderer>( size, line_height, shaping, - }) => Some((*font, *code_point, *size, *line_height, *shaping)), + wrap, + }) => Some((*font, *code_point, *size, *line_height, *shaping, *wrap)), Handle::Dynamic { open, closed } => { if state().is_open { Some(( @@ -689,6 +709,7 @@ pub fn draw<'a, T, Theme, Renderer>( open.size, open.line_height, open.shaping, + open.wrap, )) } else { Some(( @@ -697,13 +718,14 @@ pub fn draw<'a, T, Theme, Renderer>( closed.size, closed.line_height, closed.shaping, + closed.wrap, )) } } Handle::None => None, }; - if let Some((font, code_point, size, line_height, shaping)) = handle { + if let Some((font, code_point, size, line_height, shaping, wrap)) = handle { let size = size.unwrap_or_else(|| renderer.default_size()); renderer.fill_text( @@ -719,6 +741,7 @@ pub fn draw<'a, T, Theme, Renderer>( horizontal_alignment: alignment::Horizontal::Right, vertical_alignment: alignment::Vertical::Center, shaping, + wrap, }, Point::new( bounds.x + bounds.width - padding.horizontal(), @@ -747,6 +770,7 @@ pub fn draw<'a, T, Theme, Renderer>( horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: text_shaping, + wrap: text_wrap, }, Point::new(bounds.x + padding.left, bounds.center_y()), if is_selected { diff --git a/widget/src/radio.rs b/widget/src/radio.rs index cbc8d2924b..862a03cd14 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -83,6 +83,7 @@ where text_size: Option, text_line_height: text::LineHeight, text_shaping: text::Shaping, + text_wrap: text::Wrap, font: Option, style: Theme::Style, } @@ -127,6 +128,7 @@ where text_size: None, text_line_height: text::LineHeight::default(), text_shaping: text::Shaping::Advanced, + text_wrap: text::Wrap::default(), font: None, style: Default::default(), } @@ -171,6 +173,12 @@ where self } + /// Sets the [`text::Wrap`] mode of the [`Radio`] button. + pub fn text_wrap(mut self, wrap: text::Wrap) -> Self { + self.text_wrap = wrap; + self + } + /// Sets the text font of the [`Radio`] button. pub fn font(mut self, font: impl Into) -> Self { self.font = Some(font.into()); @@ -234,6 +242,7 @@ where alignment::Horizontal::Left, alignment::Vertical::Top, self.text_shaping, + self.text_wrap, ) }, ) diff --git a/widget/src/text_input/text_input.rs b/widget/src/text_input/text_input.rs index c34d658e1d..fcde1a7f6d 100644 --- a/widget/src/text_input/text_input.rs +++ b/widget/src/text_input/text_input.rs @@ -497,6 +497,7 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, + wrap: text::Wrap::default(), }; state.placeholder.update(placeholder_text); @@ -519,6 +520,7 @@ where horizontal_alignment: alignment::Horizontal::Center, vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, + wrap: text::Wrap::default(), }; state.icon.update(icon_text); @@ -1434,6 +1436,7 @@ fn replace_paragraph( horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, shaping: text::Shaping::Advanced, + wrap: text::Wrap::default(), }); } diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index 35a0e8537d..20d34bf2d5 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -61,6 +61,7 @@ pub struct Toggler< text_line_height: text::LineHeight, text_alignment: alignment::Horizontal, text_shaping: text::Shaping, + text_wrap: text::Wrap, spacing: f32, font: Option, style: Theme::Style, @@ -110,6 +111,7 @@ where text_line_height: text::LineHeight::default(), text_alignment: alignment::Horizontal::Left, text_shaping: text::Shaping::Advanced, + text_wrap: text::Wrap::default(), spacing: 0.0, font: None, style: Default::default(), @@ -155,6 +157,12 @@ where self } + /// Sets the [`text::Wrap`] mode of the [`Toggler`]. + pub fn text_wrap(mut self, wrap: text::Wrap) -> Self { + self.text_wrap = wrap; + self + } + /// Sets the spacing between the [`Toggler`] and the text. pub fn spacing(mut self, spacing: impl Into) -> Self { self.spacing = spacing.into().0; @@ -263,6 +271,7 @@ where self.text_alignment, alignment::Vertical::Top, self.text_shaping, + self.text_wrap, ) } else { layout::Node::new(crate::core::Size::ZERO) From 5ebfaaf505e2ffd9f6d696ce241a6ef88e9b2f26 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 10 Jul 2024 00:07:19 +0200 Subject: [PATCH 166/178] feat(sctk): support ShowWindowMenu --- .../platform_specific/wayland/window.rs | 16 ++++----- sctk/src/event_loop/mod.rs | 36 +++++++++++++++++-- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/runtime/src/command/platform_specific/wayland/window.rs b/runtime/src/command/platform_specific/wayland/window.rs index e1c94ffc48..c27085a1ac 100644 --- a/runtime/src/command/platform_specific/wayland/window.rs +++ b/runtime/src/command/platform_specific/wayland/window.rs @@ -159,10 +159,6 @@ pub enum Action { 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), @@ -201,9 +197,7 @@ impl Action { 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::ShowWindowMenu { id } => Action::ShowWindowMenu { id }, Action::InteractiveResize { id, edge } => { Action::InteractiveResize { id, edge } } @@ -274,9 +268,9 @@ impl fmt::Debug for Action { "Action::Window::InteractiveMove {{ id: {:?} }}", id ), - Action::ShowWindowMenu { id, x, y } => write!( + Action::ShowWindowMenu { id } => write!( f, - "Action::Window::ShowWindowMenu {{ id: {:?}, x: {x}, y: {y} }}", + "Action::Window::ShowWindowMenu {{ id: {:?} }}", id ), Action::InteractiveResize { id, edge } => write!( @@ -372,11 +366,13 @@ impl TryFrom> for Action { | window::Action::RequestUserAttention(_, _) | window::Action::GainFocus(_) | window::Action::ChangeLevel(_, _) - | window::Action::ShowWindowMenu(_) | window::Action::FetchId(_, _) | window::Action::ChangeIcon(_, _) | window::Action::Screenshot(_, _) | window::Action::FetchMinimized(_, _) => Err(Error::NotSupported), + window::Action::ShowWindowMenu(id) => { + Ok(Action::ShowWindowMenu { id }) + } window::Action::Maximize(id, maximized) => { if maximized { Ok(Action::Maximize { id }) diff --git a/sctk/src/event_loop/mod.rs b/sctk/src/event_loop/mod.rs index 27f356a593..681735be21 100644 --- a/sctk/src/event_loop/mod.rs +++ b/sctk/src/event_loop/mod.rs @@ -9,7 +9,7 @@ use crate::application::SurfaceIdWrapper; use crate::{ application::Event, conversion, - dpi::LogicalSize, + dpi::{LogicalPosition, LogicalSize, PhysicalPosition}, handlers::{ activation::IcedRequestData, wp_fractional_scaling::FractionalScalingManager, @@ -292,6 +292,8 @@ where F: FnMut(IcedSctkEvent, &SctkState, &mut ControlFlow), { let mut control_flow = ControlFlow::Poll; + let mut cursor_position = PhysicalPosition::new(0, 0); + let mut scale_factor = 1.0; callback( IcedSctkEvent::NewEvents(StartCause::Init), @@ -577,6 +579,30 @@ where // Handle pending sctk events. for event in sctk_event_sink_back_buffer.drain(..) { match event { + SctkEvent::PointerEvent { ref variant, .. } => { + cursor_position = LogicalPosition::new( + variant.position.0, + variant.position.1, + ) + .to_physical(scale_factor); + sticky_exit_callback( + IcedSctkEvent::SctkEvent(event), + &self.state, + &mut control_flow, + &mut callback, + ) + } + + SctkEvent::ScaleFactorChanged { ref factor, .. } => { + scale_factor = *factor; + sticky_exit_callback( + IcedSctkEvent::SctkEvent(event), + &self.state, + &mut control_flow, + &mut callback, + ) + } + SctkEvent::PopupEvent { variant: PopupEventVariant::Done, toplevel_id, @@ -937,7 +963,13 @@ where } } }, - platform_specific::wayland::window::Action::ShowWindowMenu { id: _, x: _, y: _ } => todo!(), + platform_specific::wayland::window::Action::ShowWindowMenu { 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)))) { + let PhysicalPosition { x, y } = cursor_position; + window.window.xdg_toplevel().show_window_menu(seat, last_press, x as i32, y as i32); + to_commit.insert(id, window.window.wl_surface().clone()); + } + }, 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); From 26a8598b393d90fbad113551cdfc4fc9e7ef259b Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 10 Jul 2024 16:21:40 +0200 Subject: [PATCH 167/178] improv(sctk): per-surface cursor position tracking --- sctk/src/event_loop/mod.rs | 74 ++++++++++++++++++++++---------------- 1 file changed, 43 insertions(+), 31 deletions(-) diff --git a/sctk/src/event_loop/mod.rs b/sctk/src/event_loop/mod.rs index 681735be21..f5b7fcce1a 100644 --- a/sctk/src/event_loop/mod.rs +++ b/sctk/src/event_loop/mod.rs @@ -47,7 +47,11 @@ use sctk::{ registry::RegistryState, seat::SeatState, session_lock::SessionLockState, - shell::{wlr_layer::LayerShell, xdg::XdgShell, WaylandSurface}, + shell::{ + wlr_layer::{LayerShell, LayerSurface}, + xdg::XdgShell, + WaylandSurface, + }, shm::Shm, }; use sctk::{ @@ -292,8 +296,8 @@ where F: FnMut(IcedSctkEvent, &SctkState, &mut ControlFlow), { let mut control_flow = ControlFlow::Poll; - let mut cursor_position = PhysicalPosition::new(0, 0); - let mut scale_factor = 1.0; + + let mut cursor_position = HashMap::<_, LogicalPosition>::new(); callback( IcedSctkEvent::NewEvents(StartCause::Init), @@ -580,27 +584,15 @@ where for event in sctk_event_sink_back_buffer.drain(..) { match event { SctkEvent::PointerEvent { ref variant, .. } => { - cursor_position = LogicalPosition::new( - variant.position.0, - variant.position.1, - ) - .to_physical(scale_factor); - sticky_exit_callback( - IcedSctkEvent::SctkEvent(event), - &self.state, - &mut control_flow, - &mut callback, - ) - } + let surface_id = variant.surface.id(); - SctkEvent::ScaleFactorChanged { ref factor, .. } => { - scale_factor = *factor; - sticky_exit_callback( - IcedSctkEvent::SctkEvent(event), - &self.state, - &mut control_flow, - &mut callback, - ) + cursor_position.insert( + surface_id, + LogicalPosition::new( + variant.position.0, + variant.position.1, + ), + ); } SctkEvent::PopupEvent { @@ -631,13 +623,18 @@ where &mut callback, ); } - None => continue, + None => (), }; + + continue; } + SctkEvent::LayerSurfaceEvent { variant: LayerSurfaceEventVariant::Done, id, } => { + cursor_position.remove(&id.id()); + if let Some(i) = self.state.layer_surfaces.iter().position(|l| { l.surface.wl_surface().id() == id.id() @@ -656,7 +653,10 @@ where &mut callback, ); } + + continue; } + SctkEvent::WindowEvent { variant: WindowEventVariant::Close, id, @@ -680,14 +680,18 @@ where &mut callback, ); } + + continue; } - _ => sticky_exit_callback( - IcedSctkEvent::SctkEvent(event), - &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. @@ -965,7 +969,15 @@ where }, platform_specific::wayland::window::Action::ShowWindowMenu { 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)))) { - let PhysicalPosition { x, y } = cursor_position; + let surface_id = window.window.wl_surface().id(); + + let cursor_position = cursor_position.get(&surface_id) + .cloned() + .unwrap_or_default(); + + // Cursor position does not need to be scaled here. + let PhysicalPosition { x, y } = cursor_position.to_physical::(1.0); + window.window.xdg_toplevel().show_window_menu(seat, last_press, x as i32, y as i32); to_commit.insert(id, window.window.wl_surface().clone()); } From dd2e93a54df9e5a711833d9551532e1794eda60f Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Thu, 11 Jul 2024 14:16:12 -0700 Subject: [PATCH 168/178] sctk: Fixes for cursor icon * With multiple windows, `SetCursor` is only sent for the focused window. Fixing a flicker between icons when two windows are using different cursors. * If there is a drag surface, let that surface set the cursor. And not any other. * Set cursor on `enter`, and when switching between CSDs and app area. Fixes https://github.com/pop-os/libcosmic/issues/533. --- sctk/src/application.rs | 15 +++++++++-- sctk/src/event_loop/mod.rs | 5 ++-- sctk/src/event_loop/state.rs | 12 +++++++++ sctk/src/handlers/seat/pointer.rs | 45 ++++++++++--------------------- sctk/src/handlers/seat/seat.rs | 2 ++ 5 files changed, 44 insertions(+), 35 deletions(-) diff --git a/sctk/src/application.rs b/sctk/src/application.rs index dc66c363cf..478dba5bc2 100644 --- a/sctk/src/application.rs +++ b/sctk/src/application.rs @@ -929,7 +929,7 @@ where // just draw here immediately and never again for dnd icons // TODO handle scale factor? - let _new_mouse_interaction = user_interface.draw( + let new_mouse_interaction = user_interface.draw( &mut renderer, state.theme(), &Style { @@ -940,6 +940,13 @@ where state.cursor(), ); + mouse_interaction = new_mouse_interaction; + ev_proxy.send_event(Event::SetCursor(mouse_interaction)); + // Pre-emptively remove cursor focus from other surface so they won't set cursor + for state in states.values_mut() { + state.cursor_position = None; + } + let subsurfaces = crate::subsurface_widget::take_subsurfaces(); if let Some(subsurface_state) = subsurface_state.as_mut() { subsurface_state.update_subsurfaces( @@ -1439,7 +1446,11 @@ where } debug.draw_finished(); - if new_mouse_interaction != mouse_interaction { + + // Set cursor if mouse interaction has changed, and surface has pointer focus + if state.cursor_position.is_some() + && new_mouse_interaction != mouse_interaction + { mouse_interaction = new_mouse_interaction; ev_proxy .send_event(Event::SetCursor(mouse_interaction)); diff --git a/sctk/src/event_loop/mod.rs b/sctk/src/event_loop/mod.rs index f5b7fcce1a..36fadfdd91 100644 --- a/sctk/src/event_loop/mod.rs +++ b/sctk/src/event_loop/mod.rs @@ -838,9 +838,10 @@ where }, }, Event::SetCursor(iced_icon) => { - if let Some(ptr) = self.state.seats.get(0).and_then(|s| s.ptr.as_ref()) { + if let Some(seat) = self.state.seats.get_mut(0) { let icon = conversion::cursor_icon(iced_icon); - let _ = ptr.set_cursor(self.wayland_dispatcher.as_source_ref().connection(), icon); + seat.icon = Some(icon); + seat.set_cursor(self.wayland_dispatcher.as_source_ref().connection(), icon); } } diff --git a/sctk/src/event_loop/state.rs b/sctk/src/event_loop/state.rs index 0782ab3ae8..00c64797b7 100644 --- a/sctk/src/event_loop/state.rs +++ b/sctk/src/event_loop/state.rs @@ -98,9 +98,21 @@ pub(crate) struct SctkSeat { pub(crate) last_touch_down: Option<(u32, i32, u32)>, // (time, point, serial) pub(crate) _modifiers: Modifiers, pub(crate) data_device: DataDevice, + // Cursor icon currently set (by CSDs, or application) + pub(crate) active_icon: Option, + // Cursor icon set by application pub(crate) icon: Option, } +impl SctkSeat { + pub(crate) fn set_cursor(&mut self, conn: &Connection, icon: CursorIcon) { + if let Some(ptr) = self.ptr.as_ref() { + ptr.set_cursor(conn, icon); + self.active_icon = Some(icon); + } + } +} + #[derive(Debug, Clone)] pub struct SctkWindow { pub(crate) id: window::Id, diff --git a/sctk/src/handlers/seat/pointer.rs b/sctk/src/handlers/seat/pointer.rs index 9777a320e4..eaddf814a0 100644 --- a/sctk/src/handlers/seat/pointer.rs +++ b/sctk/src/handlers/seat/pointer.rs @@ -94,41 +94,24 @@ impl PointerHandler for SctkState { 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); + if my_seat.active_icon != Some(icon) { + let _ = my_seat.set_cursor(conn, 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; - } + PointerEventKind::Enter { .. } => {} + PointerEventKind::Leave { .. } => {} _ => {} } - 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 my_seat.active_icon != Some(icon) { + my_seat.set_cursor(conn, icon); + } + } else if my_seat.active_icon != my_seat.icon { + // Restore cursor that was set by appliction, or default + my_seat.set_cursor( + conn, + my_seat.icon.unwrap_or(CursorIcon::Default), + ); } if is_active { @@ -144,7 +127,7 @@ impl PointerHandler for SctkState { } PointerEventKind::Leave { .. } => { my_seat.ptr_focus.take(); - my_seat.icon = None; + my_seat.active_icon = None; } PointerEventKind::Press { time, diff --git a/sctk/src/handlers/seat/seat.rs b/sctk/src/handlers/seat/seat.rs index adc5e0e842..ce90b7d86f 100644 --- a/sctk/src/handlers/seat/seat.rs +++ b/sctk/src/handlers/seat/seat.rs @@ -43,6 +43,7 @@ where last_kbd_press: None, last_touch_down: None, icon: None, + active_icon: None, }); } @@ -71,6 +72,7 @@ where last_kbd_press: None, last_touch_down: None, icon: None, + active_icon: None, }); self.seats.last_mut().unwrap() } From fd3d7c72bd19b4ee425e02649851b71391843c64 Mon Sep 17 00:00:00 2001 From: Ashley Wulber <48420062+wash2@users.noreply.github.com> Date: Tue, 16 Jul 2024 10:41:11 -0400 Subject: [PATCH 169/178] feat(sctk): support for overflow widget --- core/src/event/wayland/window.rs | 30 ++++++++++++-- sctk/src/application.rs | 20 +++++---- sctk/src/event_loop/mod.rs | 12 ++---- sctk/src/event_loop/state.rs | 31 ++++++++------ sctk/src/handlers/seat/pointer.rs | 59 +++++++++++++-------------- sctk/src/handlers/shell/xdg_window.rs | 26 ++---------- sctk/src/sctk_event.rs | 56 +++++++++++++++++-------- 7 files changed, 129 insertions(+), 105 deletions(-) diff --git a/core/src/event/wayland/window.rs b/core/src/event/wayland/window.rs index 210b1ce1ca..bd771f4b03 100644 --- a/core/src/event/wayland/window.rs +++ b/core/src/event/wayland/window.rs @@ -1,12 +1,34 @@ #![allow(missing_docs)] -use sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState}; +use sctk::{ + reexports::csd_frame::{WindowManagerCapabilities, WindowState}, + shell::xdg::window::WindowConfigure, +}; /// window events -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone)] pub enum WindowEvent { - /// window manager capabilities + /// Window manager capabilities. WmCapabilities(WindowManagerCapabilities), - /// window state + /// Window state. State(WindowState), + /// Window configure event. + Configure(WindowConfigure), +} + +impl PartialEq for WindowEvent { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::WmCapabilities(a), Self::WmCapabilities(b)) => a == b, + (Self::State(a), Self::State(b)) => a == b, + (Self::Configure(a), Self::Configure(b)) => { + a.capabilities == b.capabilities + && a.state == b.state + && a.decoration_mode == b.decoration_mode + && a.new_size == b.new_size + && a.suggested_bounds == b.suggested_bounds + } + _ => false, + } + } } diff --git a/sctk/src/application.rs b/sctk/src/application.rs index 478dba5bc2..dadb47a65d 100644 --- a/sctk/src/application.rs +++ b/sctk/src/application.rs @@ -535,7 +535,12 @@ where crate::sctk_event::WindowEventVariant::WmCapabilities(_) | crate::sctk_event::WindowEventVariant::ConfigureBounds { .. } => {} crate::sctk_event::WindowEventVariant::Configure( - configure, + current_size, + _, + wl_surface, + first, + ) | crate::sctk_event::WindowEventVariant::Size( + current_size, wl_surface, first, ) => { @@ -543,6 +548,7 @@ where let Some(state) = states.get_mut(&id.inner()) else { continue; }; + let (w, h) = auto_size_surfaces.get(id).map_or_else(|| (current_size.0.get(), current_size.1.get()), |(w, h, _, _)| (*w, *h)); if state.surface.is_none() { let wrapper = SurfaceDisplayWrapper { backend: backend.clone(), @@ -553,16 +559,12 @@ where simple_clipboard = unsafe {Clipboard::connect(&h)}; } } - 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()); + let mut c_surface = compositor.create_surface(wrapper.clone(), w, h); + compositor.configure_surface(&mut c_surface, w, h); 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); - } + state.set_logical_size(w as f32, h as f32); + if first { let user_interface = build_user_interface( &application, diff --git a/sctk/src/event_loop/mod.rs b/sctk/src/event_loop/mod.rs index 36fadfdd91..c89689a4e5 100644 --- a/sctk/src/event_loop/mod.rs +++ b/sctk/src/event_loop/mod.rs @@ -876,18 +876,12 @@ where }, 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())); + window.set_size(LogicalSize::new(NonZeroU32::new(width).unwrap_or(NonZeroU32::new(1).unwrap()), NonZeroU32::new(height).unwrap_or(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)); + if window.last_configure.is_some() { sticky_exit_callback( - IcedSctkEvent::SctkEvent(SctkEvent::WindowEvent { variant: WindowEventVariant::Configure(prev_configure, window.window.wl_surface().clone(), false), id: window.window.wl_surface().clone()}), + IcedSctkEvent::SctkEvent(SctkEvent::WindowEvent { variant: WindowEventVariant::Size(window.current_size, window.window.wl_surface().clone(), false), id: window.window.wl_surface().clone()}), &self.state, &mut control_flow, &mut callback, diff --git a/sctk/src/event_loop/state.rs b/sctk/src/event_loop/state.rs index 00c64797b7..5623f7e0b8 100644 --- a/sctk/src/event_loop/state.rs +++ b/sctk/src/event_loop/state.rs @@ -118,8 +118,8 @@ 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) requested_size: Option<(NonZeroU32, NonZeroU32)>, + pub(crate) current_size: (NonZeroU32, NonZeroU32), pub(crate) last_configure: Option, pub(crate) resizable: Option, /// Requests that SCTK window should perform. @@ -131,18 +131,24 @@ pub struct SctkWindow { 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) + self.requested_size = Some((logical_size.width, logical_size.height)); + self.update_size((Some(logical_size.width), Some(logical_size.height))) } pub(crate) fn update_size( &mut self, - LogicalSize { width, height }: LogicalSize, + (width, height): (Option, Option), ) { + let (width, height) = ( + width.unwrap_or_else(|| self.current_size.0), + height.unwrap_or_else(|| self.current_size.1), + ); + if self.current_size == (width, height) { + return; + } self.window .set_window_geometry(0, 0, width.get(), height.get()); - self.current_size = Some((width, height)); + self.current_size = (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. @@ -733,15 +739,16 @@ where fsm.fractional_scaling(window.wl_surface(), &self.queue_handle) }); + let w = NonZeroU32::new(size.0 as u32) + .unwrap_or_else(|| NonZeroU32::new(1).unwrap()); + let h = NonZeroU32::new(size.1 as u32) + .unwrap_or_else(|| NonZeroU32::new(1).unwrap()); 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(), - )), + requested_size: Some((w, h)), + current_size: (w, h), last_configure: None, _pending_requests: Vec::new(), resizable, diff --git a/sctk/src/handlers/seat/pointer.rs b/sctk/src/handlers/seat/pointer.rs index eaddf814a0..eec170964e 100644 --- a/sctk/src/handlers/seat/pointer.rs +++ b/sctk/src/handlers/seat/pointer.rs @@ -35,37 +35,36 @@ impl PointerHandler for SctkState { .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; + w.resizable.and_then(|border| { + let (width, height) = w.current_size; + 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 - } - }, - ) + 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 { diff --git a/sctk/src/handlers/shell/xdg_window.rs b/sctk/src/handlers/shell/xdg_window.rs index 6b4505737b..6dd9da14f4 100644 --- a/sctk/src/handlers/shell/xdg_window.rs +++ b/sctk/src/handlers/shell/xdg_window.rs @@ -37,7 +37,7 @@ impl WindowHandler for SctkState { _conn: &sctk::reexports::client::Connection, _qh: &sctk::reexports::client::QueueHandle, window: &sctk::shell::xdg::window::Window, - mut configure: sctk::shell::xdg::window::WindowConfigure, + configure: sctk::shell::xdg::window::WindowConfigure, _serial: u32, ) { let window = match self @@ -68,28 +68,7 @@ impl WindowHandler for SctkState { }); } - 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()), - ); - } - 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, - }); - } + window.update_size(configure.new_size); let wl_surface = window.window.wl_surface(); let id = wl_surface.clone(); @@ -98,6 +77,7 @@ impl WindowHandler for SctkState { self.sctk_events.push(SctkEvent::WindowEvent { variant: WindowEventVariant::Configure( + window.current_size, configure, wl_surface.clone(), first, diff --git a/sctk/src/sctk_event.rs b/sctk/src/sctk_event.rs index c7a1f211c6..5c9ac61b39 100755 --- a/sctk/src/sctk_event.rs +++ b/sctk/src/sctk_event.rs @@ -41,7 +41,7 @@ use sctk::{ xdg::{popup::PopupConfigure, window::WindowConfigure}, }, }; -use std::{collections::HashMap, time::Instant}; +use std::{collections::HashMap, num::NonZeroU32, time::Instant}; use wayland_protocols::wp::viewporter::client::wp_viewport::WpViewport; use xkeysym::Keysym; @@ -310,8 +310,8 @@ pub enum WindowEventVariant { height: u32, }, /// - Configure(WindowConfigure, WlSurface, bool), - + Configure((NonZeroU32, NonZeroU32), WindowConfigure, WlSurface, bool), + Size((NonZeroU32, NonZeroU32), WlSurface, bool), /// window state changed StateChanged(sctk::reexports::csd_frame::WindowState), /// Scale Factor @@ -674,27 +674,46 @@ impl SctkEvent { WindowEventVariant::ConfigureBounds { .. } => { Default::default() } - WindowEventVariant::Configure(configure, surface, _) => { - if let (Some(new_width), Some(new_height)) = - configure.new_size - { - surface_ids - .get(&surface.id()) - .map(|id| { + WindowEventVariant::Configure( + (new_width, new_height), + configure, + surface, + _, + ) => surface_ids + .get(&surface.id()) + .map(|id| { + if configure.is_resizing() { + vec![iced_runtime::core::Event::Window( + id.inner(), + window::Event::Resized { + width: new_width.get(), + height: new_height.get(), + }, + )] + } else { + vec![ iced_runtime::core::Event::Window( id.inner(), window::Event::Resized { width: new_width.get(), height: new_height.get(), }, - ) - }) - .into_iter() - .collect() - } else { - Default::default() - } - } + ), + iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::Window( + wayland::WindowEvent::Configure( + configure, + ), + surface, + id.inner(), + ), + ), + ), + ] + } + }) + .unwrap_or_default(), WindowEventVariant::ScaleFactorChanged(..) => { Default::default() } @@ -711,6 +730,7 @@ impl SctkEvent { }) .into_iter() .collect(), + WindowEventVariant::Size(_, _, _) => vec![], }, SctkEvent::LayerSurfaceEvent { variant, From 49d9bf7363579468ca288cffe15c107d6d42f942 Mon Sep 17 00:00:00 2001 From: Victoria Brekenfeld Date: Tue, 16 Jul 2024 18:23:09 +0200 Subject: [PATCH 170/178] wayland: Don't crash if libwayland isn't available --- wgpu/src/window/wayland.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/wgpu/src/window/wayland.rs b/wgpu/src/window/wayland.rs index a77b1150cd..024dbe2242 100644 --- a/wgpu/src/window/wayland.rs +++ b/wgpu/src/window/wayland.rs @@ -70,6 +70,10 @@ impl ProvidesRegistryState for AppData { } pub fn get_wayland_device_ids(window: &W) -> Option<(u16, u16)> { + if !wayland_sys::client::is_lib_available() { + return None; + } + let conn = match window.display_handle().map(|handle| handle.as_raw()) { #[allow(unsafe_code)] Ok(RawDisplayHandle::Wayland(WaylandDisplayHandle { From 3a9c6c33f2a7402db80eb5332e323fcc8fadb789 Mon Sep 17 00:00:00 2001 From: Victoria Brekenfeld Date: Tue, 16 Jul 2024 18:23:48 +0200 Subject: [PATCH 171/178] refactor: Extract ids_from_dev from wayland specific code --- wgpu/src/window.rs | 27 +++++++++++++++++++++++++++ wgpu/src/window/wayland.rs | 30 +----------------------------- 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/wgpu/src/window.rs b/wgpu/src/window.rs index baf8a05458..8af4797bc2 100644 --- a/wgpu/src/window.rs +++ b/wgpu/src/window.rs @@ -5,3 +5,30 @@ mod wayland; pub use compositor::Compositor; pub use wgpu::Surface; + +#[cfg(all(unix, not(target_os = "macos")))] +fn ids_from_dev(dev: u64) -> Option((u16, u16)) { + let path = PathBuf::from(format!( + "/sys/dev/char/{}:{}/device", + major(dev), + minor(dev) + )); + let vendor = { + let path = path.join("vendor"); + let mut file = File::open(&path).ok()?; + let mut contents = String::new(); + let _ = file.read_to_string(&mut contents).ok()?; + u16::from_str_radix(contents.trim().trim_start_matches("0x"), 16) + .ok()? + }; + let device = { + let path = path.join("device"); + let mut file = File::open(&path).ok()?; + let mut contents = String::new(); + let _ = file.read_to_string(&mut contents).ok()?; + u16::from_str_radix(contents.trim().trim_start_matches("0x"), 16) + .ok()? + }; + + Some((vendor, device)) +} diff --git a/wgpu/src/window/wayland.rs b/wgpu/src/window/wayland.rs index 024dbe2242..01d7aeac6f 100644 --- a/wgpu/src/window/wayland.rs +++ b/wgpu/src/window/wayland.rs @@ -107,35 +107,7 @@ pub fn get_wayland_device_ids(window: &W) -> Option<(u16, u16)> { }; let dev = feedback.main_device(); - let path = PathBuf::from(format!( - "/sys/dev/char/{}:{}/device", - major(dev), - minor(dev) - )); - let vendor = { - let path = path.join("vendor"); - let mut file = File::open(&path).ok()?; - let mut contents = String::new(); - let _ = file.read_to_string(&mut contents).ok()?; - u16::from_str_radix( - contents.trim().trim_start_matches("0x"), - 16, - ) - .ok()? - }; - let device = { - let path = path.join("device"); - let mut file = File::open(&path).ok()?; - let mut contents = String::new(); - let _ = file.read_to_string(&mut contents).ok()?; - u16::from_str_radix( - contents.trim().trim_start_matches("0x"), - 16, - ) - .ok()? - }; - - Some((vendor, device)) + super::ids_from_dev(dev) } _ => None, } From e6a8a071e03e83b34e9f4bdceeed51155575108e Mon Sep 17 00:00:00 2001 From: Victoria Brekenfeld Date: Tue, 16 Jul 2024 18:24:11 +0200 Subject: [PATCH 172/178] compositor: Add code to extract adapter from x11 --- wgpu/Cargo.toml | 3 ++ wgpu/src/window.rs | 9 +++- wgpu/src/window/compositor.rs | 7 +++- wgpu/src/window/wayland.rs | 2 - wgpu/src/window/x11.rs | 77 +++++++++++++++++++++++++++++++++++ 5 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 wgpu/src/window/x11.rs diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 5b5e7c6e69..8360548acd 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -51,3 +51,6 @@ wayland-protocols.workspace = true wayland-backend = { version = "0.3.3", features = ["client_system"] } wayland-client = { version = "0.31.2" } wayland-sys = { version = "0.31.1", features = ["dlopen"] } +as-raw-xcb-connection = "1.0.1" +tiny-xlib = "0.2.3" +x11rb = { version = "0.13.1", features = ["allow-unsafe-code", "dl-libxcb", "dri3"] } \ No newline at end of file diff --git a/wgpu/src/window.rs b/wgpu/src/window.rs index 8af4797bc2..92f1687372 100644 --- a/wgpu/src/window.rs +++ b/wgpu/src/window.rs @@ -2,12 +2,19 @@ pub mod compositor; #[cfg(all(unix, not(target_os = "macos")))] mod wayland; +#[cfg(all(unix, not(target_os = "macos")))] +mod x11; pub use compositor::Compositor; pub use wgpu::Surface; #[cfg(all(unix, not(target_os = "macos")))] -fn ids_from_dev(dev: u64) -> Option((u16, u16)) { +use rustix::fs::{major, minor}; +#[cfg(all(unix, not(target_os = "macos")))] +use std::{fs::File, io::Read, path::PathBuf}; + +#[cfg(all(unix, not(target_os = "macos")))] +fn ids_from_dev(dev: u64) -> Option<(u16, u16)> { let path = PathBuf::from(format!( "/sys/dev/char/{}:{}/device", major(dev), diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 15919db5e7..c0839451ac 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -8,6 +8,8 @@ use crate::{Backend, Primitive, Renderer, Settings}; #[cfg(all(unix, not(target_os = "macos")))] use super::wayland::get_wayland_device_ids; +#[cfg(all(unix, not(target_os = "macos")))] +use super::x11::get_x11_device_ids; /// A window graphics backend for iced powered by `wgpu`. #[allow(missing_debug_implementations)] @@ -29,7 +31,10 @@ impl Compositor { compatible_window: Option, ) -> Option { #[cfg(all(unix, not(target_os = "macos")))] - let ids = compatible_window.as_ref().and_then(get_wayland_device_ids); + let ids = compatible_window.as_ref().and_then(|window| { + get_wayland_device_ids(window) + .or_else(|| get_x11_device_ids(window)) + }); // HACK: // 1. If we specifically didn't select an nvidia gpu diff --git a/wgpu/src/window/wayland.rs b/wgpu/src/window/wayland.rs index 01d7aeac6f..b50f5d6909 100644 --- a/wgpu/src/window/wayland.rs +++ b/wgpu/src/window/wayland.rs @@ -1,12 +1,10 @@ use crate::graphics::compositor::Window; use raw_window_handle::{RawDisplayHandle, WaylandDisplayHandle}; -use rustix::fs::{major, minor}; use sctk::{ dmabuf::{DmabufFeedback, DmabufHandler, DmabufState}, registry::{ProvidesRegistryState, RegistryState}, registry_handlers, }; -use std::{fs::File, io::Read, path::PathBuf}; use wayland_client::{ backend::Backend, globals::registry_queue_init, protocol::wl_buffer, Connection, QueueHandle, diff --git a/wgpu/src/window/x11.rs b/wgpu/src/window/x11.rs new file mode 100644 index 0000000000..23c772ea2a --- /dev/null +++ b/wgpu/src/window/x11.rs @@ -0,0 +1,77 @@ +use crate::graphics::compositor::Window; + +use as_raw_xcb_connection::AsRawXcbConnection; +use raw_window_handle::{ + RawDisplayHandle, XcbDisplayHandle, XlibDisplayHandle, +}; +use rustix::fs::fstat; +use tiny_xlib::Display; +use x11rb::{ + connection::{Connection, RequestConnection}, + protocol::dri3::{ConnectionExt as _, X11_EXTENSION_NAME as DRI3_NAME}, + xcb_ffi::XCBConnection, +}; + +pub fn get_x11_device_ids(window: &W) -> Option<(u16, u16)> { + x11rb::xcb_ffi::load_libxcb().ok()?; + + #[allow(unsafe_code)] + let (conn, screen) = match window + .display_handle() + .map(|handle| handle.as_raw()) + { + #[allow(unsafe_code)] + Ok(RawDisplayHandle::Xlib(XlibDisplayHandle { + display, + screen, + .. + })) => match display { + Some(ptr) => unsafe { + let xlib_display = Display::from_ptr(ptr.as_ptr()); + let conn = XCBConnection::from_raw_xcb_connection( + xlib_display.as_raw_xcb_connection() as *mut _, + false, + ) + .ok(); + // intentially leak the display, we don't want to close the connection + + (conn?, screen) + }, + None => (XCBConnection::connect(None).ok()?.0, screen), + }, + Ok(RawDisplayHandle::Xcb(XcbDisplayHandle { + connection, + screen, + .. + })) => match connection { + Some(ptr) => ( + unsafe { + XCBConnection::from_raw_xcb_connection(ptr.as_ptr(), false) + .ok()? + }, + screen, + ), + None => (XCBConnection::connect(None).ok()?.0, screen), + }, + _ => { + return None; + } + }; + + // check for DRI3 + let _ = conn.extension_information(DRI3_NAME).ok()??; + // we have dri3, dri3_open exists on any version, so lets skip version checks. + + // provider being NONE tells the X server to use the RandR provider. + let screen = &conn.setup().roots[screen as usize]; + let dri3 = conn + .dri3_open(screen.root, x11rb::NONE) + .ok()? + .reply() + .ok()?; + let device_fd = dri3.device_fd; + let stat = fstat(device_fd).ok()?; + let dev = stat.st_rdev; + + super::ids_from_dev(dev) +} From cb315f66b8f1372d6b96c0e99abb8d2f79063a4e Mon Sep 17 00:00:00 2001 From: Victoria Brekenfeld Date: Tue, 16 Jul 2024 22:33:30 +0200 Subject: [PATCH 173/178] x11: Workaround nvidia driver lacking DRI --- wgpu/Cargo.toml | 2 +- wgpu/src/window/x11.rs | 126 +++++++++++++++++++++++++++++++++++------ 2 files changed, 111 insertions(+), 17 deletions(-) diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 8360548acd..15499d38f9 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -53,4 +53,4 @@ wayland-client = { version = "0.31.2" } wayland-sys = { version = "0.31.1", features = ["dlopen"] } as-raw-xcb-connection = "1.0.1" tiny-xlib = "0.2.3" -x11rb = { version = "0.13.1", features = ["allow-unsafe-code", "dl-libxcb", "dri3"] } \ No newline at end of file +x11rb = { version = "0.13.1", features = ["allow-unsafe-code", "dl-libxcb", "dri3", "randr"] } \ No newline at end of file diff --git a/wgpu/src/window/x11.rs b/wgpu/src/window/x11.rs index 23c772ea2a..58da401a2e 100644 --- a/wgpu/src/window/x11.rs +++ b/wgpu/src/window/x11.rs @@ -1,14 +1,26 @@ +use std::{ + fs, + io::{BufRead, BufReader}, + path::Path, +}; + use crate::graphics::compositor::Window; use as_raw_xcb_connection::AsRawXcbConnection; use raw_window_handle::{ RawDisplayHandle, XcbDisplayHandle, XlibDisplayHandle, }; -use rustix::fs::fstat; +use rustix::fs::{fstat, stat}; use tiny_xlib::Display; use x11rb::{ connection::{Connection, RequestConnection}, - protocol::dri3::{ConnectionExt as _, X11_EXTENSION_NAME as DRI3_NAME}, + protocol::{ + dri3::{ConnectionExt as _, X11_EXTENSION_NAME as DRI3_NAME}, + randr::{ + ConnectionExt as _, ProviderCapability, + X11_EXTENSION_NAME as RANDR_NAME, + }, + }, xcb_ffi::XCBConnection, }; @@ -57,21 +69,103 @@ pub fn get_x11_device_ids(window: &W) -> Option<(u16, u16)> { return None; } }; + let root = conn.setup().roots[screen as usize].root; - // check for DRI3 - let _ = conn.extension_information(DRI3_NAME).ok()??; - // we have dri3, dri3_open exists on any version, so lets skip version checks. + // The nvidia xorg driver advertises DRI2 and DRI3, + // but doesn't really return any useful data for either of them. + // We also can't query EGL, as a display created from an X11 display + // running on the properietary driver won't return an EGLDevice. + // + // So we have to resort to hacks. + + // check for randr + let _ = conn.extension_information(RANDR_NAME).ok()??; + // check version, because we need providers to exist + let version = conn.randr_query_version(1, 4).ok()?.reply().ok()?; + if version.major_version < 1 + || (version.major_version == 1 && version.minor_version < 4) + { + return None; + } + + // get the name of the first Source Output provider, that will be our main device + let randr = conn.randr_get_providers(root).ok()?.reply().ok()?; + let mut name = None; + for provider in randr.providers { + let info = conn + .randr_get_provider_info(provider, randr.timestamp) + .ok()? + .reply() + .ok()?; + if info + .capabilities + .contains(ProviderCapability::SOURCE_OUTPUT) + || name.is_none() + { + name = std::str::from_utf8(&info.name) + .ok() + .map(ToString::to_string); + } + } + + // if that name is formatted `NVIDIA-x`, then x represents the /dev/nvidiaX number, which we can relate to /dev/dri + if let Some(number) = name.and_then(|name| { + name.trim().strip_prefix("NVIDIA-")?.parse::().ok() + }) { + // let it be known, that I hate this "interface"... + for busid in fs::read_dir("/proc/driver/nvidia/gpus") + .ok()? + .map(Result::ok) + .flatten() + { + for line in BufReader::new( + fs::File::open(busid.path().join("information")).ok()?, + ) + .lines() + { + if let Ok(line) = line { + if line.starts_with("Device Minor") { + if let Some((_, num)) = line.split_once(":") { + let minor = num.trim().parse::().ok()?; + if minor == number { + // we found the device + for device in fs::read_dir( + Path::new("/sys/module/nvidia/drivers/pci:nvidia/") + .join(busid.file_name()) + .join("drm"), + ) + .ok()? + .map(Result::ok) + .flatten() + { + let device = device.file_name(); + if device.to_string_lossy().starts_with("card") + || device.to_string_lossy().starts_with("render") + { + let stat = + stat(Path::new("/dev/dri").join(device)).ok()?; + let dev = stat.st_rdev; + return super::ids_from_dev(dev); + } + } + } + } + } + } + } + } - // provider being NONE tells the X server to use the RandR provider. - let screen = &conn.setup().roots[screen as usize]; - let dri3 = conn - .dri3_open(screen.root, x11rb::NONE) - .ok()? - .reply() - .ok()?; - let device_fd = dri3.device_fd; - let stat = fstat(device_fd).ok()?; - let dev = stat.st_rdev; + None + } else { + // check via DRI3 + let _ = conn.extension_information(DRI3_NAME).ok()??; + // we have dri3, dri3_open exists on any version, so skip version checks. - super::ids_from_dev(dev) + // provider being NONE tells the X server to use the RandR provider. + let dri3 = conn.dri3_open(root, x11rb::NONE).ok()?.reply().ok()?; + let device_fd = dri3.device_fd; + let stat = fstat(device_fd).ok()?; + let dev = stat.st_rdev; + super::ids_from_dev(dev) + } } From fa817c704dd815cc5723470c49459ad8f81c78f8 Mon Sep 17 00:00:00 2001 From: Dominic Gerhauser Date: Mon, 22 Jul 2024 19:04:31 +0200 Subject: [PATCH 174/178] feat(mouse area): add double click mouse area: add double click --- Cargo.toml | 2 ++ widget/src/mouse_area.rs | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index fbc6057a8c..b4260463d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -199,3 +199,5 @@ mime = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd- # dnd = { path = "../../window_clipboard/dnd" } # mime = { path = "../../window_clipboard/mime" } winit = { git = "https://github.com/pop-os/winit.git", branch = "winit-0.29" } + + diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index 0e45ffe8e5..8ba079a53e 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -1,5 +1,6 @@ //! A container for capturing mouse events. +use iced_renderer::core::mouse::Click; use iced_renderer::core::widget::OperationOutputWrapper; use iced_renderer::core::Point; @@ -25,6 +26,7 @@ pub struct MouseArea< content: Element<'a, Message, Theme, Renderer>, on_drag: Option, on_press: Option, + on_double_press: Option, on_release: Option, on_right_press: Option, on_right_release: Option, @@ -48,6 +50,12 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { self.on_press = Some(message); self } + /// The message to emit on a left double button press. + #[must_use] + pub fn on_double_press(mut self, message: Message) -> Self { + self.on_double_press = Some(message); + self + } /// The message to emit on a left button release. #[must_use] @@ -102,12 +110,14 @@ struct State { // TODO: Support on_mouse_enter and on_mouse_exit drag_initiated: Option, is_out_of_bounds: bool, + last_click: Option, } impl Default for State { fn default() -> Self { Self { drag_initiated: Default::default(), is_out_of_bounds: true, + last_click: Default::default(), } } } @@ -121,6 +131,7 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { content: content.into(), on_drag: None, on_press: None, + on_double_press: None, on_release: None, on_right_press: None, on_right_release: None, @@ -330,6 +341,22 @@ fn update( return event::Status::Ignored; } + if let Some(message) = widget.on_double_press.as_ref() { + if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) = + event + { + if let Some(cursor_position) = cursor.position() { + let click = + mouse::Click::new(cursor_position, state.last_click); + state.last_click = Some(click); + if let mouse::click::Kind::Double = click.kind() { + shell.publish(message.clone()); + return event::Status::Captured; + } + } + } + } + if let Some(message) = widget.on_press.as_ref() { if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) = event @@ -394,6 +421,7 @@ fn update( return event::Status::Captured; } } + if let Some(message) = widget .on_mouse_enter .as_ref() From e31e29e99ecf56e446258287d235237e026e1f96 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 2 Aug 2024 09:34:05 -0400 Subject: [PATCH 175/178] fix: apply scale to lock surface --- sctk/src/application.rs | 12 +++++++++++ sctk/src/event_loop/mod.rs | 3 +++ sctk/src/event_loop/state.rs | 42 +++++++++++++++++++++++++++++++++++- sctk/src/sctk_event.rs | 6 ++++++ 4 files changed, 62 insertions(+), 1 deletion(-) diff --git a/sctk/src/application.rs b/sctk/src/application.rs index dadb47a65d..d6e6337b62 100644 --- a/sctk/src/application.rs +++ b/sctk/src/application.rs @@ -824,6 +824,15 @@ where destroyed_surface_ids.insert(surface.id(), surface_id); } } + SctkEvent::SessionLockSurfaceScaleFactorChanged { surface, scale_factor, viewport } => { + if let Some(state) = surface_ids + .get(&surface.id()) + .and_then(|id| states.get_mut(&id.inner())) + { + state.wp_viewport = viewport; + state.set_scale_factor(scale_factor); + } + } _ => {} } } @@ -2365,6 +2374,9 @@ where SctkEvent::SessionLockSurfaceDone { surface } => { &surface.id() == object_id } + SctkEvent::SessionLockSurfaceScaleFactorChanged { surface, .. } => { + &surface.id() == object_id + } SctkEvent::SessionUnlocked => false, } } diff --git a/sctk/src/event_loop/mod.rs b/sctk/src/event_loop/mod.rs index c89689a4e5..f9994d0656 100644 --- a/sctk/src/event_loop/mod.rs +++ b/sctk/src/event_loop/mod.rs @@ -516,6 +516,9 @@ where | SctkEvent::WindowEvent { variant: WindowEventVariant::ScaleFactorChanged(..), .. + } + | SctkEvent::SessionLockSurfaceScaleFactorChanged { + .. } => true, // ignore other events that shouldn't be in this buffer event => { diff --git a/sctk/src/event_loop/state.rs b/sctk/src/event_loop/state.rs index 5623f7e0b8..b86eacab2c 100644 --- a/sctk/src/event_loop/state.rs +++ b/sctk/src/event_loop/state.rs @@ -8,7 +8,7 @@ use crate::{ application::Event, dpi::LogicalSize, handlers::{ - wp_fractional_scaling::FractionalScalingManager, + shell::layer, wp_fractional_scaling::FractionalScalingManager, wp_viewporter::ViewporterState, }, sctk_event::{ @@ -240,6 +240,9 @@ pub struct SctkLockSurface { pub(crate) id: window::Id, pub(crate) session_lock_surface: SessionLockSurface, pub(crate) last_configure: Option, + pub(crate) scale_factor: Option, + pub(crate) wp_fractional_scale: Option, + pub(crate) wp_viewport: Option, } pub struct Dnd { @@ -460,6 +463,27 @@ impl SctkState { }); } + if let Some(l) = self + .lock_surfaces + .iter_mut() + .find(|l| l.session_lock_surface.wl_surface() == surface) + { + if legacy && self.fractional_scaling_manager.is_some() { + return; + } + l.scale_factor = Some(scale_factor); + let wl_surface = l.session_lock_surface.wl_surface(); + if legacy { + let _ = wl_surface.set_buffer_scale(scale_factor as i32); + } + self.compositor_updates.push( + SctkEvent::SessionLockSurfaceScaleFactorChanged { + surface: wl_surface.clone(), + scale_factor, + viewport: l.wp_viewport.clone(), + }, + ); + } // TODO winit sets cursor size after handling the change for the window, so maybe that should be done as well. } } @@ -864,10 +888,26 @@ where output, &self.queue_handle, ); + let wp_viewport = self.viewporter_state.as_ref().map(|state| { + state.get_viewport( + session_lock_surface.wl_surface(), + &self.queue_handle, + ) + }); + let wp_fractional_scale = + self.fractional_scaling_manager.as_ref().map(|fsm| { + fsm.fractional_scaling( + session_lock_surface.wl_surface(), + &self.queue_handle, + ) + }); self.lock_surfaces.push(SctkLockSurface { id, session_lock_surface, last_configure: None, + wp_viewport, + wp_fractional_scale, + scale_factor: None, }); Some(wl_surface) } else { diff --git a/sctk/src/sctk_event.rs b/sctk/src/sctk_event.rs index 5c9ac61b39..0d89347596 100755 --- a/sctk/src/sctk_event.rs +++ b/sctk/src/sctk_event.rs @@ -215,6 +215,11 @@ pub enum SctkEvent { SessionLockSurfaceDone { surface: WlSurface, }, + SessionLockSurfaceScaleFactorChanged { + surface: WlSurface, + scale_factor: f64, + viewport: Option, + }, SessionUnlocked, } @@ -988,6 +993,7 @@ impl SctkEvent { .into_iter() .collect() } + SctkEvent::SessionLockSurfaceScaleFactorChanged { .. } => vec![], } } } From b9e8fa5ca4794597762885f0464642ad790044f7 Mon Sep 17 00:00:00 2001 From: Peter Krull Date: Fri, 2 Aug 2024 18:43:06 +0200 Subject: [PATCH 176/178] Fix horizontal offset of slider when hovered or dragging to account for border --- widget/src/slider.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/widget/src/slider.rs b/widget/src/slider.rs index 5951d25cf4..9799b59f6d 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -554,8 +554,8 @@ pub fn draw( let offset = if range_start >= range_end { 0.0 } else { - (bounds.width - handle_width) * (value - range_start) - / (range_end - range_start) + (bounds.width - handle_width + 2.0 * border_width) * (value - range_start) + / (range_end - range_start) - border_width }; let rail_y = bounds.y + bounds.height / 2.0; From 9f26f7142e719583ba7c860a529b17490a271cff Mon Sep 17 00:00:00 2001 From: Peter Krull Date: Sat, 3 Aug 2024 11:37:39 +0200 Subject: [PATCH 177/178] Fixed height-squishing and handle-cursor position calculations --- widget/src/slider.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/widget/src/slider.rs b/widget/src/slider.rs index 9799b59f6d..e01804f04b 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -422,8 +422,11 @@ where let start = (*range.start()).into(); let end = (*range.end()).into(); - let percent = f64::from(cursor_position.x - bounds.x) - / f64::from(bounds.width); + // TODO - Should depend on the styling + const HANDLE_RADIUS: f64 = 10.0; + + let percent = (f64::from(cursor_position.x - bounds.x - HANDLE_RADIUS) + / f64::from(bounds.width - 2.0 * HANDLE_RADIUS)).clamp(0.0, 1.0); let steps = (percent * (end - start) / step).round(); let value = steps * step + start; @@ -531,7 +534,7 @@ pub fn draw( .min(bounds.width); let height = (f32::from(height)) .max(2.0 * border_width) - .min(bounds.height); + .min(bounds.height + 2.0 * border_width); let mut border_radius: [f32; 4] = border_radius.into(); for r in &mut border_radius { *r = (*r) From e888eb7d1b73ceb9965846a774647d40a9a5976a Mon Sep 17 00:00:00 2001 From: Peter Krull Date: Sat, 3 Aug 2024 12:48:07 +0200 Subject: [PATCH 178/178] Formatting, fixed placement of const value --- widget/src/slider.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/widget/src/slider.rs b/widget/src/slider.rs index e01804f04b..4614f8ed6c 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -425,8 +425,10 @@ where // TODO - Should depend on the styling const HANDLE_RADIUS: f64 = 10.0; - let percent = (f64::from(cursor_position.x - bounds.x - HANDLE_RADIUS) - / f64::from(bounds.width - 2.0 * HANDLE_RADIUS)).clamp(0.0, 1.0); + let percent = ((f64::from(cursor_position.x - bounds.x) + - HANDLE_RADIUS) + / (f64::from(bounds.width) - 2.0 * HANDLE_RADIUS)) + .clamp(0.0, 1.0); let steps = (percent * (end - start) / step).round(); let value = steps * step + start; @@ -557,8 +559,10 @@ pub fn draw( let offset = if range_start >= range_end { 0.0 } else { - (bounds.width - handle_width + 2.0 * border_width) * (value - range_start) - / (range_end - range_start) - border_width + (bounds.width - handle_width + 2.0 * border_width) + * (value - range_start) + / (range_end - range_start) + - border_width }; let rail_y = bounds.y + bounds.height / 2.0;