diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 80bbcacda2..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 diff --git a/.github/workflows/document.yml b/.github/workflows/document.yml index 62e28ca3b7..14aea25d7a 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..fa42f9a047 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,11 +2,19 @@ name: Lint on: [push, pull_request] jobs: all: - runs-on: macOS-latest + runs-on: ubuntu-latest steps: - 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 a11y" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9c5ee0d949..db548fa31d 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 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 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/CHANGELOG.md b/CHANGELOG.md index fdd832e495..65e305d0bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,11 @@ Many thanks to... - @akshayr-mecha - @dtzxporter +Many thanks to... +- @edfloreshz +- @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 0afbcd513d..159fdd4e39 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,6 @@ all-features = true maintenance = { status = "actively-developed" } [features] -default = ["wgpu"] # Enable the `wgpu` GPU-accelerated renderer backend wgpu = ["iced_renderer/wgpu", "iced_widget/wgpu"] # Enables the `Image` widget @@ -32,7 +31,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 +40,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,9 +47,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", "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"] [dependencies] iced_core.workspace = true @@ -61,41 +64,46 @@ iced_renderer.workspace = true 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 +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", + "sctk" ] +[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,15 +126,17 @@ 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_sctk = { version = "0.1", path = "sctk" } +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" } futures = "0.3" glam = "0.24" -glyphon = { git = "https://github.com/grovesNL/glyphon.git", rev = "2caa9fc5e5923c1d827d177c3619cab7e9885b85" } +glyphon = { git = "https://github.com/jackpot51/glyphon.git" } guillotiere = "0.6" half = "2.2" image = "0.24" @@ -142,10 +152,11 @@ ouroboros = "0.17" palette = "0.7" qrcode = { version = "0.12", default-features = false } raw-window-handle = "0.5" -resvg = "0.36" +resvg = "0.37" rustc-hash = "1.0" +sctk = { package = "smithay-client-toolkit", git = "https://github.com/smithay/client-toolkit", rev = "2e9bf9f" } smol = "1.0" -softbuffer = "0.2" +softbuffer = { git = "https://github.com/pop-os/softbuffer", tag = "v0.3-cosmic" } syntect = "5.1" sysinfo = "0.28" thiserror = "1.0" @@ -156,8 +167,11 @@ xxhash-rust = { version = "0.8", features = ["xxh3"] } unicode-segmentation = "1.0" wasm-bindgen-futures = "0.4" wasm-timer = "0.2" -web-sys = "0.3" +wayland-protocols = { version = "0.31.0", features = [ "staging"]} +web-sys = "=0.3.64" wgpu = "0.18" winapi = "0.3" window_clipboard = "0.3" -winit = { git = "https://github.com/iced-rs/winit.git", rev = "c52db2045d0a2f1b8d9923870de1d4ab1994146e", default-features = false } +[workspace.dependencies.winit] +git = "https://github.com/pop-os/winit.git" +branch = "master" diff --git a/accessibility/Cargo.toml b/accessibility/Cargo.toml new file mode 100644 index 0000000000..338980e154 --- /dev/null +++ b/accessibility/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "iced_accessibility" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +# TODO Ashley re-export more platform adapters + +[dependencies] +accesskit = { git = "https://github.com/wash2/accesskit.git", tag = "winit-0.28", version = "0.11.0" } +accesskit_unix = { git = "https://github.com/wash2/accesskit.git", tag = "winit-0.28", version = "0.4.0", optional = true } +accesskit_windows = { git = "https://github.com/wash2/accesskit.git", tag = "winit-0.28", version = "0.14.0", optional = true} +accesskit_macos = { git = "https://github.com/wash2/accesskit.git", tag = "winit-0.28", version = "0.7.0", optional = true} +accesskit_winit = { git = "https://github.com/wash2/accesskit.git", tag = "winit-0.28", version = "0.13.0", optional = true} +# accesskit = { path = "../../fork/accesskit/common/", version = "0.11.0" } +# accesskit_unix = { path = "../../fork/accesskit/platforms/unix/", version = "0.4.0", optional = true } +# accesskit_windows = { path = "../../fork/accesskit/platforms/windows/", version = "0.14.0", optional = true} +# accesskit_macos = { path = "../../fork/accesskit/platforms/macos/", version = "0.7.0", optional = true} +# accesskit_winit = { path = "../../fork/accesskit/platforms/winit/", version = "0.13.0", optional = true} diff --git a/accessibility/src/a11y_tree.rs b/accessibility/src/a11y_tree.rs new file mode 100644 index 0000000000..bb61981571 --- /dev/null +++ b/accessibility/src/a11y_tree.rs @@ -0,0 +1,80 @@ +use crate::{A11yId, A11yNode, IdEq}; + +#[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| IdEq::eq(n.id(), id)) + || self.children.iter().any(|n| IdEq::eq(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..cc99d496d0 --- /dev/null +++ b/accessibility/src/node.rs @@ -0,0 +1,46 @@ +use accesskit::NodeClassSet; + +use crate::A11yId; + +#[derive(Debug, Clone, PartialEq)] +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..88e1d9208e --- /dev/null +++ b/accessibility/src/traits.rs @@ -0,0 +1,19 @@ +use std::borrow::Cow; + +use crate::A11yId; + +#[derive(Debug, Clone, PartialEq)] +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 4672c754ac..544ef5d22d 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -10,16 +10,26 @@ homepage.workspace = true categories.workspace = true keywords.workspace = true +[features] +a11y = ["iced_accessibility"] +wayland = ["iced_accessibility?/accesskit_unix", "sctk"] + [dependencies] bitflags.workspace = true log.workspace = true thiserror.workspace = true xxhash-rust.workspace = true num-traits.workspace = true - +sctk.workspace = true +sctk.optional = true palette.workspace = true palette.optional = true +[dependencies.serde] +version = "1" +optional = true +features = ["serde_derive"] + [target.'cfg(target_arch = "wasm32")'.dependencies] instant.workspace = true @@ -28,3 +38,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 dea111afc7..d055926751 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; @@ -10,7 +11,7 @@ use crate::{ }; use std::any::Any; -use std::borrow::Borrow; +use std::borrow::{Borrow, BorrowMut}; /// A generic [`Widget`]. /// @@ -254,6 +255,68 @@ impl<'a, Message, Renderer> Borrow + 'a> } } +impl<'a, Message, Renderer> Borrow + 'a> + for &mut Element<'a, Message, Renderer> +{ + fn borrow(&self) -> &(dyn Widget + 'a) { + self.widget.borrow() + } +} + +impl<'a, Message, Renderer> BorrowMut + 'a> + for &mut Element<'a, Message, Renderer> +{ + fn borrow_mut(&mut self) -> &mut (dyn Widget + 'a) { + self.widget.borrow_mut() + } +} + +impl<'a, Message, Renderer> BorrowMut + 'a> + for Element<'a, Message, Renderer> +{ + fn borrow_mut(&mut self) -> &mut (dyn Widget + 'a) { + self.widget.borrow_mut() + } +} + +impl<'a, Message, Renderer> Widget + for Element<'a, Message, Renderer> +where + Renderer: crate::Renderer, +{ + fn width(&self) -> Length { + self.widget.width() + } + + fn height(&self) -> Length { + self.widget.height() + } + + fn layout( + &self, + _tree: &mut Tree, + _renderer: &Renderer, + _limits: &layout::Limits, + ) -> layout::Node { + self.widget.layout(_tree, _renderer, _limits) + } + + fn draw( + &self, + _state: &Tree, + _renderer: &mut Renderer, + _theme: &::Theme, + _style: &renderer::Style, + _layout: Layout<'_>, + _cursor: Cursor, + _viewport: &Rectangle, + ) { + self.widget.draw( + _state, _renderer, _theme, _style, _layout, _cursor, _viewport, + ); + } +} + struct Map<'a, A, B, Renderer> { widget: Box + 'a>, mapper: Box B + 'a>, @@ -292,8 +355,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 width(&self) -> Length { @@ -318,7 +381,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, @@ -445,6 +510,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, Renderer: crate::Renderer> { @@ -486,7 +569,7 @@ where self.element.widget.children() } - fn diff(&self, tree: &mut Tree) { + fn diff(&mut self, tree: &mut Tree) { self.element.widget.diff(tree); } @@ -504,7 +587,9 @@ where state: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn widget::Operation< + widget::OperationOutputWrapper, + >, ) { self.element .widget @@ -585,4 +670,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..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 @@ -24,13 +26,23 @@ 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), } /// A platform specific event -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq)] pub enum PlatformSpecific { + /// A Wayland specific event + #[cfg(feature = "wayland")] + Wayland(wayland::Event), /// A MacOS specific event MacOS(MacOS), } diff --git a/core/src/event/wayland/data_device.rs b/core/src/event/wayland/data_device.rs new file mode 100644 index 0000000000..342352ef37 --- /dev/null +++ b/core/src/event/wayland/data_device.rs @@ -0,0 +1,141 @@ +use sctk::{ + data_device_manager::ReadPipe, + reexports::client::protocol::wl_data_device_manager::DndAction, +}; +use std::{ + os::fd::{AsRawFd, OwnedFd}, + 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..f7bfd5bac7 --- /dev/null +++ b/core/src/event/wayland/mod.rs @@ -0,0 +1,62 @@ +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), +} + +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/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/id.rs b/core/src/id.rs new file mode 100644 index 0000000000..3fc74ce925 --- /dev/null +++ b/core/src/id.rs @@ -0,0 +1,155 @@ +//! 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 +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(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() + } + // 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 { + /// 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, + (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 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; + + #[test] + fn unique_generates_different_ids() { + let a = Id::unique(); + let b = Id::unique(); + + assert_ne!(a, b); + } +} 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/core/src/keyboard/key_code.rs b/core/src/keyboard/key_code.rs index 74ead170bf..0a08055984 100644 --- a/core/src/keyboard/key_code.rs +++ b/core/src/keyboard/key_code.rs @@ -4,6 +4,7 @@ /// /// [`winit`]: https://docs.rs/winit/0.20.0-alpha3/winit/ #[derive(Debug, Hash, Ord, PartialOrd, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[repr(u32)] #[allow(missing_docs)] pub enum KeyCode { diff --git a/core/src/lib.rs b/core/src/lib.rs index 54ea58394c..59ada0032b 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -17,7 +17,13 @@ rustdoc::broken_intra_doc_links )] pub mod alignment; +mod angle; +mod background; +mod border_radius; pub mod clipboard; +mod color; +pub mod content_fit; +mod element; pub mod event; pub mod font; pub mod gradient; @@ -34,13 +40,9 @@ pub mod touch; pub mod widget; pub mod window; -mod angle; -mod background; -mod border_radius; -mod color; -mod content_fit; -mod element; mod hasher; +#[cfg(not(feature = "a11y"))] +pub mod id; mod length; mod padding; mod pixels; @@ -62,6 +64,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/mouse/button.rs b/core/src/mouse/button.rs index 3eec7f42f0..eb62eef337 100644 --- a/core/src/mouse/button.rs +++ b/core/src/mouse/button.rs @@ -10,6 +10,12 @@ pub enum Button { /// The middle (wheel) button. Middle, + /// The forward button. + Forward, + + /// The back button. + Back, + /// Some other button. Other(u16), } diff --git a/core/src/overlay.rs b/core/src/overlay.rs index af10afee60..61833d1980 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::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. @@ -42,12 +42,12 @@ where cursor: mouse::Cursor, ); - /// Applies a [`widget::Operation`] to the [`Overlay`]. + /// Applies an [`Operation`] to the [`Overlay`]. fn operate( &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 a279fe2859..8b4d907448 100644 --- a/core/src/overlay/element.rs +++ b/core/src/overlay/element.rs @@ -4,7 +4,7 @@ use crate::event::{self, Event}; use crate::layout; use crate::mouse; use crate::renderer; -use crate::widget; +use crate::widget::{self, Operation, OperationOutputWrapper}; use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size, Vector}; use std::any::Any; @@ -117,7 +117,7 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { self.overlay.operate(layout, renderer, operation); } @@ -174,7 +174,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 e1e9727a40..c0bf43e9a0 100644 --- a/core/src/overlay/group.rs +++ b/core/src/overlay/group.rs @@ -3,7 +3,9 @@ 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::{ Clipboard, Event, Layout, Overlay, Point, Rectangle, Shell, Size, Vector, }; @@ -137,7 +139,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/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/core/src/renderer.rs b/core/src/renderer.rs index 1b327e563c..dc8b55429a 100644 --- a/core/src/renderer.rs +++ b/core/src/renderer.rs @@ -50,14 +50,20 @@ pub struct 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 + pub scale_factor: f64, } impl Default for Style { fn default() -> Self { Style { + icon_color: Color::BLACK, text_color: Color::BLACK, + scale_factor: 1.0, } } } diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index 7accd34e10..0ad1f82455 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -44,6 +44,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'; @@ -54,7 +55,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]>) {} @@ -77,6 +78,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 edef79c236..1598ef67b1 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) } } @@ -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/core/src/widget.rs b/core/src/widget.rs index 294d59847e..ccd6331924 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; @@ -92,7 +90,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( @@ -100,7 +98,7 @@ where _state: &mut Tree, _layout: Layout<'_>, _renderer: &Renderer, - _operation: &mut dyn Operation, + _operation: &mut dyn Operation>, ) { } @@ -144,4 +142,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/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 e020b0303e..a617419f2f 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -18,6 +18,7 @@ where Renderer: text::Renderer, Renderer::Theme: StyleSheet, { + id: crate::widget::Id, content: Cow<'a, str>, size: Option, line_height: LineHeight, @@ -38,6 +39,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(), @@ -46,7 +48,7 @@ where height: Length::Shrink, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, - shaping: Shaping::Basic, + shaping: Shaping::Advanced, style: Default::default(), } } @@ -185,6 +187,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()) + } + + 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 ff52b1ce42..99c82032ad 100644 --- a/core/src/widget/tree.rs +++ b/core/src/widget/tree.rs @@ -1,9 +1,11 @@ //! Store internal widget state in a state tree to ensure continuity. +use crate::id::{Id, Internal}; use crate::Widget; - use std::any::{self, Any}; -use std::borrow::Borrow; +use std::borrow::{Borrow, BorrowMut}; +use std::collections::HashMap; use std::fmt; +use std::hash::Hash; /// A persistent state widget tree. /// @@ -13,6 +15,9 @@ pub struct Tree { /// The tag of the [`Tree`]. pub tag: Tag, + /// the Id of the [`Tree`] + pub id: Option, + /// The [`State`] of the [`Tree`]. pub state: State, @@ -24,6 +29,7 @@ impl Tree { /// Creates an empty, stateless [`Tree`] with no children. pub fn empty() -> Self { Self { + id: None, tag: Tag::stateless(), state: State::None, children: Vec::new(), @@ -40,6 +46,7 @@ impl Tree { let widget = widget.borrow(); Self { + id: widget.id(), tag: widget.tag(), state: widget.state(), children: widget.children(), @@ -56,12 +63,27 @@ impl Tree { /// [`Widget::diff`]: crate::Widget::diff pub fn diff<'a, Message, Renderer>( &mut self, - new: impl Borrow + 'a>, + mut new: impl BorrowMut + 'a>, ) where Renderer: crate::Renderer, { - if self.tag == new.borrow().tag() { - new.borrow().diff(self); + let borrowed: &mut dyn Widget = new.borrow_mut(); + if self.tag == borrowed.tag() { + // TODO can we take here? + if let Some(id) = self.id.clone() { + if matches!(id, Id(Internal::Custom(_, _))) { + borrowed.set_id(id); + } else if borrowed.id() == Some(id.clone()) { + for (old_c, new_c) in + self.children.iter_mut().zip(borrowed.children()) + { + old_c.id = new_c.id; + } + } else { + borrowed.set_id(id); + } + } + borrowed.diff(self) } else { *self = Self::new(new); } @@ -70,32 +92,80 @@ impl Tree { /// Reconciles the children of the tree with the provided list of widgets. pub fn diff_children<'a, Message, Renderer>( &mut self, - new_children: &[impl Borrow + 'a>], + new_children: &mut [impl BorrowMut + 'a>], ) where Renderer: crate::Renderer, { self.diff_children_custom( new_children, - |tree, widget| tree.diff(widget.borrow()), - |widget| Self::new(widget.borrow()), - ); + new_children.iter().map(|c| c.borrow().id()).collect(), + |tree, widget| { + let borrowed: &mut dyn Widget<_, _> = widget.borrow_mut(); + tree.diff(borrowed) + }, + |widget| { + let borrowed: &dyn Widget<_, _> = widget.borrow(); + Self::new(borrowed) + }, + ) } /// 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 let Internal::Custom(_, ref name) = id.0 { + let _ = id_map.insert(name.to_string(), 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| { + 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]; + if len_changed { + c.id = new_id.clone(); + } + child_state_i += 1; + c + } else { + continue; + }; + diff(child_state, new); } @@ -114,8 +184,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 +253,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/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/examples/editor/Cargo.toml b/examples/editor/Cargo.toml index a3f6ea3b45..f9127cb2c2 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/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/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"] } diff --git a/examples/integration/src/controls.rs b/examples/integration/src/controls.rs index 4714c397e7..c81db9e72f 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 276794c820..03be223cb9 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; @@ -153,6 +154,7 @@ pub fn main() -> Result<(), Box> { ); let mut state = program::State::new( + Id::MAIN, controls, viewport.logical_size(), &mut renderer, @@ -197,6 +199,7 @@ pub fn main() -> Result<(), Box> { if !state.is_queue_empty() { // We update iced let _ = state.update( + Id::MAIN, viewport.logical_size(), cursor_position .map(|p| { @@ -211,6 +214,8 @@ pub fn main() -> Result<(), Box> { &Theme::Dark, &renderer::Style { text_color: Color::WHITE, + scale_factor: viewport.scale_factor(), + icon_color: Color::WHITE, }, &mut clipboard, &mut debug, diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index acb14372f1..804123fc75 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -224,7 +224,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; @@ -277,8 +279,8 @@ mod modal { ] } - fn diff(&self, tree: &mut widget::Tree) { - tree.diff_children(&[&self.base, &self.modal]); + fn diff(&mut self, tree: &mut widget::Tree) { + tree.diff_children(&mut [&mut self.base, &mut self.modal]); } fn width(&self) -> Length { @@ -385,7 +387,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], @@ -504,7 +506,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/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/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/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 d82ea84108..52ce2d73eb 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -3,12 +3,13 @@ use iced::widget::{ button, column, container, horizontal_space, progress_bar, radio, row, scrollable, slider, text, vertical_space, }; -use iced::{executor, theme, Alignment, Color}; -use iced::{Application, Command, Element, Length, Settings, Theme}; +use iced::{executor, theme, Alignment, Application, Color}; +use iced::{Command, Element, Length, Settings, Theme}; +use iced_core::id::Id; 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..f85265563a --- /dev/null +++ b/examples/sctk_drag/src/main.rs @@ -0,0 +1,252 @@ +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::MAIN, + 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 { + border_color: Color::from_rgb(1.0, 0.0, 0.0), + border_radius: 2.0.into(), + border_width: 2.0, + ..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..28ef6246e6 --- /dev/null +++ b/examples/sctk_todos/Cargo.toml @@ -0,0 +1,22 @@ +[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", "image",] } +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" +async-std = "1.0" +directories-next = "2.0.0" + +[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 0000000000..4498299db2 Binary files /dev/null and b/examples/sctk_todos/fonts/icons.ttf differ 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..ce986c85b6 --- /dev/null +++ b/examples/sctk_todos/src/main.rs @@ -0,0 +1,655 @@ +use env_logger::Env; +use iced::alignment::{self, Alignment}; +use iced::event::{self, listen_raw, Event}; +use iced::theme::{self, Theme}; +use iced::wayland::actions::window::SctkWindowSettings; +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::layout::Limits; +use iced_core::{id, keyboard}; + +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; + +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_code: keyboard::KeyCode::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/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/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/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 31b6f19166..c01c201673 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -184,7 +184,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; @@ -354,7 +356,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 @@ -375,8 +377,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::>(), ); } @@ -386,7 +388,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( @@ -631,7 +633,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 3c62bfbc8f..5926a39a7f 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", "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 4dac032c83..558721f452 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -9,12 +9,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"))] @@ -249,8 +250,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); @@ -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/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/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index 920189f541..c5b843dd5d 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -13,6 +13,7 @@ use once_cell::sync::Lazy; pub fn main() -> iced::Result { WebSocket::run(Settings::default()) } +use iced::id::Id; #[derive(Default)] struct WebSocket { @@ -168,4 +169,4 @@ impl Default for State { } } -static MESSAGE_LOG: Lazy = Lazy::new(scrollable::Id::unique); +static MESSAGE_LOG: Lazy = Lazy::new(|| Id::new("message_log")); 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/graphics/src/compositor.rs b/graphics/src/compositor.rs index b8b575b41c..696d9fc0a4 100644 --- a/graphics/src/compositor.rs +++ b/graphics/src/compositor.rs @@ -96,6 +96,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/graphics/src/damage.rs b/graphics/src/damage.rs index 595cc2746d..59e9f5b4c0 100644 --- a/graphics/src/damage.rs +++ b/graphics/src/damage.rs @@ -73,6 +73,11 @@ impl Damage for Primitive { bounds.expand(1.5) } + Self::RawText(raw) => { + // TODO: Add `size` field to `raw` to compute more accurate + // damage bounds (?) + raw.clip_bounds.expand(1.5) + } Self::Quad { bounds, .. } | Self::Image { bounds, .. } | Self::Svg { bounds, .. } => bounds.expand(1.0), diff --git a/graphics/src/geometry/text.rs b/graphics/src/geometry/text.rs index 0bf7ec9794..a1e4debe3e 100644 --- a/graphics/src/geometry/text.rs +++ b/graphics/src/geometry/text.rs @@ -38,12 +38,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/graphics/src/primitive.rs b/graphics/src/primitive.rs index ed75776c15..c7efedce48 100644 --- a/graphics/src/primitive.rs +++ b/graphics/src/primitive.rs @@ -57,6 +57,8 @@ pub enum Primitive { /// The clip bounds of the editor. clip_bounds: Rectangle, }, + /// A raw `cosmic-text` primitive + RawText(crate::text::Raw), /// A quad primitive Quad { /// The bounds of the quad @@ -78,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 1b0f5c5b3a..4b8968d95a 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -142,6 +142,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}'; @@ -189,6 +190,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>, @@ -226,11 +231,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/graphics/src/text.rs b/graphics/src/text.rs index fc7694c248..7c4b5e31fe 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -9,14 +9,13 @@ pub use paragraph::Paragraph; pub use cosmic_text; -use crate::color; use crate::core::font::{self, Font}; use crate::core::text::Shaping; -use crate::core::{Color, Size}; +use crate::core::{Color, Point, Rectangle, Size}; use once_cell::sync::OnceCell; use std::borrow::Cow; -use std::sync::{Arc, RwLock}; +use std::sync::{Arc, RwLock, Weak}; /// Returns the global [`FontSystem`]. pub fn font_system() -> &'static RwLock { @@ -68,6 +67,29 @@ impl FontSystem { #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct Version(u32); +/// A weak reference to a [`cosmic-text::Buffer`] that can be drawn. +#[derive(Debug, Clone)] +pub struct Raw { + /// A weak reference to a [`cosmic_text::Buffer`]. + pub buffer: Weak, + /// The position of the text. + pub position: Point, + /// The color of the text. + pub color: Color, + /// The clip bounds of the text. + pub clip_bounds: Rectangle, +} + +impl PartialEq for Raw { + fn eq(&self, _other: &Self) -> bool { + // TODO: There is no proper way to compare raw buffers + // For now, no two instances of `Raw` text will be equal. + // This should be fine, but could trigger unnecessary redraws + // in the future. + false + } +} + /// Measures the dimensions of the given [`cosmic_text::Buffer`]. pub fn measure(buffer: &cosmic_text::Buffer) -> Size { let (width, total_lines) = buffer @@ -150,12 +172,7 @@ pub fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping { /// Converts some [`Color`] to a [`cosmic_text::Color`]. pub fn to_color(color: Color) -> cosmic_text::Color { - let [r, g, b, a] = color::pack(color).components(); + let [r, g, b, a] = color.into_rgba8(); - cosmic_text::Color::rgba( - (r * 255.0) as u8, - (g * 255.0) as u8, - (b * 255.0) as u8, - (a * 255.0) as u8, - ) + cosmic_text::Color::rgba(r, g, b, a) } diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index d5262ae820..e6f12d9300 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -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, @@ -32,7 +32,11 @@ impl Editor { /// Returns the buffer of the [`Editor`]. 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`]. @@ -101,133 +105,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 +255,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 +285,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 +309,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 +354,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 +427,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 +458,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 +505,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 +518,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 +538,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 +569,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 +584,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 = internal.editor.with_buffer(|buffer| { + let scroll = buffer.scroll(); + let mut window = scroll.layout + 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)); + 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 +628,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 +675,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 +781,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 +795,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/renderer/Cargo.toml b/renderer/Cargo.toml index 56e17209c6..6bd8f9c46c 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/renderer/src/compositor.rs b/renderer/src/compositor.rs index 9d0ff9ab3c..e08a26e471 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/lib.rs b/renderer/src/lib.rs index f2acfa00f5..26e75f6db8 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; @@ -152,6 +151,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; @@ -198,6 +198,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>, @@ -229,8 +233,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) + ); } } @@ -262,8 +271,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/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/runtime/Cargo.toml b/runtime/Cargo.toml index 8089d545a1..3a06eaa594 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -13,10 +13,15 @@ keywords.workspace = true [features] 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 f70da915fb..4a9e0551e0 100644 --- a/runtime/src/command.rs +++ b/runtime/src/command.rs @@ -1,5 +1,7 @@ //! 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/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..1bb74473d1 --- /dev/null +++ b/runtime/src/command/platform_specific/mod.rs @@ -0,0 +1,46 @@ +//! Platform specific actions defined for wayland + +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 { + /// 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!(), + #[cfg(feature = "wayland")] + Action::Wayland(action) => Action::Wayland(action.map(_f)), + } + } +} + +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..aff488ddbf --- /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, + }, + 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..261de781b1 --- /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, + }, + 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..81f24c30fb --- /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, + }, + 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/multi_window/state.rs b/runtime/src/multi_window/state.rs index 49f72c3971..aa0af37001 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 4256efb705..a0f22a8d1b 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; @@ -142,13 +144,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, 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 6f8f4063e2..0db55d086f 100644 --- a/runtime/src/program/state.rs +++ b/runtime/src/program/state.rs @@ -1,10 +1,13 @@ +use iced_core::widget::operation::{OperationWrapper, Outcome}; +use iced_core::widget::OperationOutputWrapper; + use crate::core::event::{self, Event}; use crate::core::mouse; 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. @@ -27,12 +30,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, @@ -88,6 +93,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, @@ -95,8 +101,9 @@ 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, self.cache.take().unwrap(), renderer, @@ -129,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); @@ -137,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); @@ -152,9 +159,15 @@ where debug.update_finished(); command - })); + })) + .actions() + .into_iter() + .partition::, _>(|action| { + !matches!(action, Action::Widget(_)) + }); let mut user_interface = build_user_interface( + id, &mut self.program, temp_cache, renderer, @@ -162,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); @@ -169,21 +223,25 @@ where self.cache = Some(user_interface.into_cache()); - Some(commands) + actions }; - (uncaptured_events, command) + (uncaptured_events, actions) } /// Applies [`Operation`]s to the [`State`] pub fn operate( &mut self, + id: crate::window::Id, renderer: &mut P::Renderer, - operations: impl Iterator>>, + operations: impl Iterator< + Item = Box>>, + >, bounds: Size, debug: &mut Debug, ) { let mut user_interface = build_user_interface( + id, &mut self.program, self.cache.take().unwrap(), renderer, @@ -199,12 +257,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); } + _ => {} }; } } @@ -214,6 +275,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/user_interface.rs b/runtime/src/user_interface.rs index 3594ac188c..901805cd2b 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/runtime/src/window.rs b/runtime/src/window.rs index f9d943f6aa..9b0ed80f08 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -3,14 +3,14 @@ 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; @@ -23,9 +23,32 @@ 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(id, Event::RedrawRequested(at)) => { + Some((id, 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 { - crate::core::Event::Window(_, Event::RedrawRequested(at)) => Some(at), + 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/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/sctk/Cargo.toml b/sctk/Cargo.toml new file mode 100644 index 0000000000..fd156f6a5d --- /dev/null +++ b/sctk/Cargo.toml @@ -0,0 +1,53 @@ +[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 = [] +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.5" +enum-repr = "0.2" +futures = "0.3" +wayland-backend = {version = "0.3.1", features = ["client_system"]} +float-cmp = "0.9" +smithay-clipboard = "0.7.0" +accesskit = { version = "0.8.1", optional = true } +accesskit_unix = { version = "0.1.1", optional = true } +itertools = "0.10" +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..cd6034480a --- /dev/null +++ b/sctk/src/application.rs @@ -0,0 +1,2189 @@ +#[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, 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::{ + collections::HashMap, hash::Hash, marker::PhantomData, time::Duration, +}; +use wayland_backend::client::ObjectId; +use wayland_protocols::wp::viewporter::client::wp_viewport::WpViewport; + +use iced_graphics::{compositor, 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::{ + HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle, + WaylandDisplayHandle, WaylandWindowHandle, +}; +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; + +pub struct SurfaceDisplayWrapper { + comp_surface: Option<::Surface>, + backend: wayland_backend::client::Backend, + wl_surface: WlSurface, +} + +unsafe impl HasRawDisplayHandle for SurfaceDisplayWrapper { + fn raw_display_handle(&self) -> RawDisplayHandle { + let mut display_handle = WaylandDisplayHandle::empty(); + display_handle.display = self.backend.display_ptr() as *mut _; + RawDisplayHandle::Wayland(display_handle) + } +} + +unsafe impl HasRawWindowHandle for SurfaceDisplayWrapper { + fn raw_window_handle(&self) -> RawWindowHandle { + let mut window_handle = WaylandWindowHandle::empty(); + window_handle.surface = self.wl_surface.id().as_ptr() as *mut _; + 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 + ::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) -> ::Theme; + + /// Returns the `Style` variation of the `Theme`. + fn style( + &self, + ) -> <::Theme as StyleSheet>::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:: { + comp_surface: None, + backend: backend.clone(), + wl_surface, + }; + + #[allow(unsafe_code)] + let compositor = C::new(compositor_settings, Some(&wrapper)).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 compositor_surfaces = HashMap::new(); + let mut instance = Box::pin(run_instance::( + application, + compositor, + renderer, + runtime, + ev_proxy, + debug, + receiver, + control_sender, + compositor_surfaces, + 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 compositor_surfaces: HashMap>, + 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, + &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 } => 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))); + } + crate::sctk_event::WindowEventVariant::Close => { + if let Some(surface_id) = surface_ids.remove(&id.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(id.id(), surface_id); + compositor_surfaces.remove(&surface_id.inner()); + if exit_on_close_request && compositor_surfaces.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(&id.id()) { + compositor_surfaces.entry(id.inner()).or_insert_with(|| { + let mut wrapper = SurfaceDisplayWrapper { + comp_surface: None, + backend: backend.clone(), + wl_surface + }; + if matches!(simple_clipboard.state, crate::clipboard::State::Unavailable) { + if let RawDisplayHandle::Wayland(handle) = wrapper.raw_display_handle() { + assert!(!handle.display.is_null()); + simple_clipboard = unsafe { Clipboard::connect(handle.display) }; + } + } + let c_surface = compositor.create_surface(&wrapper, configure.new_size.0.unwrap().get(), configure.new_size.1.unwrap().get()); + wrapper.comp_surface.replace(c_surface); + wrapper + }); + if first { + let Some(state) = states.get(&id.inner()) else { + continue; + }; + 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; + } + } + if let Some(state) = states.get_mut(&id.inner()) { + 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(&id.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 } => 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))); + + } + LayerSurfaceEventVariant::Done => { + if let Some(surface_id) = surface_ids.remove(&id.id()) { + if kbd_surface_id == Some(id.id()) { + kbd_surface_id = None; + } + 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(id.id(), surface_id); + compositor_surfaces.remove(&surface_id.inner()); + if exit_on_close_request && compositor_surfaces.is_empty() { + break 'main; + } + } + } + LayerSurfaceEventVariant::Configure(configure, wl_surface, first) => { + if let Some(id) = surface_ids.get(&id.id()) { + compositor_surfaces.entry(id.inner()).or_insert_with(|| { + let mut wrapper = SurfaceDisplayWrapper { + comp_surface: None, + backend: backend.clone(), + wl_surface + }; + if matches!(simple_clipboard.state, crate::clipboard::State::Unavailable) { + if let RawDisplayHandle::Wayland(handle) = wrapper.raw_display_handle() { + assert!(!handle.display.is_null()); + simple_clipboard = unsafe { Clipboard::connect(handle.display) }; + } + } + let mut c_surface = compositor.create_surface(&wrapper, configure.new_size.0, configure.new_size.1); + compositor.configure_surface(&mut c_surface, configure.new_size.0, configure.new_size.1); + wrapper.comp_surface.replace(c_surface); + wrapper + }); + if first { + let Some(state) = states.get(&id.inner()) else { + continue; + }; + 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(&id.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, + } => 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))); + + } + PopupEventVariant::Done => { + if let Some(surface_id) = surface_ids.remove(&id.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(id.id(), surface_id); + compositor_surfaces.remove(&surface_id.inner()); + } + } + PopupEventVariant::Configure(configure, wl_surface, first) => { + if let Some(id) = surface_ids.get(&id.id()) { + compositor_surfaces.entry(id.inner()).or_insert_with(|| { + let mut wrapper = SurfaceDisplayWrapper { + comp_surface: None, + backend: backend.clone(), + wl_surface + }; + let c_surface = compositor.create_surface(&wrapper, configure.width as u32, configure.height as u32); + wrapper.comp_surface.replace(c_surface); + wrapper + }); + if first { + let Some(state) = states.get(&id.inner()) else { + continue; + }; + 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; + } + } + if let Some(state) = states.get_mut(&id.inner()) { + 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(&id.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(&id.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) => { + drop(compositor_surfaces.remove(inner)); + interfaces.remove(inner); + states.remove(inner); + destroyed_surface_ids.insert(id.clone(), *surface_id); + compositor_surfaces.remove(&surface_id.inner()); + 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))); + } + SctkEvent::SessionLockSurfaceConfigure { surface, configure, first } => { + if let Some(id) = surface_ids.get(&surface.id()) { + compositor_surfaces.entry(id.inner()).or_insert_with(|| { + let mut wrapper = SurfaceDisplayWrapper { + comp_surface: None, + backend: backend.clone(), + wl_surface: surface.clone() + }; + let c_surface = compositor.create_surface(&wrapper, configure.new_size.0, configure.new_size.1); + wrapper.comp_surface.replace(c_surface); + wrapper + }); + + let Some(state) = states.get_mut(&id.inner()) else { + continue; + }; + + 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 mut wrapper = SurfaceDisplayWrapper { + comp_surface: None, + backend: backend.clone(), + wl_surface, + }; + let mut c_surface = compositor.create_surface(&wrapper, w, h); + compositor.configure_surface(&mut c_surface, w, h); + let mut state = + State::new(&application, SurfaceIdWrapper::Dnd(native_id)); + 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 { + icon_color: state.icon_color(), + text_color: state.text_color(), + scale_factor: state.scale_factor(), + }, + state.cursor(), + ); + + let _ = compositor.present( + &mut renderer, + &mut c_surface, + state.viewport(), + Color::TRANSPARENT, + &debug.overlay(), + ); + wrapper.comp_surface.replace(c_surface); + surface_ids.insert(id, SurfaceIdWrapper::Dnd(native_id)); + compositor_surfaces + .entry(native_id) + .or_insert_with(move || wrapper); + 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 + | 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, + ) { + 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() + || 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 += 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(wrapper), + Some(mut user_interface), + Some(state), + )) = surface_ids.get(&object_id).and_then(|id| { + if matches!(id, SurfaceIdWrapper::Dnd(_)) { + return None; + } + let surface = compositor_surfaces.get_mut(&id.inner()); + let interface = interfaces.remove(&id.inner()); + let state = states.get_mut(&id.inner()); + Some((*id, surface, 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(); + } + + 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, + }); + } + let comp_surface = match wrapper.comp_surface.as_mut() { + Some(s) => s, + None => continue, + }; + + if state.viewport_changed() { + let physical_size = state.physical_size(); + let logical_size = state.logical_size(); + compositor.configure_surface( + 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 { + icon_color: state.icon_color(), + text_color: state.text_color(), + scale_factor: state.scale_factor(), + }, + state.cursor(), + ); + + debug.draw_finished(); + 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, + comp_surface, + state.viewport(), + state.background_color(), + &debug.overlay(), + ); + + 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( + operation::focusable::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::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.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, +} + +impl State +where + ::Theme: application::StyleSheet, +{ + /// Creates a new [`State`] for the provided [`Application`] + pub fn new(application: &A, id: SurfaceIdWrapper) -> 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, + } + } + + 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 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 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 + } + + 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, +{ + 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, +{ + match action { + command::Action::Future(future) => { + runtime + .spawn(Box::pin(future.map(|e| { + 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(_)) { + 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, // 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>( + 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, ::Renderer>, +> +where + A: Application + 'static, + ::Theme: StyleSheet, +{ + 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..9558821a3a --- /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, + }, + )), + )) +} + +/// +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..9f3bb0dd52 --- /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, + }, + )), + )) +} + +// 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..d93daa073f --- /dev/null +++ b/sctk/src/conversion.rs @@ -0,0 +1,156 @@ +use std::collections::HashMap; + +use iced_futures::core::mouse::Interaction; +use iced_runtime::core::{ + keyboard::{self, KeyCode}, + mouse::{self, ScrollDelta}, +}; +use sctk::{ + reexports::client::protocol::wl_pointer::AxisSource, + seat::{ + keyboard::Modifiers, + pointer::{ + AxisScroll, CursorIcon, BTN_EXTRA, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT, + BTN_SIDE, + }, + }, +}; +use xkeysym::{key, RawKeysym}; + +lazy_static::lazy_static! { + pub static ref KEY_CONVERSION: HashMap = [ + (key::_1, KeyCode::Key1), (key::_2, KeyCode::Key2), (key::_3, KeyCode::Key3), (key::_4, KeyCode::Key4), (key::_5, KeyCode::Key5), (key::_6, KeyCode::Key6), (key::_7, KeyCode::Key7), (key::_8, KeyCode::Key8), (key::_9, KeyCode::Key9), (key::_0, KeyCode::Key0), + // Letters. + (key::A, KeyCode::A), (key::a, KeyCode::A), (key::B, KeyCode::B), (key::b, KeyCode::B), (key::C, KeyCode::C), (key::c, KeyCode::C), (key::D, KeyCode::D), (key::d, KeyCode::D), (key::E, KeyCode::E), (key::e, KeyCode::E), (key::F, KeyCode::F), (key::f, KeyCode::F), (key::G, KeyCode::G), (key::g, KeyCode::G), (key::H, KeyCode::H), (key::h, KeyCode::H), (key::I, KeyCode::I), (key::i, KeyCode::I), (key::J, KeyCode::J), (key::j, KeyCode::J), (key::K, KeyCode::K), (key::k, KeyCode::K), (key::L, KeyCode::L), (key::l, KeyCode::L), (key::M, KeyCode::M), (key::m, KeyCode::M), (key::N, KeyCode::N), (key::n, KeyCode::N), (key::O, KeyCode::O), (key::o, KeyCode::O), (key::P, KeyCode::P), (key::p, KeyCode::P), (key::Q, KeyCode::Q), (key::q, KeyCode::Q), (key::R, KeyCode::R), (key::r, KeyCode::R), (key::S, KeyCode::S), (key::s, KeyCode::S), (key::T, KeyCode::T), (key::t, KeyCode::T), (key::U, KeyCode::U), (key::u, KeyCode::U), (key::V, KeyCode::V), (key::v, KeyCode::V), (key::W, KeyCode::W), (key::w, KeyCode::W), (key::X, KeyCode::X), (key::x, KeyCode::X), (key::Y, KeyCode::Y), (key::y, KeyCode::Y), (key::Z, KeyCode::Z), (key::z, KeyCode::Z), + // Escape. + (key::Escape, KeyCode::Escape), + // Function keys. + (key::F1, KeyCode::F1), (key::F2, KeyCode::F2), (key::F3, KeyCode::F3), (key::F4, KeyCode::F4), (key::F5, KeyCode::F5), (key::F6, KeyCode::F6), (key::F7, KeyCode::F7), (key::F8, KeyCode::F8), (key::F9, KeyCode::F9), (key::F10, KeyCode::F10), (key::F11, KeyCode::F11), (key::F12, KeyCode::F12), (key::F13, KeyCode::F13), (key::F14, KeyCode::F14), (key::F15, KeyCode::F15), (key::F16, KeyCode::F16), (key::F17, KeyCode::F17), (key::F18, KeyCode::F18), (key::F19, KeyCode::F19), (key::F20, KeyCode::F20), (key::F21, KeyCode::F21), (key::F22, KeyCode::F22), (key::F23, KeyCode::F23), (key::F24, KeyCode::F24), + // Flow control. + (key::Print, KeyCode::Snapshot), (key::Scroll_Lock, KeyCode::Scroll), (key::Pause, KeyCode::Pause), (key::Insert, KeyCode::Insert), (key::Home, KeyCode::Home), (key::Delete, KeyCode::Delete), (key::End, KeyCode::End), (key::Page_Down, KeyCode::PageDown), (key::Page_Up, KeyCode::PageUp), + // Arrows. + (key::Left, KeyCode::Left), (key::Up, KeyCode::Up), (key::Right, KeyCode::Right), (key::Down, KeyCode::Down), (key::BackSpace, KeyCode::Backspace), (key::Return, KeyCode::Enter), (key::space, KeyCode::Space), (key::Multi_key, KeyCode::Compose), (key::caret, KeyCode::Caret), + // Keypad. + (key::Num_Lock, KeyCode::Numlock), (key::KP_0, KeyCode::Numpad0), (key::KP_1, KeyCode::Numpad1), (key::KP_2, KeyCode::Numpad2), (key::KP_3, KeyCode::Numpad3), (key::KP_4, KeyCode::Numpad4), (key::KP_5, KeyCode::Numpad5), (key::KP_6, KeyCode::Numpad6), (key::KP_7, KeyCode::Numpad7), (key::KP_8, KeyCode::Numpad8), (key::KP_9, KeyCode::Numpad9), + // Misc. + // => Some(KeyCode::AbntC1), + // => Some(KeyCode::AbntC2), + (key::plus, KeyCode::Plus), (key::apostrophe, KeyCode::Apostrophe), + // => Some(KeyCode::Apps), + (key::at, KeyCode::At), + // => Some(KeyCode::Ax), + (key::backslash, KeyCode::Backslash), (key::XF86_Calculator, KeyCode::Calculator), + // => Some(KeyCode::Capital), + (key::colon, KeyCode::Colon), (key::comma, KeyCode::Comma), + // => Some(KeyCode::Convert), + (key::equal, KeyCode::Equals), (key::grave, KeyCode::Grave), + // => Some(KeyCode::Kana), + (key::Kanji, KeyCode::Kanji), (key::Alt_L, KeyCode::LAlt), (key::bracketleft, KeyCode::LBracket), (key::Control_L, KeyCode::LControl), (key::Shift_L, KeyCode::LShift), (key::Super_L, KeyCode::LWin), (key::XF86_Mail, KeyCode::Mail), + // => Some(KeyCode::MediaSelect), + // => Some(KeyCode::MediaStop), + (key::minus, KeyCode::Minus), (key::asterisk, KeyCode::Asterisk), (key::XF86_AudioMute, KeyCode::Mute), + // => Some(KeyCode::MyComputer), + (key::XF86_AudioNext, KeyCode::NextTrack), + // => Some(KeyCode::NoConvert), + (key::KP_Separator, KeyCode::NumpadComma), (key::KP_Enter, KeyCode::NumpadEnter), (key::KP_Equal, KeyCode::NumpadEquals), (key::KP_Add, KeyCode::NumpadAdd), (key::KP_Subtract, KeyCode::NumpadSubtract), (key::KP_Multiply, KeyCode::NumpadMultiply), (key::KP_Divide, KeyCode::NumpadDivide), (key::KP_Decimal, KeyCode::NumpadDecimal), (key::KP_Page_Up, KeyCode::PageUp), (key::KP_Page_Down, KeyCode::PageDown), (key::KP_Home, KeyCode::Home), (key::KP_End, KeyCode::End), (key::KP_Left, KeyCode::Left), (key::KP_Up, KeyCode::Up), (key::KP_Right, KeyCode::Right), (key::KP_Down, KeyCode::Down), + // => Some(KeyCode::OEM102), + (key::period, KeyCode::Period), + // => Some(KeyCode::Playpause), + (key::XF86_PowerOff, KeyCode::Power), (key::XF86_AudioPrev, KeyCode::PrevTrack), (key::Alt_R, KeyCode::RAlt), (key::bracketright, KeyCode::RBracket), (key::Control_R, KeyCode::RControl), (key::Shift_R, KeyCode::RShift), (key::Super_R, KeyCode::RWin), (key::semicolon, KeyCode::Semicolon), (key::slash, KeyCode::Slash), (key::XF86_Sleep, KeyCode::Sleep), + // => Some(KeyCode::Stop), + // => Some(KeyCode::Sysrq), + (key::Tab, KeyCode::Tab), (key::ISO_Left_Tab, KeyCode::Tab), (key::underscore, KeyCode::Underline), + // => Some(KeyCode::Unlabeled), + (key::XF86_AudioLowerVolume, KeyCode::VolumeDown), (key::XF86_AudioRaiseVolume, KeyCode::VolumeUp), + // => Some(KeyCode::Wake), + // => Some(KeyCode::Webback), + // => Some(KeyCode::WebFavorites), + // => Some(KeyCode::WebForward), + // => Some(KeyCode::WebHome), + // => Some(KeyCode::WebRefresh), + // => Some(KeyCode::WebSearch), + // => Some(KeyCode::WebStop), + (key::yen, KeyCode::Yen), (key::XF86_Copy, KeyCode::Copy), (key::XF86_Paste, KeyCode::Paste), (key::XF86_Cut, KeyCode::Cut) + ].iter().copied().collect(); +} + +/// 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 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) + } +} + +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..8e2acd1dd3 --- /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::{ + 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..e2b0e017c0 --- /dev/null +++ b/sctk/src/event_loop/state.rs @@ -0,0 +1,844 @@ +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(), 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() { + // 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 { + 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..797861258a --- /dev/null +++ b/sctk/src/handlers/compositor.rs @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MPL-2.0-only +use sctk::{ + compositor::CompositorHandler, + delegate_compositor, + reexports::client::{protocol::wl_surface, Connection, QueueHandle}, +}; +use std::fmt::Debug; + +use crate::event_loop::state::SctkState; + +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..5eb4b36747 --- /dev/null +++ b/sctk/src/handlers/data_device/data_device.rs @@ -0,0 +1,137 @@ +use sctk::{ + data_device_manager::{ + data_device::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..2901c07038 --- /dev/null +++ b/sctk/src/handlers/data_device/data_offer.rs @@ -0,0 +1,56 @@ +use sctk::{ + data_device_manager::data_offer::{DataOfferHandler, DragOffer}, + reexports::client::{ + protocol::wl_data_device_manager::DndAction, Connection, QueueHandle, + }, +}; + +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..bf0d6fb207 --- /dev/null +++ b/sctk/src/handlers/shell/xdg_window.rs @@ -0,0 +1,112 @@ +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()), + ); + } + 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(); + 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/lib.rs b/sctk/src/lib.rs new file mode 100644 index 0000000000..b90a64c788 --- /dev/null +++ b/sctk/src/lib.rs @@ -0,0 +1,24 @@ +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 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..be9092dcaf --- /dev/null +++ b/sctk/src/sctk_event.rs @@ -0,0 +1,976 @@ +use crate::{ + application::SurfaceIdWrapper, + conversion::{ + keysym_to_vkey, modifiers_to_native, pointer_axis_to_native, + pointer_button_to_native, + }, + dpi::PhysicalSize, +}; + +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::KeyCode, + 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 mut skip_char = false; + + let mut events: Vec<_> = keysym_to_vkey(ke.keysym.raw()) + .map(|k| { + if k == KeyCode::Backspace { + skip_char = true; + } + iced_runtime::core::Event::Keyboard( + keyboard::Event::KeyPressed { + key_code: k, + modifiers: modifiers_to_native(*modifiers), + }, + ) + }) + .into_iter() + .collect(); + if !skip_char { + if let Some(s) = ke.utf8 { + let mut chars = s + .chars() + .map(|c| { + iced_runtime::core::Event::Keyboard( + keyboard::Event::CharacterReceived(c), + ) + }) + .collect(); + events.append(&mut chars); + } + } + events + } + KeyboardEventVariant::Repeat(ke) => { + let mut skip_char = false; + + let mut events: Vec<_> = keysym_to_vkey(ke.keysym.raw()) + .map(|k| { + if k == KeyCode::Backspace { + skip_char = true; + } + iced_runtime::core::Event::Keyboard( + keyboard::Event::KeyPressed { + key_code: k, + modifiers: modifiers_to_native(*modifiers), + }, + ) + }) + .into_iter() + .collect(); + if !skip_char { + if let Some(s) = ke.utf8 { + let mut chars = s + .chars() + .map(|c| { + iced_runtime::core::Event::Keyboard( + keyboard::Event::CharacterReceived(c), + ) + }) + .collect(); + events.append(&mut chars); + } + } + events + } + KeyboardEventVariant::Release(k) => { + keysym_to_vkey(k.keysym.raw()) + .map(|k| { + iced_runtime::core::Event::Keyboard( + keyboard::Event::KeyReleased { + key_code: k, + 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() + } + } + } +} 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..fd6fa19715 --- /dev/null +++ b/sctk/src/util.rs @@ -0,0 +1,119 @@ +/// 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))] +#[derive(Default)] +pub enum CursorIcon { + /// The platform-dependent default cursor. + #[default] + 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, +} + +#[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, Default)] +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. + #[default] + 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 9518b8c59d..005c13c541 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,4 +1,5 @@ //! Build interactive cross-platform applications. + use crate::{Command, Element, Executor, Settings, Subscription}; pub use crate::style::application::{Appearance, StyleSheet}; @@ -23,8 +24,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 @@ -33,8 +32,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]. @@ -49,7 +46,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/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 002d2a79c6..93f26b6625 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/ @@ -151,38 +151,64 @@ #![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))] + +#[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; @@ -190,8 +216,9 @@ pub use style::theme; pub use crate::core::alignment; pub use crate::core::gradient; pub use crate::core::{ - color, Alignment, Background, BorderRadius, Color, ContentFit, Degrees, - Gradient, Length, Padding, Pixels, Point, Radians, Rectangle, Size, Vector, + color, id, layout::Limits, Alignment, Background, BorderRadius, Color, + ContentFit, Degrees, Gradient, Length, Padding, Pixels, Point, Radians, + Rectangle, Size, Vector, }; pub mod clipboard { @@ -224,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}; } @@ -293,12 +322,14 @@ pub mod widget { mod runtime {} } +#[cfg(feature = "winit")] pub use application::Application; pub use command::Command; pub use error::Error; pub use event::Event; pub use executor::Executor; -pub use font::Font; +pub use iced_core::Font; +#[cfg(any(feature = "winit", feature = "wayland"))] pub use sandbox::Sandbox; pub use settings::Settings; pub use subscription::Subscription; @@ -313,7 +344,5 @@ pub type Renderer = renderer::Renderer; pub type Element<'a, Message, Renderer = crate::Renderer> = crate::core::Element<'a, Message, 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 d9476b614e..c45922f5f4 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,11 +20,14 @@ pub struct Settings { /// The window settings. /// /// They will be ignored on the Web. + #[cfg(feature = "winit")] pub window: window::Settings, - /// The data needed to initialize the [`Application`]. - /// - /// [`Application`]: crate::Application + /// The window settings. + #[cfg(feature = "wayland")] + pub initial_surface: InitialSurface, + + /// The data needed to initialize the Application. pub flags: Flags, /// The fonts to load on boot. @@ -39,22 +46,57 @@ 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. + pub exit_on_close_request: bool, } +#[cfg(not(any(feature = "winit", feature = "wayland")))] +impl Settings { + /// Initialize Application settings using the given data. + pub fn with_flags(flags: Flags) -> Self { + let default_settings = Settings::<()>::default(); + 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, + 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: iced_core::Pixels(14.0), + fonts: Vec::new(), + 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 +105,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, @@ -78,12 +122,14 @@ 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, } } } +#[cfg(feature = "winit")] impl From> for iced_winit::Settings { fn from(settings: Settings) -> iced_winit::Settings { iced_winit::Settings { @@ -94,3 +140,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(14.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..72133b9184 --- /dev/null +++ b/src/wayland/mod.rs @@ -0,0 +1,187 @@ +use crate::runtime::window::Id; +use crate::{Command, Element, Executor, Settings as 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`]. +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; + + /// 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. + fn style(&self) -> ::Style { + ::Style::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, 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 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::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, window: Id) -> A::Theme { + self.0.theme(window) + } + + 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..bffafb7ae7 --- /dev/null +++ b/src/wayland/sandbox.rs @@ -0,0 +1,201 @@ +use iced_core::window::Id; + +use crate::theme::{self, Theme}; +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. +/// - [`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 +/// [`tour`]: https://github.com/iced-rs/iced/tree/0.4/examples/tour +/// [the overview]: index.html#overview +/// [`iced_wgpu`]: https://github.com/iced-rs/iced/tree/0.4/wgpu +/// [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, window::Id}; +/// +/// 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, _: Id) -> 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>; + + /// 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/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 e49ad94ac1..60dc3299fe 100644 --- a/style/src/button.rs +++ b/style/src/button.rs @@ -14,6 +14,8 @@ pub struct Appearance { pub border_width: f32, /// The border [`Color`] of the button. pub border_color: Color, + /// The icon [`Color`] of the button. + pub icon_color: Option, /// The text [`Color`] of the button. pub text_color: Color, } @@ -26,6 +28,7 @@ impl std::default::Default for Appearance { border_radius: 0.0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, + icon_color: None, text_color: Color::BLACK, } } diff --git a/style/src/container.rs b/style/src/container.rs index ec543ae4db..e31d9dcc7d 100644 --- a/style/src/container.rs +++ b/style/src/container.rs @@ -4,6 +4,8 @@ use iced_core::{Background, BorderRadius, Color}; /// The appearance of a container. #[derive(Debug, Clone, Copy)] 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. @@ -19,6 +21,7 @@ pub struct Appearance { impl std::default::Default for Appearance { fn default() -> Self { Self { + icon_color: None, text_color: None, background: None, border_radius: 0.0.into(), diff --git a/style/src/slider.rs b/style/src/slider.rs index f006855883..dc28fbfd6c 100644 --- a/style/src/slider.rs +++ b/style/src/slider.rs @@ -1,5 +1,5 @@ //! Change the apperance of a slider. -use iced_core::{BorderRadius, Color}; +use iced_core::{gradient::Linear, BorderRadius, Color}; /// The appearance of a slider. #[derive(Debug, Clone, Copy)] @@ -14,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: BorderRadius, } +/// 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 470107283e..53c54f7cc4 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; @@ -16,6 +16,7 @@ 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; @@ -116,6 +117,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), @@ -399,6 +401,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_radius: 2.0.into(), @@ -449,7 +452,7 @@ impl slider::StyleSheet for Theme { slider::Appearance { rail: slider::Rail { - colors: ( + colors: RailBackground::Pair( palette.primary.base.color, palette.secondary.base.color, ), diff --git a/style/src/theme/palette.rs b/style/src/theme/palette.rs index aaeb799df4..6c98d98ad5 100644 --- a/style/src/theme/palette.rs +++ b/style/src/theme/palette.rs @@ -117,12 +117,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`]. @@ -134,9 +137,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/tiny_skia/fonts/Iced-Icons.ttf b/tiny_skia/fonts/Iced-Icons.ttf new file mode 100644 index 0000000000..e3273141c4 Binary files /dev/null and b/tiny_skia/fonts/Iced-Icons.ttf differ diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 3e9bd2a5e2..df7bfc1c8b 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -1,5 +1,6 @@ use crate::core::{Background, Color, Gradient, Rectangle, Vector}; use crate::graphics::backend; +use crate::graphics::text; use crate::graphics::Viewport; use crate::primitive::{self, Primitive}; @@ -159,8 +160,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, @@ -173,13 +175,21 @@ impl Backend { .min(bounds.width / 2.0) .min(bounds.height / 2.0); + // 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 let mut fill_border_radius = *border_radius; 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); pixels.fill_path( &path, @@ -370,8 +380,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, @@ -395,8 +406,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, @@ -426,8 +438,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, @@ -444,11 +457,41 @@ impl Backend { clip_mask, ); } + Primitive::RawText(text::Raw { + buffer, + position, + color, + clip_bounds: text_clip_bounds, + }) => { + let Some(buffer) = buffer.upgrade() else { + return; + }; + + let physical_bounds = + (*text_clip_bounds + translation) * scale_factor; + + if !clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + .then_some(clip_mask as &_); + + self.text_pipeline.draw_raw( + &buffer, + *position + translation, + *color, + scale_factor, + pixels, + clip_mask, + ); + } #[cfg(feature = "image")] Primitive::Image { handle, filter_method, bounds, + border_radius, } => { let physical_bounds = (*bounds + translation) * scale_factor; @@ -456,8 +499,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, @@ -472,6 +516,7 @@ impl Backend { pixels, transform, clip_mask, + *border_radius, ); } #[cfg(not(feature = "image"))] @@ -492,8 +537,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, @@ -529,8 +575,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, @@ -562,8 +609,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, 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/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/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index 70e95d0170..9413e31170 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -1,7 +1,6 @@ use crate::core::alignment; use crate::core::text::{LineHeight, Shaping}; -use crate::core::{Color, Font, Pixels, Point, Rectangle}; -use crate::graphics::color; +use crate::core::{Color, Font, Pixels, Point, Rectangle, Size}; use crate::graphics::text::cache::{self, Cache}; use crate::graphics::text::editor; use crate::graphics::text::font_system; @@ -149,6 +148,33 @@ impl Pipeline { ); } + pub fn draw_raw( + &mut self, + buffer: &cosmic_text::Buffer, + position: Point, + color: Color, + scale_factor: f32, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: Option<&tiny_skia::Mask>, + ) { + let mut font_system = font_system().write().expect("Write font system"); + + let (width, height) = buffer.size(); + + draw( + font_system.raw(), + &mut self.glyph_cache, + buffer, + Rectangle::new(position, Size::new(width, height)), + color, + alignment::Horizontal::Left, + alignment::Vertical::Top, + scale_factor, + pixels, + clip_mask, + ); + } + pub fn trim_cache(&mut self) { self.cache.get_mut().trim(); self.glyph_cache.trim(); @@ -217,18 +243,7 @@ fn draw( fn from_color(color: cosmic_text::Color) -> Color { let [r, g, b, a] = color.as_rgba(); - if color::GAMMA_CORRECTION { - // `cosmic_text::Color` is linear RGB in this case, so we - // need to convert back to sRGB - Color::from_linear_rgba( - r as f32 / 255.0, - g as f32 / 255.0, - b as f32 / 255.0, - a as f32 / 255.0, - ) - } else { - Color::from_rgba8(r, g, b, a as f32 / 255.0) - } + Color::from_rgba8(r, g, b, a as f32 / 255.0) } #[derive(Debug, Clone, Default)] diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 87ded74640..bf73cfcdce 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -6,6 +6,7 @@ use crate::{Backend, Primitive, Renderer, Settings}; use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; use std::marker::PhantomData; +use std::num::NonZeroU32; pub struct Compositor { settings: Settings, @@ -13,7 +14,7 @@ pub struct Compositor { } pub struct Surface { - window: softbuffer::GraphicsContext, + window: softbuffer::Surface, buffer: Vec, clip_mask: tiny_skia::Mask, primitives: Option>, @@ -47,9 +48,14 @@ impl crate::graphics::Compositor for Compositor { height: u32, ) -> Surface { #[allow(unsafe_code)] - let window = - unsafe { softbuffer::GraphicsContext::new(window, window) } - .expect("Create softbuffer for window"); + let window = unsafe { + softbuffer::Surface::new( + &softbuffer::Context::new(window) + .expect("Failed to create softbuffer context"), + window, + ) + } + .expect("Create softbuffer for window"); Surface { window, @@ -174,11 +180,31 @@ pub fn present>( overlay, ); - surface.window.set_buffer( - &surface.buffer, - physical_size.width as u16, - physical_size.height as u16, - ); + surface + .window + .resize( + NonZeroU32::new(physical_size.width) + .ok_or(compositor::SurfaceError::InvalidDimensions)?, + NonZeroU32::new(physical_size.height) + .ok_or(compositor::SurfaceError::InvalidDimensions)?, + ) + .map_err(|_| compositor::SurfaceError::Resize)?; + if let Ok(mut b) = surface.window.buffer_mut() { + let damage = damage + .iter() + .filter_map(|r| { + Some(softbuffer::Rect { + x: r.x as u32, + y: r.y as u32, + width: NonZeroU32::new(r.width as u32)?, + height: NonZeroU32::new(r.height as u32)?, + }) + }) + .collect::>(); + b.copy_from_slice(&surface.buffer); + b.present_with_damage(&damage) + .map_err(|e| compositor::SurfaceError::Present(e.to_string()))?; + } Ok(()) } diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs index b78802c7ba..c87cb5383e 100644 --- a/wgpu/src/image.rs +++ b/wgpu/src/image.rs @@ -426,6 +426,7 @@ impl Pipeline { handle, filter_method, bounds, + .. } => { if let Some(atlas_entry) = raster_cache.upload( device, @@ -661,12 +662,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, }; diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index 557a76335a..204a6b748e 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())); @@ -177,6 +177,21 @@ impl<'a> Layer<'a> { clip_bounds: *clip_bounds + translation, })); } + graphics::Primitive::RawText(graphics::text::Raw { + buffer, + position, + color, + clip_bounds, + }) => { + let layer = &mut layers[current_layer]; + + layer.text.push(Text::Raw(graphics::text::Raw { + buffer: buffer.clone(), + position: *position + translation, + color: *color, + clip_bounds: *clip_bounds + translation, + })); + } Primitive::Quad { bounds, background, @@ -203,6 +218,7 @@ impl<'a> Layer<'a> { handle, filter_method, bounds, + border_radius, } => { let layer = &mut layers[current_layer]; @@ -210,6 +226,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 { diff --git a/wgpu/src/layer/text.rs b/wgpu/src/layer/text.rs index df2f2875d4..37ee524728 100644 --- a/wgpu/src/layer/text.rs +++ b/wgpu/src/layer/text.rs @@ -1,6 +1,7 @@ use crate::core::alignment; use crate::core::text; use crate::core::{Color, Font, Pixels, Point, Rectangle}; +use crate::graphics; use crate::graphics::text::editor; use crate::graphics::text::paragraph; @@ -23,8 +24,10 @@ pub enum Text<'a> { color: Color, clip_bounds: Rectangle, }, - /// A cached text. + /// Some cached text. Cached(Cached<'a>), + /// Some raw text. + Raw(graphics::text::Raw), } #[derive(Debug, Clone)] 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/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/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/wgpu/src/text.rs b/wgpu/src/text.rs index 888b192486..dca09cb8a8 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -7,6 +7,7 @@ use crate::layer::Text; use std::borrow::Cow; use std::cell::RefCell; +use std::sync::Arc; #[allow(missing_debug_implementations)] pub struct Pipeline { @@ -76,6 +77,7 @@ impl Pipeline { Paragraph(Paragraph), Editor(Editor), Cache(cache::KeyHash), + Raw(Arc), } let allocations: Vec<_> = sections @@ -107,6 +109,7 @@ impl Pipeline { Some(Allocation::Cache(key)) } + Text::Raw(text) => text.buffer.upgrade().map(Allocation::Raw), }) .collect(); @@ -185,6 +188,25 @@ impl Pipeline { text.clip_bounds, ) } + Text::Raw(text) => { + let Some(Allocation::Raw(buffer)) = allocation else { + return None; + }; + + let (width, height) = buffer.size(); + + ( + buffer.as_ref(), + Rectangle::new( + text.position, + Size::new(width, height), + ), + alignment::Horizontal::Left, + alignment::Vertical::Top, + text.color, + text.clip_bounds, + ) + } }; let bounds = bounds * scale_factor; diff --git a/widget/Cargo.toml b/widget/Cargo.toml index e8e363c41d..64dde4be8a 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -21,12 +21,17 @@ svg = ["iced_renderer/svg"] canvas = ["iced_renderer/geometry"] qr_code = ["canvas", "qrcode"] wgpu = ["iced_renderer/wgpu"] +a11y = ["iced_accessibility"] +wayland = ["sctk"] [dependencies] iced_renderer.workspace = true 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/button.rs b/widget/src/button.rs index 384a315643..2528d893ad 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -1,6 +1,11 @@ //! 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}; +#[cfg(feature = "a11y")] +use std::borrow::Cow; + use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; @@ -14,6 +19,7 @@ use crate::core::{ Rectangle, Shell, Vector, Widget, }; +use iced_renderer::core::widget::{operation, OperationOutputWrapper}; pub use iced_style::button::{Appearance, StyleSheet}; /// A generic widget that produces a message when pressed. @@ -56,6 +62,13 @@ where Renderer: crate::core::Renderer, Renderer::Theme: StyleSheet, { + id: Id, + #[cfg(feature = "a11y")] + name: Option>, + #[cfg(feature = "a11y")] + description: Option>, + #[cfg(feature = "a11y")] + label: Option>, content: Element<'a, Message, Renderer>, on_press: Option, width: Length, @@ -72,6 +85,13 @@ where /// Creates a new [`Button`] with the given content. pub fn new(content: impl Into>) -> Self { Button { + id: Id::unique(), + #[cfg(feature = "a11y")] + name: None, + #[cfg(feature = "a11y")] + description: None, + #[cfg(feature = "a11y")] + label: None, content: content.into(), on_press: None, width: Length::Shrink, @@ -124,6 +144,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, Renderer> Widget @@ -145,8 +206,8 @@ where vec![Tree::new(&self.content)] } - fn diff(&self, tree: &mut Tree) { - tree.diff_children(std::slice::from_ref(&self.content)); + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(std::slice::from_mut(&mut self.content)) } fn width(&self) -> Length { @@ -177,7 +238,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( @@ -213,9 +274,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( @@ -223,7 +290,7 @@ where tree: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, - _style: &renderer::Style, + renderer_style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, _viewport: &Rectangle, @@ -246,7 +313,11 @@ where renderer, theme, &renderer::Style { + icon_color: styling + .icon_color + .unwrap_or(renderer_style.icon_color), text_color: styling.text_color, + scale_factor: renderer_style.scale_factor, }, content_layout, cursor, @@ -277,6 +348,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, Renderer> From> @@ -294,7 +449,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 { @@ -302,11 +459,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, @@ -347,9 +525,38 @@ pub fn update<'a, Message: Clone>( } } } - Event::Touch(touch::Event::FingerLost { .. }) => { + #[cfg(feature = "a11y")] + Event::A11y( + event_id, + iced_accessibility::accesskit::ActionRequest { action, .. }, + ) => { let state = state(); - + if let Some(Some(on_press)) = (_id == event_id + && matches!( + action, + iced_accessibility::accesskit::Action::Default + )) + .then(|| on_press.clone()) + { + state.is_pressed = false; + shell.publish(on_press); + } + return event::Status::Captured; + } + Event::Keyboard(keyboard::Event::KeyPressed { key_code, .. }) => { + if let Some(on_press) = on_press.clone() { + let state = state(); + if state.is_focused && key_code == keyboard::KeyCode::Enter { + state.is_pressed = true; + shell.publish(on_press); + return event::Status::Captured; + } + } + } + Event::Touch(touch::Event::FingerLost { .. }) + | Event::Mouse(mouse::Event::CursorLeft) => { + let state = state(); + state.is_hovered = false; state.is_pressed = false; } _ => {} @@ -456,3 +663,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 a0d9559b93..4f28a25f93 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -1,4 +1,8 @@ //! Show toggle controls using checkboxes. +use iced_runtime::core::widget::Id; +#[cfg(feature = "a11y")] +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, Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, + Widget, }; pub use iced_style::checkbox::{Appearance, StyleSheet}; @@ -38,6 +43,12 @@ where Renderer: text::Renderer, Renderer::Theme: StyleSheet + crate::text::StyleSheet, { + id: Id, + label_id: Id, + #[cfg(feature = "a11y")] + name: Option>, + #[cfg(feature = "a11y")] + description: Option>, is_checked: bool, on_toggle: Box Message + 'a>, label: String, @@ -76,6 +87,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(), @@ -84,14 +101,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(), } @@ -158,6 +175,33 @@ where self.style = style.into(); self } + + #[cfg(feature = "a11y")] + /// Sets the name of the [`Button`]. + pub fn name(mut self, name: impl Into>) -> Self { + self.name = Some(name.into()); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description_widget( + mut self, + description: &T, + ) -> Self { + self.description = Some(iced_accessibility::Description::Id( + description.description(), + )); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description(mut self, description: impl Into>) -> Self { + self.description = + Some(iced_accessibility::Description::Text(description.into())); + self + } } impl<'a, Message, Renderer> Widget @@ -191,7 +235,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 @@ -335,6 +379,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, Renderer> From> diff --git a/widget/src/column.rs b/widget/src/column.rs index abb522be99..35dd110db6 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; @@ -108,8 +110,8 @@ where self.children.iter().map(Tree::new).collect() } - fn diff(&self, tree: &mut Tree) { - tree.diff_children(&self.children); + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(self.children.as_mut_slice()); } fn width(&self) -> Length { @@ -148,7 +150,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 @@ -246,6 +248,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, Renderer> From> diff --git a/widget/src/container.rs b/widget/src/container.rs index 5dd7705b03..4051ae96dd 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. @@ -148,8 +149,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 width(&self) -> Length { @@ -184,10 +185,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( @@ -260,9 +261,13 @@ where renderer, theme, &renderer::Style { + icon_color: style + .icon_color + .unwrap_or(renderer_style.icon_color), text_color: style .text_color .unwrap_or(renderer_style.text_color), + scale_factor: renderer_style.scale_factor, }, layout.children().next().unwrap(), cursor, @@ -283,6 +288,21 @@ 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[0]; + self.content + .as_widget() + .a11y_nodes(c_layout, c_state, cursor) + } } impl<'a, Message, Renderer> From> @@ -355,30 +375,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> { @@ -461,7 +457,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/dnd_listener.rs b/widget/src/dnd_listener.rs new file mode 100644 index 0000000000..eac05a4d99 --- /dev/null +++ b/widget/src/dnd_listener.rs @@ -0,0 +1,521 @@ +//! 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, Renderer> { + content: Element<'a, Message, 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, Renderer> DndListener<'a, Message, 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, Renderer> DndListener<'a, Message, 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, Renderer> Widget + for DndListener<'a, Message, 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 width(&self) -> Length { + self.content.as_widget().width() + } + + fn height(&self) -> Length { + self.content.as_widget().height() + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout( + renderer, + limits, + Widget::::width(self), + Widget::::height(self), + u32::MAX, + u32::MAX, + |renderer, limits| { + self.content.as_widget().layout( + &mut tree.children[0], + 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: &Renderer::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()) + } +} + +impl<'a, Message, Renderer> From> + for Element<'a, Message, Renderer> +where + Message: 'a + Clone, + Renderer: 'a + crate::core::Renderer, +{ + fn from( + listener: DndListener<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(listener) + } +} + +/// Processes the given [`Event`] and updates the [`State`] of an [`DndListener`] +/// accordingly. +fn update( + widget: &mut DndListener<'_, Message, 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::None | DndState::External(..)) { + return event::Status::Ignored; + } + + 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 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; + } + } + _ => {} + }; + 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(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..ac6fa923dc --- /dev/null +++ b/widget/src/dnd_source.rs @@ -0,0 +1,427 @@ +//! 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, Renderer> { + content: Element<'a, Message, 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, Renderer> DndSource<'a, Message, 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, Renderer> DndSource<'a, Message, 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, Renderer> From> + for Element<'a, Message, Renderer> +where + Renderer: crate::core::Renderer + 'a, + Message: Clone + 'a, +{ + fn from( + dnd_source: DndSource<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(dnd_source) + } +} + +impl<'a, Message, Renderer> Widget + for DndSource<'a, Message, 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 width(&self) -> Length { + self.content.as_widget().width() + } + + fn height(&self) -> Length { + self.content.as_widget().height() + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout( + renderer, + limits, + Widget::::width(self), + Widget::::height(self), + u32::MAX, + u32::MAX, + |renderer, limits| { + self.content.as_widget().layout( + &mut tree.children[0], + 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, + ) + } +} + +/// 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(content.size()); + + layout::Node::with_children(size, vec![content]) +} diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 115198fbac..51ca760913 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, 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; @@ -352,7 +357,10 @@ where /// /// [`Image`]: crate::Image #[cfg(feature = "image")] -pub fn image(handle: impl Into) -> crate::Image { +#[cfg_attr(docsrs, doc(cfg(feature = "image")))] +pub fn image<'a, Handle>( + handle: impl Into, +) -> crate::Image<'a, Handle> { crate::Image::new(handle.into()) } @@ -361,9 +369,10 @@ pub fn image(handle: impl Into) -> crate::Image { /// [`Svg`]: crate::Svg /// [`Handle`]: crate::svg::Handle #[cfg(feature = "svg")] -pub fn svg( +#[cfg_attr(docsrs, doc(cfg(feature = "svg")))] +pub fn svg<'a, Renderer>( handle: impl Into, -) -> crate::Svg +) -> crate::Svg<'a, Renderer> where Renderer: core::svg::Renderer, Renderer::Theme: crate::svg::StyleSheet, @@ -421,3 +430,25 @@ where { MouseArea::new(widget) } + +#[cfg(feature = "wayland")] +/// A container for a dnd source +pub fn dnd_source<'a, Message, Renderer>( + widget: impl Into>, +) -> DndSource<'a, Message, Renderer> +where + Renderer: core::Renderer, +{ + DndSource::new(widget) +} + +#[cfg(feature = "wayland")] +/// A container for a dnd target +pub fn dnd_listener<'a, Message, Renderer>( + widget: impl Into>, +) -> DndListener<'a, Message, Renderer> +where + Renderer: core::Renderer, +{ + DndListener::new(widget) +} diff --git a/widget/src/image.rs b/widget/src/image.rs index 6769910276..baa7f54f3e 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; @@ -15,6 +16,9 @@ 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) @@ -32,26 +36,50 @@ 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, + border_radius: [f32; 4], + 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(), + 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(); @@ -77,6 +105,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`]. @@ -87,6 +150,7 @@ pub fn layout( width: Length, height: Length, content_fit: ContentFit, + _border_radius: [f32; 4], ) -> layout::Node where Renderer: image::Renderer, @@ -126,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, @@ -148,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 @@ -159,7 +229,8 @@ pub fn draw( } } -impl Widget for Image +impl<'a, Message, Renderer, Handle> Widget + for Image<'a, Handle> where Renderer: image::Renderer, Handle: Clone + Hash, @@ -185,6 +256,7 @@ where self.width, self.height, self.content_fit, + self.border_radius, ) } @@ -204,17 +276,78 @@ where &self.handle, self.content_fit, self.filter_method, + self.border_radius, ); } + + #[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, Renderer, Handle> From> +impl<'a, Message, Renderer, Handle> From> for Element<'a, Message, Renderer> where Renderer: image::Renderer, Handle: Clone + Hash + 'a, { - fn from(image: Image) -> Element<'a, Message, Renderer> { + fn from(image: Image<'a, Handle>) -> Element<'a, Message, Renderer> { Element::new(image) } } diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 68015ba8db..bc26c3c45c 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -337,6 +337,7 @@ where y: bounds.y, ..Rectangle::with_size(image_size) }, + [0.0; 4], ); }); }); diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs index 0ef82407cf..2455c0c8b4 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; @@ -150,7 +152,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; @@ -159,8 +161,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]) @@ -209,7 +211,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 167a055dcf..09d7b643be 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, @@ -120,7 +121,7 @@ where self.with_element(|element| vec![Tree::new(element.as_widget())]) } - fn diff(&self, tree: &mut Tree) { + fn diff(&mut self, tree: &mut Tree) { let current = tree.state.downcast_mut::>(); let mut hasher = Hasher::default(); @@ -134,8 +135,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()); @@ -168,7 +171,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( @@ -281,6 +284,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 ad0c382354..0f91413043 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; @@ -56,7 +57,7 @@ pub trait Component { fn operate( &self, _state: &mut Self::State, - _operation: &mut dyn widget::Operation, + _operation: &mut dyn Operation>, ) { } } @@ -111,13 +112,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)); }); } @@ -155,7 +156,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(); @@ -226,6 +227,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()], @@ -238,7 +240,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(); @@ -336,7 +338,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { self.rebuild_element_with_operation(operation); @@ -501,6 +503,34 @@ where ) }) } + fn id(&self) -> Option { + self.with_element(|element| element.as_widget().id()) + } + + fn set_id(&mut self, _id: iced_runtime::core::id::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, Renderer, Event, S>( diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index 86d37b6c1c..46bdab9232 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; @@ -85,7 +85,7 @@ where self.size = new_size; self.layout = None; - tree.diff(&self.element); + tree.diff(&mut self.element); } fn resolve( @@ -157,7 +157,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(); @@ -314,6 +314,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_runtime::core::id::Id) { + self.content + .borrow_mut() + .element + .as_widget_mut() + .set_id(_id); + } } impl<'a, Message, Renderer> From> diff --git a/widget/src/lib.rs b/widget/src/lib.rs index 07378d8349..47fd1888d4 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -132,4 +132,9 @@ pub mod qr_code; #[doc(no_inline)] pub use qr_code::QRCode; +#[cfg(feature = "wayland")] +pub mod dnd_listener; +#[cfg(feature = "wayland")] +pub mod dnd_source; + type Renderer = renderer::Renderer; diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index 3a5b01a3b1..75cf24403d 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -1,5 +1,8 @@ //! 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; use crate::core::mouse; @@ -15,6 +18,7 @@ use crate::core::{ #[allow(missing_debug_implementations)] pub struct MouseArea<'a, Message, Renderer> { content: Element<'a, Message, Renderer>, + on_drag: Option, on_press: Option, on_release: Option, on_right_press: Option, @@ -24,6 +28,13 @@ pub struct MouseArea<'a, Message, Renderer> { } impl<'a, Message, Renderer> MouseArea<'a, Message, 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 { @@ -71,6 +82,7 @@ impl<'a, Message, Renderer> MouseArea<'a, Message, Renderer> { #[derive(Default)] struct State { // TODO: Support on_mouse_enter and on_mouse_exit + drag_initiated: Option, } impl<'a, Message, Renderer> MouseArea<'a, Message, Renderer> { @@ -78,6 +90,7 @@ impl<'a, Message, Renderer> MouseArea<'a, Message, Renderer> { pub fn new(content: impl Into>) -> Self { MouseArea { content: content.into(), + on_drag: None, on_press: None, on_release: None, on_right_press: None, @@ -106,8 +119,8 @@ where vec![Tree::new(&self.content)] } - fn diff(&self, tree: &mut Tree) { - tree.diff_children(std::slice::from_ref(&self.content)); + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(std::slice::from_mut(&mut self.content)); } fn width(&self) -> Length { @@ -134,7 +147,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], @@ -168,7 +181,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( @@ -244,6 +264,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; @@ -253,6 +274,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; @@ -263,6 +285,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; @@ -312,5 +335,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 } diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index e45b44ae60..ef629d1fe9 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -65,7 +65,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(), } @@ -200,7 +200,7 @@ where style, } = menu; - let container = Container::new(Scrollable::new(List { + let mut container = Container::new(Scrollable::new(List { options, hovered_option, on_selected, @@ -213,7 +213,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 2d25a543ee..460a3a94d2 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -24,6 +24,7 @@ pub use configuration::Configuration; pub use content::Content; pub use direction::Direction; pub use draggable::Draggable; +use iced_renderer::core::widget::{Operation, OperationOutputWrapper}; pub use node::Node; pub use pane::Pane; pub use split::Split; @@ -39,7 +40,6 @@ use crate::core::mouse; use crate::core::overlay::{self, Group}; use crate::core::renderer; use crate::core::touch; -use crate::core::widget; use crate::core::widget::tree::{self, Tree}; use crate::core::{ Clipboard, Color, Element, Layout, Length, Pixels, Point, Rectangle, Shell, @@ -250,15 +250,20 @@ where .collect() } - fn diff(&self, tree: &mut Tree) { - match &self.contents { - Contents::All(contents, _) => tree.diff_children_custom( - contents, - |state, (_, content)| content.diff(state), - |(_, content)| content.state(), - ), + fn diff(&mut self, tree: &mut Tree) { + match &mut self.contents { + Contents::All(contents, _) => { + let ids = contents.iter().map(|_| None).collect(); // TODO + tree.diff_children_custom( + contents, + ids, + |state, (_, content)| content.diff(state), + |(_, content)| content.state(), + ) + } Contents::Maximized(_, content, _) => tree.diff_children_custom( - &[content], + &mut [content], + vec![None], // TODO |state, content| content.diff(state), |content| content.state(), ), @@ -299,7 +304,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 826ea66349..dd0ab7cea5 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}; @@ -73,13 +75,13 @@ where } } - pub(super) fn diff(&self, tree: &mut Tree) { + pub(super) fn diff(&mut self, tree: &mut Tree) { if tree.children.len() == 2 { - if let Some(title_bar) = self.title_bar.as_ref() { + if let Some(title_bar) = self.title_bar.as_mut() { title_bar.diff(&mut tree.children[1]); } - tree.children[0].diff(&self.body); + tree.children[0].diff(&mut self.body); } else { *tree = self.state(); } @@ -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 f4dbb6b145..b76773d5f5 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, }; @@ -100,13 +102,13 @@ where } } - pub(super) fn diff(&self, tree: &mut Tree) { + pub(super) fn diff(&mut self, tree: &mut Tree) { if tree.children.len() == 2 { - if let Some(controls) = self.controls.as_ref() { + if let Some(controls) = self.controls.as_mut() { tree.children[1].diff(controls); } - tree.children[0].diff(&self.content); + tree.children[0].diff(&mut self.content); } else { *tree = self.state(); } @@ -131,7 +133,9 @@ 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, }; container::draw_background(renderer, &style, bounds); @@ -263,7 +267,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 022ca8d92a..918deff29f 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}; @@ -74,7 +76,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(), @@ -663,8 +665,8 @@ pub fn draw<'a, T, 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/radio.rs b/widget/src/radio.rs index ae2365dddc..6626c54522 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/row.rs b/widget/src/row.rs index d52b8c43a6..4d26be198a 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; @@ -100,8 +102,8 @@ where self.children.iter().map(Tree::new).collect() } - fn diff(&self, tree: &mut Tree) { - tree.diff_children(&self.children); + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(&mut self.children) } fn width(&self) -> Length { @@ -137,7 +139,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 @@ -235,6 +237,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, Renderer> From> diff --git a/widget/src/rule.rs b/widget/src/rule.rs index b5c5fa5576..a9570bb150 100644 --- a/widget/src/rule.rs +++ b/widget/src/rule.rs @@ -47,6 +47,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, diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 49aed2f0f9..3525b01952 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -1,4 +1,8 @@ //! 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}; use crate::core::keyboard; use crate::core::layout; @@ -6,16 +10,16 @@ use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; use crate::core::touch; -use crate::core::widget; use crate::core::widget::operation::{self, Operation}; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Background, Clipboard, Color, Element, Layout, Length, Pixels, Point, - Rectangle, Shell, Size, Vector, Widget, + id::Internal, Background, Clipboard, Color, Element, Layout, Length, + Pixels, Point, Rectangle, Shell, Size, Vector, Widget, }; use crate::runtime::Command; pub use crate::style::scrollable::{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 @@ -26,7 +30,14 @@ where Renderer: crate::core::Renderer, Renderer::Theme: StyleSheet, { - id: Option, + id: Id, + scrollbar_id: Id, + #[cfg(feature = "a11y")] + name: Option>, + #[cfg(feature = "a11y")] + description: Option>, + #[cfg(feature = "a11y")] + label: Option>, width: Length, height: Length, direction: Direction, @@ -43,7 +54,14 @@ where /// Creates a new [`Scrollable`]. pub fn new(content: impl Into>) -> Self { Scrollable { - id: None, + id: Id::unique(), + scrollbar_id: Id::unique(), + #[cfg(feature = "a11y")] + name: None, + #[cfg(feature = "a11y")] + description: None, + #[cfg(feature = "a11y")] + label: None, width: Length::Shrink, height: Length::Shrink, direction: Direction::default(), @@ -55,7 +73,7 @@ where /// Sets the [`Id`] of the [`Scrollable`]. pub fn id(mut self, id: Id) -> Self { - self.id = Some(id); + self.id = id; self } @@ -76,7 +94,6 @@ where self.direction = direction; self } - /// Sets a function to call when the [`Scrollable`] is scrolled. /// /// The function takes the [`Viewport`] of the [`Scrollable`] @@ -93,6 +110,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`]. @@ -216,8 +268,8 @@ where vec![Tree::new(&self.content)] } - fn diff(&self, tree: &mut Tree) { - tree.diff_children(std::slice::from_ref(&self.content)); + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(std::slice::from_mut(&mut self.content)) } fn width(&self) -> Length { @@ -255,7 +307,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation>, ) { let state = tree.state.downcast_mut::(); @@ -265,25 +317,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( @@ -403,6 +446,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, Renderer> From> @@ -419,37 +602,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`] @@ -458,7 +617,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`]. @@ -498,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, @@ -540,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, diff --git a/widget/src/slider.rs b/widget/src/slider.rs index ac0982c82f..ed92429ed4 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -7,6 +7,7 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; +use crate::core::widget::Id; use crate::core::{ Clipboard, Color, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size, Widget, @@ -14,10 +15,14 @@ use crate::core::{ use std::ops::RangeInclusive; +use iced_renderer::core::{BorderRadius, Degrees, Radians}; 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. /// @@ -48,6 +53,13 @@ where Renderer: crate::core::Renderer, Renderer::Theme: StyleSheet, { + id: Id, + #[cfg(feature = "a11y")] + name: Option>, + #[cfg(feature = "a11y")] + description: Option>, + #[cfg(feature = "a11y")] + label: Option>, range: RangeInclusive, step: T, value: T, @@ -93,6 +105,13 @@ where }; Slider { + id: Id::unique(), + #[cfg(feature = "a11y")] + name: None, + #[cfg(feature = "a11y")] + description: None, + #[cfg(feature = "a11y")] + label: None, value, range, step: T::from(1), @@ -141,6 +160,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, Renderer> Widget @@ -236,6 +290,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, Renderer> From> @@ -366,16 +501,35 @@ pub fn draw( } else { style_sheet.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, BorderRadius::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; @@ -394,46 +548,75 @@ pub fn draw( let rail_y = bounds.y + bounds.height / 2.0; - 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_radius: style.rail.border_radius, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + 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_radius: style.rail.border_radius, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + 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_radius: style.rail.border_radius, + border_width: 0.0, + border_color: Color::TRANSPARENT, }, - border_radius: style.rail.border_radius, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - style.rail.colors.0, - ); - - 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::from(Degrees(90.0)); + gradient + } else { + gradient }, - border_radius: style.rail.border_radius, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - 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, }, border_radius: handle_border_radius, - border_width: style.handle.border_width, + border_width, border_color: style.handle.border_color, }, style.handle.color, diff --git a/widget/src/svg.rs b/widget/src/svg.rs index 2d01d1ab0a..49a558f627 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,8 @@ use crate::core::{ ContentFit, Element, Layout, Length, Rectangle, Size, Vector, Widget, }; +#[cfg(feature = "a11y")] +use std::borrow::Cow; use std::path::PathBuf; pub use crate::style::svg::{Appearance, StyleSheet}; @@ -20,19 +24,28 @@ pub use svg::Handle; /// [`Svg`] images can have a considerable rendering cost when resized, /// specially when they are complex. #[allow(missing_debug_implementations)] -pub struct Svg +pub struct Svg<'a, Renderer = crate::Renderer> where Renderer: svg::Renderer, Renderer::Theme: StyleSheet, { + id: Id, + #[cfg(feature = "a11y")] + name: Option>, + #[cfg(feature = "a11y")] + description: Option>, + #[cfg(feature = "a11y")] + label: Option>, handle: Handle, width: Length, height: Length, content_fit: ContentFit, + symbolic: bool, style: ::Style, + phantom_data: std::marker::PhantomData<&'a ()>, } -impl Svg +impl<'a, Renderer> Svg<'a, Renderer> where Renderer: svg::Renderer, Renderer::Theme: StyleSheet, @@ -40,11 +53,20 @@ 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, content_fit: ContentFit::Contain, + symbolic: false, style: Default::default(), + phantom_data: std::marker::PhantomData, } } @@ -80,6 +102,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( @@ -89,9 +118,44 @@ where self.style = style; self } + + #[cfg(feature = "a11y")] + /// Sets the name of the [`Button`]. + pub fn name(mut self, name: impl Into>) -> Self { + self.name = Some(name.into()); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description_widget( + mut self, + description: &T, + ) -> Self { + self.description = Some(iced_accessibility::Description::Id( + description.description(), + )); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description(mut self, description: impl Into>) -> Self { + self.description = + Some(iced_accessibility::Description::Text(description.into())); + self + } + + #[cfg(feature = "a11y")] + /// Sets the label of the [`Button`]. + pub fn label(mut self, label: &dyn iced_accessibility::Labels) -> Self { + self.label = + Some(label.label().into_iter().map(|l| l.into()).collect()); + self + } } -impl Widget for Svg +impl<'a, Message, Renderer> Widget for Svg<'a, Renderer> where Renderer: svg::Renderer, Renderer::Theme: iced_style::svg::StyleSheet, @@ -143,7 +207,7 @@ where _state: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, - _style: &renderer::Style, + style: &renderer::Style, layout: Layout<'_>, _cursor: mouse::Cursor, _viewport: &Rectangle, @@ -166,7 +230,10 @@ where ..bounds }; - let appearance = theme.appearance(&self.style); + let mut appearance = theme.appearance(&self.style); + if self.symbolic && appearance.color.is_none() { + appearance.color = Some(style.icon_color); + } renderer.draw( self.handle.clone(), @@ -183,15 +250,75 @@ 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, Renderer> From> +impl<'a, Message, Renderer> From> for Element<'a, Message, Renderer> where Renderer: svg::Renderer + 'a, Renderer::Theme: iced_style::svg::StyleSheet, { - fn from(icon: Svg) -> Element<'a, Message, Renderer> { + fn from(icon: Svg<'a, Renderer>) -> Element<'a, Message, Renderer> { Element::new(icon) } } diff --git a/widget/src/text_input/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 97% rename from widget/src/text_input.rs rename to widget/src/text_input/text_input.rs index 65d3e1eb6e..535ed2deef 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input/text_input.rs @@ -1,15 +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; +use super::cursor; -pub use cursor::Cursor; -pub use value::Value; +use super::editor::Editor; -use editor::Editor; +use iced_renderer::core::widget::OperationOutputWrapper; use crate::core::alignment; use crate::core::event::{self, Event}; @@ -20,9 +19,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, Color, Element, Layout, Length, Padding, Pixels, Point, @@ -271,7 +270,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 @@ -318,12 +317,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( @@ -431,45 +430,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 @@ -478,12 +453,12 @@ pub fn move_cursor_to( id: Id, position: usize, ) -> Command { - Command::widget(operation::text_input::move_cursor_to(id.0, position)) + Command::widget(operation::text_input::move_cursor_to(id, position)) } /// Produces a [`Command`] that selects all the content of the [`TextInput`] with the given [`Id`]. pub fn select_all(id: Id) -> Command { - Command::widget(operation::text_input::select_all(id.0)) + Command::widget(operation::text_input::select_all(id)) } /// Computes the layout of a [`TextInput`]. diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index d872308061..c44288fbcd 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -1,4 +1,7 @@ //! Show toggle controls using togglers. +#[cfg(feature = "a11y")] +use std::borrow::Cow; + use crate::core::alignment; use crate::core::event; use crate::core::layout; @@ -6,10 +9,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::{ - Clipboard, Element, Event, Layout, Length, Pixels, Rectangle, Shell, Size, + id, Clipboard, Element, Event, Layout, Length, Pixels, Rectangle, Shell, Widget, }; @@ -37,6 +40,14 @@ where Renderer: text::Renderer, Renderer::Theme: StyleSheet, { + id: Id, + label_id: Option, + #[cfg(feature = "a11y")] + name: Option>, + #[cfg(feature = "a11y")] + description: Option>, + #[cfg(feature = "a11y")] + labeled_by_widget: Option>, is_toggled: bool, on_toggle: Box Message + 'a>, label: Option, @@ -75,17 +86,27 @@ 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, width: Length::Fill, size: Self::DEFAULT_SIZE, 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(), } @@ -152,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, Renderer> Widget @@ -187,7 +243,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 +270,7 @@ where self.text_shaping, ) } else { - layout::Node::new(Size::ZERO) + layout::Node::new(crate::core::Size::ZERO) } }, ) @@ -346,6 +407,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, Renderer> From> diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs index 9e102c5671..3539721316 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -109,14 +109,10 @@ where fn children(&self) -> Vec { vec![ widget::Tree::new(&self.content), - widget::Tree::new(&self.tooltip as &dyn Widget), + widget::Tree::new(&self.tooltip as &dyn Widget), ] } - 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()) } @@ -125,6 +121,13 @@ where widget::tree::Tag::of::() } + fn diff(&mut self, tree: &mut widget::Tree) { + tree.diff_children(&mut [ + self.content.as_widget_mut(), + &mut self.tooltip, + ]); + } + fn width(&self) -> Length { self.content.as_widget().width() } @@ -440,7 +443,9 @@ 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, }; Widget::<(), Renderer>::draw( diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index 01d3359ca5..9931f06e43 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; @@ -393,35 +395,63 @@ 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_radius: style.rail.border_radius, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - 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_radius: style.rail.border_radius, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - 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_radius: style.rail.border_radius, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + 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_radius: style.rail.border_radius, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + 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_radius: style.rail.border_radius, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + if auto_angle { + gradient.angle = Radians::from(Degrees(180.0)); + gradient + } else { + gradient + }, + ); + } + } renderer.fill_quad( renderer::Quad { 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 d970007573..9cd992f427 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -1,6 +1,12 @@ //! Create interactive, native cross-platform applications. +mod drag_resize; 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; +use iced_runtime::futures::futures::FutureExt; pub use state::State; use crate::conversion; @@ -20,11 +26,40 @@ 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; +#[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 @@ -115,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(); let proxy = event_loop.create_proxy(); @@ -193,20 +233,29 @@ where let (mut event_sender, event_receiver) = mpsc::unbounded(); let (control_sender, mut control_receiver) = mpsc::unbounded(); - let mut instance = Box::pin(run_instance::( - application, - compositor, - renderer, - runtime, - proxy, - debug, - event_receiver, - control_sender, - init_command, - window, - should_be_visible, - exit_on_close_request, - )); + let mut instance = Box::pin({ + let run_instance = run_instance::( + application, + compositor, + renderer, + runtime, + proxy, + debug, + event_receiver, + control_sender, + init_command, + window, + should_be_visible, + exit_on_close_request, + resize_border, + ); + + #[cfg(feature = "trace")] + let run_instance = + run_instance.instrument(info_span!("Application", "LOOP")); + + run_instance + }); let mut context = task::Context::from_waker(task::noop_waker_ref()); @@ -255,24 +304,28 @@ async fn run_instance( mut application: A, mut compositor: C, mut renderer: A::Renderer, - mut runtime: Runtime, A::Message>, - mut proxy: winit::event_loop::EventLoopProxy, + mut runtime: Runtime< + E, + Proxy>, + UserEventWrapper, + >, + mut proxy: winit::event_loop::EventLoopProxy>, mut debug: Debug, mut event_receiver: mpsc::UnboundedReceiver< - winit::event::Event<'_, A::Message>, + winit::event::Event<'_, UserEventWrapper>, >, mut control_sender: mpsc::UnboundedSender, init_command: Command, window: winit::window::Window, should_be_visible: bool, exit_on_close_request: bool, + resize_border: u32, ) where A: Application + 'static, E: Executor + 'static, C: Compositor + 'static, ::Theme: StyleSheet, { - use futures::stream::StreamExt; use winit::event; use winit::event_loop::ControlFlow; @@ -308,7 +361,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, @@ -318,10 +376,50 @@ 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(); let mut redraw_pending = false; + #[cfg(feature = "a11y")] + 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(); @@ -423,7 +521,9 @@ 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(), }, state.cursor(), ); @@ -466,7 +566,26 @@ async fn run_instance( )); } event::Event::UserEvent(message) => { - messages.push(message); + match message { + UserEventWrapper::Message(m) => messages.push(m), + #[cfg(feature = "a11y")] + UserEventWrapper::A11y(request) => { + match request.request.action { + iced_accessibility::accesskit::Action::Focus => { + commands.push(Command::widget(focus( + core::widget::Id::from(u128::from( + request.request.target.0, + ) + as u64), + ))); + } + _ => {} + } + events.push(conversion::a11y(request.request)); + } + #[cfg(feature = "a11y")] + UserEventWrapper::A11yEnabled => a11y_enabled = true, + }; } event::Event::RedrawRequested(_) => { let physical_size = state.physical_size(); @@ -475,6 +594,78 @@ async fn run_instance( continue; } + #[cfg(feature = "a11y")] + if a11y_enabled { + use iced_accessibility::{ + accesskit::{ + NodeBuilder, NodeId, Role, Tree, TreeUpdate, + }, + A11yId, A11yNode, A11yTree, + }; + // TODO send a11y tree + let child_tree = user_interface.a11y_nodes(state.cursor()); + let mut root = NodeBuilder::new(Role::Window); + root.set_name(state.title()); + + let window_tree = A11yTree::node_with_child_tree( + A11yNode::new(root, window_a11y_id), + child_tree, + ); + let tree = Tree::new(NodeId(window_a11y_id)); + let mut current_operation = + Some(Box::new(OperationWrapper::Id(Box::new( + operation::focusable::find_focused(), + )))); + + let mut focus = None; + while let Some(mut operation) = current_operation.take() { + user_interface.operate(&renderer, operation.as_mut()); + + match operation.finish() { + operation::Outcome::None => {} + operation::Outcome::Some(message) => match message { + operation::OperationOutputWrapper::Message( + _, + ) => { + unimplemented!(); + } + operation::OperationOutputWrapper::Id(id) => { + focus = Some(A11yId::from(id)); + } + }, + operation::Outcome::Chain(next) => { + current_operation = Some(Box::new( + OperationWrapper::Wrapper(next), + )); + } + } + } + + log::debug!( + "focus: {:?}\ntree root: {:?}\n children: {:?}", + &focus, + window_tree + .root() + .iter() + .map(|n| (n.node().role(), n.id())) + .collect::>(), + window_tree + .children() + .iter() + .map(|n| (n.node().role(), n.id())) + .collect::>() + ); + // TODO maybe optimize this? + let focus = focus + .filter(|f_id| window_tree.contains(f_id)) + .map(|id| id.into()); + adapter.update(TreeUpdate { + nodes: window_tree.into(), + tree: Some(tree), + focus, + }); + } + debug.render_started(); let current_viewport_version = state.viewport_version(); @@ -493,7 +684,9 @@ 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(), }, state.cursor(), ); @@ -506,7 +699,6 @@ async fn run_instance( mouse_interaction = new_mouse_interaction; } debug.draw_finished(); - compositor.configure_surface( &mut surface, physical_size.width, @@ -547,6 +739,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 { @@ -619,6 +818,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( @@ -628,10 +837,14 @@ pub fn update( cache: &mut user_interface::Cache, state: &State, renderer: &mut A::Renderer, - runtime: &mut Runtime, A::Message>, + runtime: &mut Runtime< + E, + Proxy>, + UserEventWrapper, + >, clipboard: &mut Clipboard, should_exit: &mut bool, - proxy: &mut winit::event_loop::EventLoopProxy, + proxy: &mut winit::event_loop::EventLoopProxy>, debug: &mut Debug, messages: &mut Vec, window: &winit::window::Window, @@ -663,8 +876,12 @@ pub fn update( ); } - let subscription = application.subscription(); - runtime.track(subscription.into_recipes()); + runtime.track( + application + .subscription() + .map(subscription_map::) + .into_recipes(), + ); } /// Runs the actions of a [`Command`]. @@ -676,10 +893,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 @@ -695,17 +916,17 @@ pub fn run_command( for action in command.actions() { match action { command::Action::Future(future) => { - runtime.spawn(future); + runtime.spawn(Box::pin(future.map(UserEventWrapper::Message))); } command::Action::Stream(stream) => { - runtime.run(stream); + runtime.run(Box::pin(stream.map(UserEventWrapper::Message))); } 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) => { @@ -736,9 +957,8 @@ 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"); } @@ -772,7 +992,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) => { @@ -794,7 +1014,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) => { @@ -807,9 +1029,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."); } @@ -828,16 +1052,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, @@ -852,12 +1076,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))); } } } @@ -872,9 +1108,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/drag_resize.rs b/winit/src/application/drag_resize.rs new file mode 100644 index 0000000000..379d7c9ed8 --- /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, + }, + }) +} diff --git a/winit/src/application/state.rs b/winit/src/application/state.rs index e655529a67..3a6d233ab9 100644 --- a/winit/src/application/state.rs +++ b/winit/src/application/state.rs @@ -111,11 +111,21 @@ 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 } + /// 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/conversion.rs b/winit/src/conversion.rs index 7e51a2d400..c10204cece 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -388,6 +388,8 @@ pub fn mouse_button(mouse_button: winit::event::MouseButton) -> mouse::Button { winit::event::MouseButton::Left => mouse::Button::Left, winit::event::MouseButton::Right => mouse::Button::Right, winit::event::MouseButton::Middle => mouse::Button::Middle, + winit::event::MouseButton::Forward => mouse::Button::Forward, + winit::event::MouseButton::Back => mouse::Button::Back, winit::event::MouseButton::Other(other) => mouse::Button::Other(other), } } @@ -665,3 +667,13 @@ pub(crate) fn is_private_use_character(c: char) -> bool { | '\u{100000}'..='\u{10FFFD}' ) } + +#[cfg(feature = "a11y")] +pub(crate) fn a11y( + event: iced_accessibility::accesskit::ActionRequest, +) -> Event { + // XXX + let id = + iced_runtime::core::id::Id::from(u128::from(event.target.0) as u64); + Event::A11y(id, event) +} diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 84651d40de..a10161e656 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,11 +23,23 @@ 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; +pub use state::State; use std::collections::HashMap; use std::mem::ManuallyDrop; 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 @@ -306,10 +320,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, @@ -332,6 +352,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), @@ -369,7 +422,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(); let mut redraw_pending = false; @@ -558,6 +616,8 @@ async fn run_instance( state.theme(), &renderer::Style { text_color: state.text_color(), + icon_color: state.icon_color(), + scale_factor: state.scale_factor(), }, cursor, ) @@ -622,7 +682,26 @@ async fn run_instance( )); } event::Event::UserEvent(message) => { - messages.push(message); + match message { + UserEventWrapper::Message(m) => messages.push(m), + #[cfg(feature = "a11y")] + UserEventWrapper::A11y(request) => { + match request.request.action { + iced_accessibility::accesskit::Action::Focus => { + // TODO send a command for this + } + _ => {} + } + events.push(( + None, + conversion::a11y(request.request), + )); + } + #[cfg(feature = "a11y")] + UserEventWrapper::A11yEnabled => { + a11y_enabled = true + } + }; } event::Event::RedrawRequested(id) => { let Some((id, window)) = @@ -666,6 +745,10 @@ async fn run_instance( window.state.theme(), &renderer::Style { text_color: window.state.text_color(), + icon_color: window.state.icon_color(), + scale_factor: window + .state + .scale_factor(), }, window.state.cursor(), ); @@ -808,10 +891,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 +928,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 +940,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 +965,17 @@ 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(UserEventWrapper::Message))); } command::Action::Stream(stream) => { - runtime.run(Box::pin(stream)); + runtime.run(Box::pin(stream.map(UserEventWrapper::Message))); } 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) => { @@ -935,9 +1030,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"); } @@ -985,7 +1079,7 @@ fn run_command( }; proxy - .send_event(tag(mode)) + .send_event(UserEventWrapper::Message(tag(mode))) .expect("Event loop doesn't exist."); } } @@ -1021,7 +1115,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."); } } @@ -1036,9 +1133,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."); } @@ -1058,15 +1157,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, @@ -1074,9 +1173,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()); @@ -1084,15 +1181,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), + )); } } } @@ -1112,9 +1219,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."); + } } } } diff --git a/winit/src/multi_window/state.rs b/winit/src/multi_window/state.rs index 03da5ad751..9a38001242 100644 --- a/winit/src/multi_window/state.rs +++ b/winit/src/multi_window/state.rs @@ -138,6 +138,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,