diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 5716979605..14cd20d1cf 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -16,15 +16,3 @@ jobs: run: cargo update - name: Audit vulnerabilities run: cargo audit - - artifacts: - runs-on: ubuntu-latest - steps: - - uses: hecrj/setup-rust-action@v1 - - name: Install cargo-outdated - run: cargo install cargo-outdated - - uses: actions/checkout@master - - name: Delete `web-sys` dependency from `integration` example - run: sed -i '$d' examples/integration/Cargo.toml - - name: Find outdated dependencies - run: cargo outdated --workspace --exit-code 1 --ignore raw-window-handle diff --git a/.github/workflows/document.yml b/.github/workflows/document.yml index 35bf10f428..d916e26538 100644 --- a/.github/workflows/document.yml +++ b/.github/workflows/document.yml @@ -13,7 +13,7 @@ jobs: - name: Generate documentation run: | RUSTDOCFLAGS="--cfg docsrs" \ - cargo doc --no-deps --all-features \ + cargo doc --no-deps --features "winit" \ -p iced_core \ -p iced_highlighter \ -p iced_style \ diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2ff86614ad..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 c9dee6b75d..b4260463d8 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,28 @@ 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", +] +# Enables clipboard for iced_sctk +wayland-clipboard = ["iced_sctk?/clipboard"] [dependencies] iced_core.workspace = true @@ -61,40 +77,50 @@ 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 +window_clipboard.workspace = true +mime.workspace = true +dnd.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", ] +exclude = ["examples/integration"] + + +[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" @@ -118,15 +144,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" +bitflags = "2.5" 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 = "0.5" +glyphon = { git = "https://github.com/pop-os/glyphon.git", tag = "v0.5.0" } guillotiere = "0.6" half = "2.2" image = "0.24" @@ -141,11 +169,12 @@ ouroboros = "0.17" palette = "0.7" qrcode = { version = "0.12", default-features = false } raw-window-handle = "0.6" -resvg = "0.36" +resvg = "0.37" rustc-hash = "1.0" +sctk = { package = "smithay-client-toolkit", version = "0.19.1" } smol = "1.0" smol_str = "0.2" -softbuffer = "0.4" +softbuffer = { git = "https://github.com/pop-os/softbuffer", tag = "cosmic-4.0" } syntect = "5.1" sysinfo = "0.28" thiserror = "1.0" @@ -156,9 +185,19 @@ xxhash-rust = { version = "0.8", features = ["xxh3"] } unicode-segmentation = "1.0" wasm-bindgen-futures = "0.4" wasm-timer = "0.2" +wayland-protocols = { version = "0.32.1", features = ["staging"] } web-sys = "0.3" web-time = "0.2" -wgpu = "0.19" +# wgpu = "0.19" +# Newer wgpu commit that fixes Vulkan backend on Nvidia +wgpu = { git = "https://github.com/gfx-rs/wgpu", rev = "20fda69" } winapi = "0.3" -window_clipboard = "0.4" -winit = { git = "https://github.com/iced-rs/winit.git", rev = "b91e39ece2c0d378c3b80da7f3ab50e17bb798a5" } +window_clipboard = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-8" } +dnd = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-8" } +mime = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-dnd-8" } +# window_clipboard = { path = "../../window_clipboard" } +# dnd = { path = "../../window_clipboard/dnd" } +# mime = { path = "../../window_clipboard/mime" } +winit = { git = "https://github.com/pop-os/winit.git", branch = "winit-0.29" } + + diff --git a/accessibility/Cargo.toml b/accessibility/Cargo.toml new file mode 100644 index 0000000000..0423ff97e3 --- /dev/null +++ b/accessibility/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "iced_accessibility" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +# TODO Ashley re-export more platform adapters + +[dependencies] +accesskit = { git = "https://github.com/wash2/accesskit.git", branch = "winit-0.29" } +accesskit_unix = { git = "https://github.com/wash2/accesskit.git", branch = "winit-0.29", optional = true, default-features = false, features = [ + "tokio", +] } +accesskit_windows = { git = "https://github.com/wash2/accesskit.git", branch = "winit-0.29", optional = true } +accesskit_macos = { git = "https://github.com/wash2/accesskit.git", branch = "winit-0.29", optional = true } +accesskit_winit = { git = "https://github.com/wash2/accesskit.git", branch = "winit-0.29", optional = true } +# accesskit = { path = "../../../accesskit/common/", version = "0.12.2" } +# accesskit_unix = { path = "../../../accesskit/platforms/unix/", version = "0.7.1", optional = true, default-features = false, features = [ +# "tokio", +# ] } +# accesskit_windows = { path = "../../../accesskit/platforms/windows/", version = "0.16.0", optional = true } +# accesskit_macos = { path = "../../../accesskit/platforms/macos/", version = "0.11.0", optional = true } +# accesskit_winit = { path = "../../../accesskit/platforms/winit/", version = "0.18.1", optional = true } 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..752e51c192 --- /dev/null +++ b/accessibility/src/id.rs @@ -0,0 +1,215 @@ +//! Widget and Window IDs. + +use std::borrow; +use std::hash::Hash; +use std::sync::atomic::{self, AtomicU64}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum A11yId { + Window(u64), + 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: u64) -> 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 = u64::from(value.0); + if val > u32::MAX as u64 { + 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 u64 { + fn from(val: Id) -> u64 { + match &val.0 { + Internal::Unique(id) => *id, + Internal::Custom(id, _) => *id, + // this is a set id, which is not a valid id and will not ever be converted to a NonZeroU128 + // so we panic + Internal::Set(_) => { + 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 u32::MAX to them +/// get window node id that won't conflict with other node ids for the duration of the program +pub fn window_node_id() -> u64 { + u32::MAX as u64 + + NEXT_WINDOW_ID.fetch_add(1, atomic::Ordering::Relaxed) as u64 +} + +// 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 32dd3df26a..40966bb89f 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -10,6 +10,10 @@ 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 @@ -18,12 +22,26 @@ smol_str.workspace = true thiserror.workspace = true web-time.workspace = true xxhash-rust.workspace = true +window_clipboard.workspace = true +dnd.workspace = true +mime.workspace = true +sctk.workspace = true +sctk.optional = true palette.workspace = true palette.optional = true +[dependencies.serde] +version = "1" +optional = true +features = ["serde_derive"] + [target.'cfg(windows)'.dependencies] 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/clipboard.rs b/core/src/clipboard.rs index 081b40046c..206e6e1e78 100644 --- a/core/src/clipboard.rs +++ b/core/src/clipboard.rs @@ -1,5 +1,12 @@ //! Access the clipboard. +use std::{any::Any, sync::Arc}; + +use dnd::{DndAction, DndDestinationRectangle, DndSurface}; +use mime::{self, AllowedMimeTypes, AsMimeTypes, ClipboardStoreData}; + +use crate::{widget::tree::State, window, Element}; + /// A buffer for short-term storage and transfer within and between /// applications. pub trait Clipboard { @@ -8,6 +15,102 @@ pub trait Clipboard { /// Writes the given text contents to the [`Clipboard`]. fn write(&mut self, contents: String); + + /// Read the current content of the primary [`Clipboard`] as text. + fn read_primary(&self) -> Option { + None + } + + /// Writes the given text contents to the primary [`Clipboard`]. + fn write_primary(&mut self, _contents: String) {} + + /// Consider using [`read_data`] instead + /// Reads the current content of the [`Clipboard`] as text. + fn read_data(&self, _mimes: Vec) -> Option<(Vec, String)> { + None + } + + /// Writes the given contents to the [`Clipboard`]. + fn write_data( + &mut self, + _contents: ClipboardStoreData< + Box, + >, + ) { + } + + /// Consider using [`read_primary_data`] instead + /// Reads the current content of the primary [`Clipboard`] as text. + fn read_primary_data( + &self, + _mimes: Vec, + ) -> Option<(Vec, String)> { + None + } + + /// Writes the given text contents to the primary [`Clipboard`]. + fn write_primary_data( + &mut self, + _contents: ClipboardStoreData< + Box, + >, + ) { + } + + /// Starts a DnD operation. + fn register_dnd_destination( + &self, + _surface: DndSurface, + _rectangles: Vec, + ) { + } + + /// Set the final action for the DnD operation. + /// Only should be done if it is requested. + fn set_action(&self, _action: DndAction) {} + + /// Registers Dnd destinations + fn start_dnd( + &self, + _internal: bool, + _source_surface: Option, + _icon_surface: Option>, + _content: Box, + _actions: DndAction, + ) { + } + + /// Ends a DnD operation. + fn end_dnd(&self) {} + + /// Consider using [`peek_dnd`] instead + /// Peeks the data on the DnD with a specific mime type. + /// Will return an error if there is no ongoing DnD operation. + fn peek_dnd(&self, _mime: String) -> Option<(Vec, String)> { + None + } +} + +/// Starts a DnD operation. +/// icon surface is a tuple of the icon element and optionally the icon element state. +pub fn start_dnd( + clipboard: &mut dyn Clipboard, + internal: bool, + source_surface: Option, + icon_surface: Option<(Element<'static, M, T, R>, State)>, + content: Box, + actions: DndAction, +) { + clipboard.start_dnd( + internal, + source_surface, + icon_surface.map(|i| { + let i: Box = Box::new(Arc::new(i)); + i + }), + content, + actions, + ); } /// A null implementation of the [`Clipboard`] trait. @@ -21,3 +124,90 @@ impl Clipboard for Null { fn write(&mut self, _contents: String) {} } + +/// Reads the current content of the [`Clipboard`]. +pub fn read_data( + clipboard: &mut dyn Clipboard, +) -> Option { + clipboard + .read_data(T::allowed().into()) + .and_then(|data| T::try_from(data).ok()) +} + +/// Reads the current content of the primary [`Clipboard`]. +pub fn read_primary_data( + clipboard: &mut dyn Clipboard, +) -> Option { + clipboard + .read_data(T::allowed().into()) + .and_then(|data| T::try_from(data).ok()) +} + +/// Reads the current content of the primary [`Clipboard`]. +pub fn peek_dnd( + clipboard: &mut dyn Clipboard, + mime: Option, +) -> Option { + let Some(mime) = mime.or_else(|| T::allowed().first().cloned().into()) + else { + return None; + }; + clipboard + .peek_dnd(mime) + .and_then(|data| T::try_from(data).ok()) +} + +/// Source of a DnD operation. +#[derive(Debug, Clone)] +pub enum DndSource { + /// A widget is the source of the DnD operation. + Widget(crate::id::Id), + /// A surface is the source of the DnD operation. + Surface(window::Id), +} + +/// A list of DnD destination rectangles. +#[derive(Debug, Clone)] +pub struct DndDestinationRectangles { + /// The rectangle of the DnD destination. + rectangles: Vec, +} + +impl DndDestinationRectangles { + /// Creates a new [`DndDestinationRectangles`]. + pub fn new() -> Self { + Self { + rectangles: Vec::new(), + } + } + + /// Creates a new [`DndDestinationRectangles`] with the given capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self { + rectangles: Vec::with_capacity(capacity), + } + } + + /// Pushes a new rectangle to the list of DnD destination rectangles. + pub fn push(&mut self, rectangle: DndDestinationRectangle) { + self.rectangles.push(rectangle); + } + + /// Appends the list of DnD destination rectangles to the current list. + pub fn append(&mut self, other: &mut Vec) { + self.rectangles.append(other); + } + + /// Returns the list of DnD destination rectangles. + /// This consumes the [`DndDestinationRectangles`]. + pub fn into_rectangles(mut self) -> Vec { + self.rectangles.reverse(); + self.rectangles + } +} + +impl AsRef<[DndDestinationRectangle]> for DndDestinationRectangles { + fn as_ref(&self) -> &[DndDestinationRectangle] { + &self.rectangles + } +} diff --git a/core/src/element.rs b/core/src/element.rs index fa07ad69dd..563d7be071 100644 --- a/core/src/element.rs +++ b/core/src/element.rs @@ -1,4 +1,5 @@ use crate::event::{self, Event}; +use crate::id::Id; use crate::layout; use crate::mouse; use crate::overlay; @@ -11,7 +12,7 @@ use crate::{ }; use std::any::Any; -use std::borrow::Borrow; +use std::borrow::{Borrow, BorrowMut}; /// A generic [`Widget`]. /// @@ -261,6 +262,37 @@ impl<'a, Message, Theme, Renderer> } } +impl<'a, Message, Theme, Renderer> + Borrow + 'a> + for &mut Element<'a, Message, Theme, Renderer> +{ + fn borrow(&self) -> &(dyn Widget + 'a) { + self.widget.borrow() + } +} + +impl<'a, Message, Theme, Renderer> + BorrowMut + 'a> + for &mut Element<'a, Message, Theme, Renderer> +{ + fn borrow_mut( + &mut self, + ) -> &mut (dyn Widget + 'a) { + self.widget.borrow_mut() + } +} + +impl<'a, Message, Theme, Renderer> + BorrowMut + 'a> + for Element<'a, Message, Theme, Renderer> +{ + fn borrow_mut( + &mut self, + ) -> &mut (dyn Widget + 'a) { + self.widget.borrow_mut() + } +} + struct Map<'a, A, B, Theme, Renderer> { widget: Box + 'a>, mapper: Box B + 'a>, @@ -300,8 +332,8 @@ where self.widget.children() } - fn diff(&self, tree: &mut Tree) { - self.widget.diff(tree); + fn diff(&mut self, tree: &mut Tree) { + self.widget.diff(tree) } fn size(&self) -> Size { @@ -322,7 +354,9 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn widget::Operation< + widget::OperationOutputWrapper, + >, ) { struct MapOperation<'a, B> { operation: &'a mut dyn widget::Operation, @@ -449,6 +483,35 @@ where .overlay(tree, layout, renderer) .map(move |overlay| overlay.map(mapper)) } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + _layout: Layout<'_>, + _state: &Tree, + _cursor_position: mouse::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); + } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + renderer: &Renderer, + dnd_rectangles: &mut crate::clipboard::DndDestinationRectangles, + ) { + self.widget + .drag_destinations(state, layout, renderer, dnd_rectangles); + } } struct Explain<'a, Message, Theme, Renderer: crate::Renderer> { @@ -489,7 +552,7 @@ where self.element.widget.children() } - fn diff(&self, tree: &mut Tree) { + fn diff(&mut self, tree: &mut Tree) { self.element.widget.diff(tree); } @@ -507,7 +570,9 @@ where state: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn widget::Operation< + widget::OperationOutputWrapper, + >, ) { self.element .widget @@ -591,4 +656,28 @@ 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); + } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + renderer: &Renderer, + dnd_rectangles: &mut crate::clipboard::DndDestinationRectangles, + ) { + self.element.widget.drag_destinations( + state, + layout, + renderer, + dnd_rectangles, + ); + } + // TODO maybe a11y_nodes } diff --git a/core/src/event.rs b/core/src/event.rs index 870b3074e4..5aec00ba9e 100644 --- a/core/src/event.rs +++ b/core/src/event.rs @@ -1,9 +1,14 @@ //! Handle events of a user interface. +use dnd::DndEvent; +use dnd::DndSurface; + 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 +29,26 @@ 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 DnD event. + Dnd(DndEvent), + /// 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..bd771f4b03 --- /dev/null +++ b/core/src/event/wayland/window.rs @@ -0,0 +1,34 @@ +#![allow(missing_docs)] + +use sctk::{ + reexports::csd_frame::{WindowManagerCapabilities, WindowState}, + shell::xdg::window::WindowConfigure, +}; + +/// window events +#[derive(Debug, Clone)] +pub enum WindowEvent { + /// Window manager capabilities. + WmCapabilities(WindowManagerCapabilities), + /// Window state. + State(WindowState), + /// Window configure event. + Configure(WindowConfigure), +} + +impl PartialEq for WindowEvent { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::WmCapabilities(a), Self::WmCapabilities(b)) => a == b, + (Self::State(a), Self::State(b)) => a == b, + (Self::Configure(a), Self::Configure(b)) => { + a.capabilities == b.capabilities + && a.state == b.state + && a.decoration_mode == b.decoration_mode + && a.new_size == b.new_size + && a.suggested_bounds == b.suggested_bounds + } + _ => false, + } + } +} diff --git a/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.rs b/core/src/keyboard/key.rs index dbde51965c..5655a16ed4 100644 --- a/core/src/keyboard/key.rs +++ b/core/src/keyboard/key.rs @@ -7,6 +7,7 @@ use crate::SmolStr; /// /// [`winit`]: https://docs.rs/winit/0.29.10/winit/keyboard/enum.Key.html #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Key { /// A key with an established name. Named(Named), @@ -38,6 +39,7 @@ impl Key { /// /// [`winit`]: https://docs.rs/winit/0.29.10/winit/keyboard/enum.Key.html #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[allow(missing_docs)] pub enum Named { /// The `Alt` (Alternative) key. diff --git a/core/src/keyboard/modifiers.rs b/core/src/keyboard/modifiers.rs index bbdd827270..3670fdaa8e 100644 --- a/core/src/keyboard/modifiers.rs +++ b/core/src/keyboard/modifiers.rs @@ -2,7 +2,7 @@ use bitflags::bitflags; bitflags! { /// The current state of the keyboard modifiers. - #[derive(Default)] + #[derive(Default, Debug, PartialEq, Eq, Clone, Copy)] pub struct Modifiers: u32{ /// The "shift" key. const SHIFT = 0b100; diff --git a/core/src/lib.rs b/core/src/lib.rs index bbc973f053..25d962902f 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -9,12 +9,13 @@ #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] -#![forbid(unsafe_code, rust_2018_idioms)] +// #![forbid(unsafe_code)] #![deny( missing_debug_implementations, missing_docs, unused_results, - rustdoc::broken_intra_doc_links + rustdoc::broken_intra_doc_links, + rust_2018_idioms )] pub mod alignment; pub mod border; @@ -41,6 +42,8 @@ mod color; mod content_fit; mod element; mod hasher; +#[cfg(not(feature = "a11y"))] +pub mod id; mod length; mod padding; mod pixels; @@ -63,6 +66,8 @@ pub use event::Event; pub use font::Font; pub use gradient::Gradient; pub use hasher::Hasher; +#[cfg(feature = "a11y")] +pub use iced_accessibility::id; pub use layout::Layout; pub use length::Length; pub use overlay::Overlay; diff --git a/core/src/overlay.rs b/core/src/overlay.rs index 6b8cf2a66e..e0a9cf2009 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 c34ab86289..e3ef2fd351 100644 --- a/core/src/overlay/element.rs +++ b/core/src/overlay/element.rs @@ -4,7 +4,7 @@ use crate::event::{self, Event}; use crate::layout; use crate::mouse; use crate::renderer; -use crate::widget; +use crate::widget::{self, Operation, OperationOutputWrapper}; use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size, Vector}; use std::any::Any; @@ -121,7 +121,7 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { self.overlay.operate(layout, renderer, operation); } @@ -179,7 +179,7 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { struct MapOperation<'a, B> { operation: &'a mut dyn widget::Operation, diff --git a/core/src/overlay/group.rs b/core/src/overlay/group.rs index 4e54a002c8..9d2ec8afa1 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, }; @@ -140,7 +142,7 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.children.iter_mut().zip(layout.children()).for_each( diff --git a/core/src/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 0af74bb323..529bbdcfe1 100644 --- a/core/src/renderer.rs +++ b/core/src/renderer.rs @@ -54,14 +54,20 @@ impl Default for Quad { /// The styling attributes of a [`Renderer`]. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { + /// The color to apply to symbolic icons. + pub icon_color: Color, /// The text color pub text_color: Color, + /// The scale factor + 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 455daa42f8..2582863fbb 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -42,6 +42,7 @@ impl text::Renderer for Null { type Font = Font; type Paragraph = (); type Editor = (); + type Raw = (); const ICON_FONT: Font = Font::DEFAULT; const CHECKMARK_ICON: char = '0'; @@ -52,7 +53,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]>) {} @@ -75,6 +76,8 @@ impl text::Renderer for Null { ) { } + fn fill_raw(&mut self, _raw: Self::Raw) {} + fn fill_text( &mut self, _paragraph: Text<'_, Self::Font>, diff --git a/core/src/text.rs b/core/src/text.rs index edef79c236..10d0c36252 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -40,6 +40,9 @@ pub struct Text<'a, Font> { /// The [`Shaping`] strategy of the [`Text`]. pub shaping: Shaping, + + /// The [`Wrap`] mode of the [`Text`]. + pub wrap: Wrap, } /// The shaping strategy of some text. @@ -66,6 +69,22 @@ pub enum Shaping { Advanced, } +/// The wrap mode of some text. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub enum Wrap { + /// No wraping + None, + /// Wraps at a glyph level + Glyph, + /// Wraps at a word level + Word, + /// Wraps at the word level, or fallback to glyph level if a word can't fit on a line by itself + /// + /// This is the default + #[default] + WordOrGlyph, +} + /// The height of a line of text in a paragraph. #[derive(Debug, Clone, Copy, PartialEq)] pub enum LineHeight { @@ -88,7 +107,7 @@ impl LineHeight { impl Default for LineHeight { fn default() -> Self { - Self::Relative(1.3) + Self::Relative(1.4) } } @@ -173,6 +192,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 +237,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 d5e2ec6fda..a9f6a58e48 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -3,10 +3,8 @@ pub mod operation; pub mod text; pub mod tree; -mod id; - -pub use id::Id; -pub use operation::Operation; +pub use crate::id::Id; +pub use operation::{Operation, OperationOutputWrapper}; pub use text::Text; pub use tree::Tree; @@ -97,7 +95,7 @@ where } /// Reconciliates the [`Widget`] with the provided [`Tree`]. - fn diff(&self, _tree: &mut Tree) {} + fn diff(&mut self, _tree: &mut Tree) {} /// Applies an [`Operation`] to the [`Widget`]. fn operate( @@ -105,7 +103,7 @@ where _state: &mut Tree, _layout: Layout<'_>, _renderer: &Renderer, - _operation: &mut dyn Operation, + _operation: &mut dyn Operation>, ) { } @@ -149,4 +147,35 @@ 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) {} + + /// Adds the drag destination rectangles of the widget. + /// Runs after the layout phase for each widget in the tree. + fn drag_destinations( + &self, + _state: &Tree, + _layout: Layout<'_>, + _renderer: &Renderer, + _dnd_rectangles: &mut crate::clipboard::DndDestinationRectangles, + ) { + } } 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..158bdf324f 100644 --- a/core/src/widget/operation.rs +++ b/core/src/widget/operation.rs @@ -1,6 +1,7 @@ //! Query or update internal widget state. pub mod focusable; pub mod scrollable; +pub mod search_id; pub mod text_input; pub use focusable::Focusable; @@ -10,9 +11,189 @@ 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(), + } + } + + fn custom(&mut self, _state: &mut dyn Any, _id: Option<&Id>) { + match self { + OperationWrapper::Message(operation) => { + operation.custom(_state, _id); + } + OperationWrapper::Id(operation) => { + operation.custom(_state, _id); + } + OperationWrapper::Wrapper(operation) => { + operation.custom(_state, _id); + } + } + } +} + +#[allow(missing_debug_implementations)] +/// 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) + } + + fn custom(&mut self, state: &mut dyn Any, id: Option<&Id>) { + self.operation.custom(state, id); + } +} /// A piece of logic that can traverse the widget tree of an application in /// order to query or update some widget state. @@ -44,7 +225,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 +234,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 +357,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/operation/search_id.rs b/core/src/widget/operation/search_id.rs new file mode 100644 index 0000000000..b6e330f779 --- /dev/null +++ b/core/src/widget/operation/search_id.rs @@ -0,0 +1,42 @@ +//! Search for widgets with the target Id. + +use super::Operation; +use crate::{id::Id, widget::operation::Outcome, Rectangle}; + +/// Produces an [`Operation`] that searches for the Id +pub fn search_id(target: Id) -> impl Operation { + struct Find { + found: bool, + target: Id, + } + + impl Operation for Find { + fn custom(&mut self, _state: &mut dyn std::any::Any, id: Option<&Id>) { + if Some(&self.target) == id { + self.found = true; + } + } + + fn container( + &mut self, + _id: Option<&Id>, + _bounds: Rectangle, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self); + } + + fn finish(&self) -> Outcome { + if self.found { + Outcome::Some(self.target.clone()) + } else { + Outcome::None + } + } + } + + Find { + found: false, + target, + } +} diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index 0796c4e4bb..c8c73bf8bc 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -11,7 +11,7 @@ use crate::{ use std::borrow::Cow; -pub use text::{LineHeight, Shaping}; +pub use text::{LineHeight, Shaping, Wrap}; /// A paragraph of text. #[allow(missing_debug_implementations)] @@ -20,6 +20,7 @@ where Theme: StyleSheet, Renderer: text::Renderer, { + id: crate::widget::Id, content: Cow<'a, str>, size: Option, line_height: LineHeight, @@ -29,6 +30,7 @@ where vertical_alignment: alignment::Vertical, font: Option, shaping: Shaping, + wrap: Wrap, style: Theme::Style, } @@ -40,6 +42,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(), @@ -48,7 +51,8 @@ where height: Length::Shrink, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, - shaping: Shaping::Basic, + shaping: Shaping::Advanced, + wrap: Default::default(), style: Default::default(), } } @@ -114,6 +118,12 @@ where self.shaping = shaping; self } + + /// Sets the [`Wrap`] mode of the [`Text`]. + pub fn wrap(mut self, wrap: Wrap) -> Self { + self.wrap = wrap; + self + } } /// The internal state of a [`Text`] widget. @@ -160,6 +170,7 @@ where self.horizontal_alignment, self.vertical_alignment, self.shaping, + self.wrap, ) } @@ -184,6 +195,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. @@ -200,6 +255,7 @@ pub fn layout( horizontal_alignment: alignment::Horizontal, vertical_alignment: alignment::Vertical, shaping: Shaping, + wrap: Wrap, ) -> layout::Node where Renderer: text::Renderer, @@ -221,6 +277,7 @@ where horizontal_alignment, vertical_alignment, shaping, + wrap, }); paragraph.min_bounds() @@ -290,6 +347,7 @@ where { fn clone(&self) -> Self { Self { + id: self.id.clone(), content: self.content.clone(), size: self.size, line_height: self.line_height, @@ -300,6 +358,7 @@ where font: self.font, style: self.style.clone(), shaping: self.shaping, + wrap: self.wrap, } } } diff --git a/core/src/widget/tree.rs b/core/src/widget/tree.rs index 6b1a130964..078a64d164 100644 --- a/core/src/widget/tree.rs +++ b/core/src/widget/tree.rs @@ -1,9 +1,16 @@ //! 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::fmt; +use std::borrow::{Borrow, BorrowMut, Cow}; +use std::collections::HashMap; +use std::hash::Hash; +use std::{fmt, mem}; + +thread_local! { + /// A map of named widget states. +pub static NAMED: std::cell::RefCell, (State, Vec<(usize, Tree)>)>> = std::cell::RefCell::new(HashMap::new()); +} /// A persistent state widget tree. /// @@ -13,6 +20,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 +34,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,12 +51,103 @@ impl Tree { let widget = widget.borrow(); Self { + id: widget.id(), tag: widget.tag(), state: widget.state(), children: widget.children(), } } + /// Takes all named widgets from the tree. + pub fn take_all_named( + &mut self, + ) -> HashMap, (State, Vec<(usize, Tree)>)> { + let mut named = HashMap::new(); + struct Visit { + parent: Cow<'static, str>, + index: usize, + visited: bool, + } + // tree traversal to find all named widgets + // and keep their state and children + let mut stack = vec![(self, None)]; + while let Some((tree, visit)) = stack.pop() { + if let Some(Id(Internal::Custom(_, n))) = tree.id.clone() { + let state = mem::replace(&mut tree.state, State::None); + let children_count = tree.children.len(); + let children = + tree.children.iter_mut().enumerate().rev().map(|(i, c)| { + if matches!(c.id, Some(Id(Internal::Custom(_, _)))) { + (c, None) + } else { + ( + c, + Some(Visit { + index: i, + parent: n.clone(), + visited: false, + }), + ) + } + }); + _ = named.insert( + n.clone(), + (state, Vec::with_capacity(children_count)), + ); + stack.extend(children); + } else if let Some(visit) = visit { + if visit.visited { + named.get_mut(&visit.parent).unwrap().1.push(( + visit.index, + mem::replace( + tree, + Tree { + id: tree.id.clone(), + tag: tree.tag, + ..Tree::empty() + }, + ), + )); + } else { + let ptr = tree as *mut Tree; + + stack.push(( + // TODO remove this unsafe block + #[allow(unsafe_code)] + // SAFETY: when the reference is finally accessed, all the children references will have been processed first. + unsafe { + ptr.as_mut().unwrap() + }, + Some(Visit { + visited: true, + ..visit + }), + )); + stack.extend(tree.children.iter_mut().map(|c| (c, None))); + } + } else { + stack.extend(tree.children.iter_mut().map(|s| (s, None))); + } + } + + named + } + + /// Finds a widget state in the tree by its id. + pub fn find<'a>(&'a self, id: &Id) -> Option<&'a Tree> { + if self.id == Some(id.clone()) { + return Some(self); + } + + for child in self.children.iter() { + if let Some(tree) = child.find(id) { + return Some(tree); + } + } + + None + } + /// Reconciliates the current tree with the provided [`Widget`]. /// /// If the tag of the [`Widget`] matches the tag of the [`Tree`], then the @@ -56,53 +158,203 @@ impl Tree { /// [`Widget::diff`]: crate::Widget::diff pub fn diff<'a, Message, Theme, Renderer>( &mut self, - new: impl Borrow + 'a>, + mut new: impl BorrowMut + 'a>, ) where Renderer: crate::Renderer, { - if self.tag == new.borrow().tag() { - new.borrow().diff(self); + let borrowed: &mut dyn Widget = + new.borrow_mut(); + let mut needs_reset = false; + let tag_match = self.tag == borrowed.tag(); + if let Some(Id(Internal::Custom(_, n))) = borrowed.id() { + if let Some((mut state, children)) = NAMED + .with(|named| named.borrow_mut().remove(&n)) + .or_else(|| { + //check self.id + if let Some(Id(Internal::Custom(_, ref name))) = self.id { + if name == &n { + Some(( + mem::replace(&mut self.state, State::None), + self.children + .iter_mut() + .map(|s| { + // take the data + mem::replace( + s, + Tree { + id: s.id.clone(), + tag: s.tag, + ..Tree::empty() + }, + ) + }) + .enumerate() + .collect(), + )) + } else { + None + } + } else { + None + } + }) + { + std::mem::swap(&mut self.state, &mut state); + let widget_children = borrowed.children(); + if !tag_match || self.children.len() != widget_children.len() { + self.children = borrowed.children(); + } else { + for (old_i, mut old) in children { + let Some(my_state) = self.children.get_mut(old_i) + else { + continue; + }; + if my_state.tag != old.tag || { + !match (&old.id, &my_state.id) { + ( + Some(Id(Internal::Custom(_, ref old_name))), + Some(Id(Internal::Custom(_, ref my_name))), + ) => old_name == my_name, + ( + Some(Id(Internal::Set(a))), + Some(Id(Internal::Set(b))), + ) => a.len() == b.len(), + ( + Some(Id(Internal::Unique(_))), + Some(Id(Internal::Unique(_))), + ) => true, + (None, None) => true, + _ => false, + } + } { + continue; + } + + mem::swap(my_state, &mut old); + } + } + } else { + needs_reset = true; + } + } else if tag_match { + if let Some(id) = self.id.clone() { + borrowed.set_id(id); + } + if self.children.len() != borrowed.children().len() { + self.children = borrowed.children(); + } + } else { + needs_reset = true; + } + if needs_reset { + *self = Self::new(borrowed); + let borrowed = new.borrow_mut(); + borrowed.diff(self); } else { - *self = Self::new(new); + borrowed.diff(self); } } /// Reconciles the children of the tree with the provided list of widgets. pub fn diff_children<'a, Message, Theme, Renderer>( &mut self, - new_children: &[impl Borrow + 'a>], + new_children: &mut [impl BorrowMut< + dyn Widget + 'a, + >], ) where Renderer: crate::Renderer, { self.diff_children_custom( new_children, - |tree, widget| tree.diff(widget.borrow()), - |widget| Self::new(widget.borrow()), - ); + new_children.iter().map(|c| c.borrow().id()).collect(), + |tree, widget| { + let borrowed: &mut dyn Widget<_, _, _> = widget.borrow_mut(); + tree.diff(borrowed) + }, + |widget| { + let borrowed: &dyn Widget<_, _, _> = widget.borrow(); + Self::new(borrowed) + }, + ) } /// Reconciliates the children of the tree with the provided list of widgets using custom /// logic both for diffing and creating new widget state. pub fn diff_children_custom( &mut self, - new_children: &[T], - diff: impl Fn(&mut Tree, &T), + new_children: &mut [T], + new_ids: Vec>, + diff: impl Fn(&mut Tree, &mut T), new_state: impl Fn(&T) -> Self, ) { if self.children.len() > new_children.len() { self.children.truncate(new_children.len()); } - for (child_state, new) in - self.children.iter_mut().zip(new_children.iter()) + let len_changed = self.children.len() != new_children.len(); + + let children_len = self.children.len(); + let (mut id_map, mut id_list): ( + HashMap, + Vec<&mut Tree>, + ) = self.children.iter_mut().fold( + (HashMap::new(), Vec::with_capacity(children_len)), + |(mut id_map, mut id_list), c| { + if let Some(id) = c.id.as_ref() { + if 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; + let mut new_trees: Vec<(Tree, usize)> = + Vec::with_capacity(new_children.len()); + for (i, (new, new_id)) in + new_children.iter_mut().zip(new_ids.iter()).enumerate() { + let child_state = if let Some(c) = new_id.as_ref().and_then(|id| { + if let Internal::Custom(_, ref name) = id.0 { + id_map.remove(name.as_ref()) + } else { + None + } + }) { + c + } else if child_state_i < id_list.len() + && !matches!( + id_list[child_state_i].id, + Some(Id(Internal::Custom(_, _))) + ) + { + let c = &mut id_list[child_state_i]; + if len_changed { + c.id.clone_from(new_id); + } + child_state_i += 1; + c + } else { + let mut my_new_state = new_state(new); + diff(&mut my_new_state, new); + new_trees.push((my_new_state, i)); + continue; + }; + diff(child_state, new); } - if self.children.len() < new_children.len() { - self.children.extend( - new_children[self.children.len()..].iter().map(new_state), - ); + for (new_tree, i) in new_trees { + if self.children.len() > i { + self.children[i] = new_tree; + } else { + self.children.push(new_tree); + } } } } @@ -114,8 +366,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 +435,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 dc885728ca..37378d61f6 100644 --- a/examples/editor/Cargo.toml +++ b/examples/editor/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["highlighter", "tokio", "debug"] +iced.features = ["highlighter", "tokio", "debug", "winit"] tokio.workspace = true tokio.features = ["fs"] diff --git a/examples/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..2fe826db45 100644 --- a/examples/integration/Cargo.toml +++ b/examples/integration/Cargo.toml @@ -20,4 +20,4 @@ iced_wgpu.features = ["webgl"] console_error_panic_hook = "0.1" console_log = "1.0" wasm-bindgen = "0.2" -web-sys = { version = "0.3", features = ["Element", "HtmlCanvasElement", "Window", "Document"] } +web-sys = { version = "=0.3", features = ["Element", "HtmlCanvasElement", "Window", "Document"] } diff --git a/examples/integration/src/controls.rs b/examples/integration/src/controls.rs index c9bab8284f..404f3c9264 100644 --- a/examples/integration/src/controls.rs +++ b/examples/integration/src/controls.rs @@ -1,3 +1,4 @@ +use iced_wgpu::core::window::Id; use iced_wgpu::Renderer; use iced_widget::{slider, text_input, Column, Row, Text}; use iced_winit::core::{Alignment, Color, Element, Length}; diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index ed61459f22..5a3fd25672 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -4,6 +4,7 @@ mod scene; use controls::Controls; use scene::Scene; +use iced_wgpu::core::window::Id; use iced_wgpu::graphics::Viewport; use iced_wgpu::{wgpu, Backend, Renderer, Settings}; use iced_winit::conversion; @@ -159,6 +160,7 @@ pub fn main() -> Result<(), Box> { ); let mut state = program::State::new( + Id::MAIN, controls, viewport.logical_size(), &mut renderer, diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index c2a4132c66..628a033332 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -226,7 +226,9 @@ mod modal { use iced::advanced::layout::{self, Layout}; use iced::advanced::overlay; use iced::advanced::renderer; - use iced::advanced::widget::{self, Widget}; + use iced::advanced::widget::{ + self, Operation, OperationOutputWrapper, Widget, + }; use iced::advanced::{self, Clipboard, Shell}; use iced::alignment::Alignment; use iced::event; @@ -276,8 +278,8 @@ mod modal { ] } - fn diff(&self, tree: &mut widget::Tree) { - tree.diff_children(&[&self.base, &self.modal]); + fn diff(&mut self, tree: &mut widget::Tree) { + tree.diff_children(&mut [&mut self.base, &mut self.modal]); } fn size(&self) -> Size { @@ -380,7 +382,7 @@ mod modal { state: &mut widget::Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { self.base.as_widget().operate( &mut state.children[0], @@ -495,7 +497,7 @@ mod modal { &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { self.content.as_widget().operate( self.tree, diff --git a/examples/multi_window/Cargo.toml b/examples/multi_window/Cargo.toml index 2e222dfbb1..f7c25082dd 100644 --- a/examples/multi_window/Cargo.toml +++ b/examples/multi_window/Cargo.toml @@ -6,4 +6,4 @@ edition = "2021" publish = false [dependencies] -iced = { path = "../..", features = ["debug", "multi-window"] } +iced = { path = "../..", features = ["debug", "winit", "multi-window"] } diff --git a/examples/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 ff69191724..d9b43d3dbb 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -1,18 +1,18 @@ -use iced::executor; -use iced::theme; use iced::widget::scrollable::{Properties, Scrollbar, Scroller}; use iced::widget::{ button, column, container, horizontal_space, progress_bar, radio, row, scrollable, slider, text, vertical_space, }; +use iced::{executor, theme}; use iced::{ Alignment, Application, Border, Color, 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..59c97187e5 --- /dev/null +++ b/examples/sctk_drag/src/main.rs @@ -0,0 +1,249 @@ +use iced::{ + event::wayland::DataSourceEvent, + subscription, + wayland::{ + actions::data_device::DataFromMimeType, data_device::start_drag, + }, + wayland::{ + actions::data_device::DndIcon, + data_device::{ + accept_mime_type, finish_dnd, request_dnd_data, set_actions, + }, + layer_surface::destroy_layer_surface, + InitialSurface, + }, + widget::{self, column, container, dnd_listener, dnd_source, text}, + window, Application, Color, Command, Element, Length, Subscription, Theme, +}; +use iced_style::application; +use sctk::reexports::client::protocol::wl_data_device_manager::DndAction; +use sctk::shell::wlr_layer::Anchor; + +fn main() { + let mut settings = iced::Settings::default(); + match &mut settings.initial_surface { + InitialSurface::LayerSurface(s) => { + s.size_limits = s.size_limits.min_width(100.0).max_width(400.0); + s.size = Some((Some(400), None)); + s.anchor = Anchor::TOP.union(Anchor::BOTTOM); + } + _ => {} + }; + DndTest::run(settings).unwrap(); +} + +const SUPPORTED_MIME_TYPES: &'static [&'static str; 6] = &[ + "text/plain;charset=utf-8", + "text/plain;charset=UTF-8", + "UTF8_STRING", + "STRING", + "text/plain", + "TEXT", +]; + +#[derive(Debug, Clone, Default)] +enum DndState { + #[default] + None, + Some(Vec), + Drop, +} + +pub struct MyDndString(String); + +impl DataFromMimeType for MyDndString { + fn from_mime_type(&self, mime_type: &str) -> Option> { + if SUPPORTED_MIME_TYPES.contains(&mime_type) { + Some(self.0.as_bytes().to_vec()) + } else { + None + } + } +} + +#[derive(Debug, Clone, Default)] +pub struct DndTest { + /// option with the dragged text + source: Option, + /// is the dnd over the target + target: DndState, + current_text: String, +} + +#[derive(Debug, Clone)] +pub enum Message { + Enter(Vec), + Leave, + Drop, + DndData(Vec), + Ignore, + StartDnd, + SourceFinished, +} + +impl Application for DndTest { + type Executor = iced::executor::Default; + type Message = Message; + type Flags = (); + type Theme = Theme; + + fn new(_flags: ()) -> (DndTest, Command) { + let current_text = String::from("Hello, world!"); + + ( + DndTest { + current_text, + ..DndTest::default() + }, + Command::none(), + ) + } + + fn title(&self, id: window::Id) -> String { + String::from("DndTest") + } + + fn update(&mut self, message: Self::Message) -> Command { + match message { + Message::Enter(mut mime_types) => { + println!("Enter: {:?}", mime_types); + let mut cmds = + vec![set_actions(DndAction::Copy, DndAction::all())]; + mime_types.retain(|mime_type| { + SUPPORTED_MIME_TYPES.contains(&mime_type.as_str()) + }); + for m in &mime_types { + cmds.push(accept_mime_type(Some(m.clone()))); + } + + self.target = DndState::Some(mime_types); + return Command::batch(cmds); + } + Message::Leave => { + self.target = DndState::None; + return Command::batch(vec![ + accept_mime_type(None), + set_actions(DndAction::None, DndAction::empty()), + ]); + } + Message::Drop => { + if let DndState::Some(m) = &self.target { + let m = m[0].clone(); + println!("Drop: {:?}", self.target); + self.target = DndState::Drop; + return request_dnd_data(m.clone()); + } + } + Message::DndData(data) => { + println!("DndData: {:?}", data); + if data.is_empty() { + return Command::none(); + } + if matches!(self.target, DndState::Drop) { + self.current_text = String::from_utf8(data).unwrap(); + self.target = DndState::None; + // Sent automatically now after a successful read of data following a drop. + // No longer needed here + // return finish_dnd(); + } + } + Message::SourceFinished => { + println!("Removing source"); + self.source = None; + } + Message::StartDnd => { + println!("Starting DnD"); + self.source = Some(self.current_text.clone()); + return start_drag( + SUPPORTED_MIME_TYPES + .iter() + .map(|t| t.to_string()) + .collect(), + DndAction::Move, + window::Id::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 { + ..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_subsurface/Cargo.toml b/examples/sctk_subsurface/Cargo.toml new file mode 100644 index 0000000000..a896a237d5 --- /dev/null +++ b/examples/sctk_subsurface/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "sctk_subsurface" +version = "0.1.0" +edition = "2021" + +[dependencies] +sctk = { package = "smithay-client-toolkit", git = "https://github.com/smithay/client-toolkit", rev = "828b1eb" } +iced = { path = "../..", default-features = false, features = ["wayland", "debug", "a11y"] } +iced_runtime = { path = "../../runtime" } +iced_sctk = { path = "../../sctk" } +env_logger = "0.10" +futures-channel = "0.3.29" +calloop = "0.12.3" +rustix = { version = "0.38.30", features = ["fs", "shm"] } diff --git a/examples/sctk_subsurface/src/main.rs b/examples/sctk_subsurface/src/main.rs new file mode 100644 index 0000000000..34649400e0 --- /dev/null +++ b/examples/sctk_subsurface/src/main.rs @@ -0,0 +1,117 @@ +// Shows a subsurface with a 1x1 px red buffer, stretch to window size + +use iced::{ + event::wayland::Event as WaylandEvent, wayland::InitialSurface, + widget::text, window, Application, Command, Element, Length, Subscription, + Theme, +}; +use iced_sctk::subsurface_widget::SubsurfaceBuffer; +use sctk::reexports::client::{Connection, Proxy}; + +mod wayland; + +fn main() { + let mut settings = iced::Settings::default(); + settings.initial_surface = InitialSurface::XdgWindow(Default::default()); + SubsurfaceApp::run(settings).unwrap(); +} + +#[derive(Debug, Clone, Default)] +struct SubsurfaceApp { + connection: Option, + red_buffer: Option, +} + +#[derive(Debug, Clone)] +pub enum Message { + WaylandEvent(WaylandEvent), + Wayland(wayland::Event), + Pressed(&'static str), +} + +impl Application for SubsurfaceApp { + type Executor = iced::executor::Default; + type Message = Message; + type Flags = (); + type Theme = Theme; + + fn new(_flags: ()) -> (SubsurfaceApp, Command) { + ( + SubsurfaceApp { + ..SubsurfaceApp::default() + }, + Command::none(), + ) + } + + fn title(&self, _id: window::Id) -> String { + String::from("SubsurfaceApp") + } + + fn update(&mut self, message: Self::Message) -> Command { + match message { + Message::WaylandEvent(evt) => match evt { + WaylandEvent::Output(_evt, output) => { + if self.connection.is_none() { + if let Some(backend) = output.backend().upgrade() { + self.connection = + Some(Connection::from_backend(backend)); + } + } + } + _ => {} + }, + Message::Wayland(evt) => match evt { + wayland::Event::RedBuffer(buffer) => { + self.red_buffer = Some(buffer); + } + }, + Message::Pressed(side) => println!("{side} surface pressed"), + } + Command::none() + } + + fn view(&self, _id: window::Id) -> Element { + if let Some(buffer) = &self.red_buffer { + iced::widget::row![ + iced::widget::button( + iced_sctk::subsurface_widget::Subsurface::new(1, 1, buffer) + .width(Length::Fill) + .height(Length::Fill) + ) + .width(Length::Fill) + .height(Length::Fill) + .on_press(Message::Pressed("left")), + iced::widget::button( + iced_sctk::subsurface_widget::Subsurface::new(1, 1, buffer) + .width(Length::Fill) + .height(Length::Fill) + ) + .width(Length::Fill) + .height(Length::Fill) + .on_press(Message::Pressed("right")) + ] + .into() + } else { + text("No subsurface").into() + } + } + + fn subscription(&self) -> Subscription { + let mut subscriptions = vec![iced::event::listen_with(|evt, _| { + if let iced::Event::PlatformSpecific( + iced::event::PlatformSpecific::Wayland(evt), + ) = evt + { + Some(Message::WaylandEvent(evt)) + } else { + None + } + })]; + if let Some(connection) = &self.connection { + subscriptions + .push(wayland::subscription(connection).map(Message::Wayland)); + } + Subscription::batch(subscriptions) + } +} diff --git a/examples/sctk_subsurface/src/wayland.rs b/examples/sctk_subsurface/src/wayland.rs new file mode 100644 index 0000000000..c3ba0b46e1 --- /dev/null +++ b/examples/sctk_subsurface/src/wayland.rs @@ -0,0 +1,125 @@ +use futures_channel::mpsc; +use iced::futures::{FutureExt, SinkExt}; +use iced_sctk::subsurface_widget::{Shmbuf, SubsurfaceBuffer}; +use rustix::{io::Errno, shm::ShmOFlags}; +use sctk::{ + reexports::{ + calloop_wayland_source::WaylandSource, + client::{ + delegate_noop, + globals::registry_queue_init, + protocol::{wl_buffer::WlBuffer, wl_shm}, + Connection, + }, + }, + registry::{ProvidesRegistryState, RegistryState}, + shm::{Shm, ShmHandler}, +}; +use std::{ + os::fd::OwnedFd, + sync::Arc, + thread, + time::{SystemTime, UNIX_EPOCH}, +}; + +#[derive(Debug, Clone)] +pub enum Event { + RedBuffer(SubsurfaceBuffer), +} + +struct AppData { + registry_state: RegistryState, + shm_state: Shm, +} + +impl ProvidesRegistryState for AppData { + fn registry(&mut self) -> &mut RegistryState { + &mut self.registry_state + } + + sctk::registry_handlers!(); +} + +impl ShmHandler for AppData { + fn shm_state(&mut self) -> &mut Shm { + &mut self.shm_state + } +} + +pub fn subscription(connection: &Connection) -> iced::Subscription { + let connection = connection.clone(); + iced::subscription::run_with_id( + "wayland-sub", + async { start(connection).await }.flatten_stream(), + ) +} + +async fn start(conn: Connection) -> mpsc::Receiver { + let (mut sender, receiver) = mpsc::channel(20); + + let (globals, event_queue) = registry_queue_init(&conn).unwrap(); + let qh = event_queue.handle(); + + let mut app_data = AppData { + registry_state: RegistryState::new(&globals), + shm_state: Shm::bind(&globals, &qh).unwrap(), + }; + + let fd = create_memfile().unwrap(); + rustix::io::write(&fd, &[0, 0, 255, 255]).unwrap(); + + let shmbuf = Shmbuf { + fd, + offset: 0, + width: 1, + height: 1, + stride: 4, + format: wl_shm::Format::Xrgb8888, + }; + + let buffer = SubsurfaceBuffer::new(Arc::new(shmbuf.into())).0; + let _ = sender.send(Event::RedBuffer(buffer)).await; + + thread::spawn(move || { + let mut event_loop = calloop::EventLoop::try_new().unwrap(); + WaylandSource::new(conn, event_queue) + .insert(event_loop.handle()) + .unwrap(); + loop { + event_loop.dispatch(None, &mut app_data).unwrap(); + } + }); + + receiver +} + +fn create_memfile() -> rustix::io::Result { + loop { + let flags = ShmOFlags::CREATE | ShmOFlags::EXCL | ShmOFlags::RDWR; + + let time = SystemTime::now(); + let name = format!( + "/iced-sctk-{}", + time.duration_since(UNIX_EPOCH).unwrap().subsec_nanos() + ); + + match rustix::io::retry_on_intr(|| { + rustix::shm::shm_open(&name, flags, 0600.into()) + }) { + Ok(fd) => match rustix::shm::shm_unlink(&name) { + Ok(_) => return Ok(fd), + Err(errno) => { + return Err(errno.into()); + } + }, + Err(Errno::EXIST) => { + continue; + } + Err(err) => return Err(err.into()), + } + } +} + +delegate_noop!(AppData: ignore WlBuffer); +sctk::delegate_registry!(AppData); +sctk::delegate_shm!(AppData); diff --git a/examples/sctk_subsurface_gst/Cargo.toml b/examples/sctk_subsurface_gst/Cargo.toml new file mode 100644 index 0000000000..f83edd45a0 --- /dev/null +++ b/examples/sctk_subsurface_gst/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "sctk_subsurface_gst" +version = "0.1.0" +edition = "2021" + +[dependencies] +sctk = { package = "smithay-client-toolkit", git = "https://github.com/smithay/client-toolkit", rev = "828b1eb" } +iced = { path = "../..", default-features = false, features = ["wayland", "debug", "a11y"] } +iced_runtime = { path = "../../runtime" } +iced_sctk = { path = "../../sctk" } +env_logger = "0.10" +futures-channel = "0.3.29" +calloop = "0.12.3" +gst = { package = "gstreamer", version = "0.21.3" } +gst-app = { package = "gstreamer-app", version = "0.21.2" } +gst-video = { package = "gstreamer-video", version = "0.21.2" } +gst-allocators = { package = "gstreamer-allocators", version = "0.21.2" } +drm-fourcc = "2.2.0" diff --git a/examples/sctk_subsurface_gst/src/main.rs b/examples/sctk_subsurface_gst/src/main.rs new file mode 100644 index 0000000000..09119117bb --- /dev/null +++ b/examples/sctk_subsurface_gst/src/main.rs @@ -0,0 +1,84 @@ +// Shows a subsurface with a 1x1 px red buffer, stretch to window size + +use iced::{ + wayland::InitialSurface, widget::text, window, Application, Command, + Element, Length, Subscription, Theme, +}; +use iced_sctk::subsurface_widget::SubsurfaceBuffer; +use std::{env, path::Path}; + +mod pipewire; + +fn main() { + let args = env::args(); + if args.len() != 2 { + eprintln!("usage: sctk_subsurface_gst [h264 mp4 path]"); + return; + } + let path = args.skip(1).next().unwrap(); + if !Path::new(&path).exists() { + eprintln!("File `{path}` not found."); + return; + } + let mut settings = iced::Settings::with_flags(path); + settings.initial_surface = InitialSurface::XdgWindow(Default::default()); + SubsurfaceApp::run(settings).unwrap(); +} + +#[derive(Debug, Clone, Default)] +struct SubsurfaceApp { + path: String, + buffer: Option, +} + +#[derive(Debug, Clone)] +pub enum Message { + Pipewire(pipewire::Event), +} + +impl Application for SubsurfaceApp { + type Executor = iced::executor::Default; + type Message = Message; + type Flags = String; + type Theme = Theme; + + fn new(flags: String) -> (SubsurfaceApp, Command) { + ( + SubsurfaceApp { + path: flags, + ..SubsurfaceApp::default() + }, + Command::none(), + ) + } + + fn title(&self, _id: window::Id) -> String { + String::from("SubsurfaceApp") + } + + fn update(&mut self, message: Self::Message) -> Command { + match message { + Message::Pipewire(evt) => match evt { + pipewire::Event::Frame(subsurface_buffer) => { + self.buffer = Some(subsurface_buffer); + } + }, + } + Command::none() + } + + fn view(&self, _id: window::Id) -> Element { + if let Some(buffer) = &self.buffer { + iced_sctk::subsurface_widget::Subsurface::new(1, 1, buffer) + .width(Length::Fill) + .height(Length::Fill) + .into() + } else { + text("No subsurface").into() + } + } + + fn subscription(&self) -> Subscription { + pipewire::subscription(&self.path).map(Message::Pipewire) + } +} diff --git a/examples/sctk_subsurface_gst/src/pipewire.rs b/examples/sctk_subsurface_gst/src/pipewire.rs new file mode 100644 index 0000000000..9933ee099d --- /dev/null +++ b/examples/sctk_subsurface_gst/src/pipewire.rs @@ -0,0 +1,185 @@ +use drm_fourcc::{DrmFourcc, DrmModifier}; +use gst::glib::{self, translate::IntoGlib}; +use gst::prelude::*; +use iced::futures::{executor::block_on, SinkExt}; +use iced_sctk::subsurface_widget::{ + BufferSource, Dmabuf, Plane, SubsurfaceBuffer, +}; +use std::{ffi::c_void, os::unix::io::BorrowedFd, sync::Arc, thread}; + +const USE_NV12: bool = false; + +// Store a reference to the `BufferSource` in the data assocaited with the `BufferRef`. +// So the `BufferSource` can be re-used, instead of dupping fds and creating a new +// `wl_buffer` each buffer swap. +// +// See https://gitlab.freedesktop.org/gstreamer/gstreamer/-/blob/main/subprojects/gst-plugins-bad/gst-libs/gst/wayland/gstwlbuffer.c +// for information about how `waylandsink` does this. +fn get_buffer_source(buffer: &gst::BufferRef) -> Option> { + let buffer_source_quark = glib::Quark::from_str("SctkBufferSource"); + unsafe { + let data = gst::ffi::gst_mini_object_get_qdata( + buffer.upcast_ref().as_mut_ptr(), + buffer_source_quark.into_glib(), + ); + if data.is_null() { + None + } else { + Arc::increment_strong_count(data as *const BufferSource); + Some(Arc::from_raw(data as *const BufferSource)) + } + } +} + +fn set_buffer_source(buffer: &gst::BufferRef, source: Arc) { + let buffer_source_quark = glib::Quark::from_str("SctkBufferSource"); + unsafe extern "C" fn destroy_buffer_source(data: *mut c_void) { + Arc::from_raw(data); + } + unsafe { + gst::ffi::gst_mini_object_set_qdata( + buffer.upcast_ref().as_mut_ptr(), + buffer_source_quark.into_glib(), + Arc::into_raw(source) as *mut c_void, + Some(destroy_buffer_source), + ); + } +} + +#[derive(Debug, Clone)] +pub enum Event { + Frame(SubsurfaceBuffer), +} + +pub fn subscription(path: &str) -> iced::Subscription { + let path = path.to_string(); + iced::subscription::channel("pw", 16, |sender| async { + thread::spawn(move || pipewire_thread(&path, sender)); + std::future::pending().await + }) +} + +fn pipewire_thread( + path: &str, + mut sender: futures_channel::mpsc::Sender, +) { + gst::init().unwrap(); + + let pipeline = gst::parse_launch(&format!( + "filesrc name=filesrc ! + qtdemux ! + h264parse ! + vah264dec ! + vapostproc name=postproc ! + capsfilter name=capfilter ! + appsink name=sink", + )) + .unwrap() + .dynamic_cast::() + .unwrap(); + pipeline + .by_name("filesrc") + .unwrap() + .set_property("location", path); + + let format = if USE_NV12 { + /* + pipeline + .remove(&pipeline.by_name("postproc").unwrap()) + .unwrap(); + */ + gst_video::VideoFormat::Nv12 + } else { + gst_video::VideoFormat::Bgra + }; + pipeline.by_name("capfilter").unwrap().set_property( + "caps", + gst_video::VideoCapsBuilder::new() + .features(["memory:DMABuf"]) + .format(format) + .build(), + ); + + let appsink = pipeline + .by_name("sink") + .unwrap() + .dynamic_cast::() + .unwrap(); + + let mut subsurface_release = None; + + appsink.set_callbacks( + gst_app::AppSinkCallbacks::builder() + .new_sample(move |appsink| { + let sample = + appsink.pull_sample().map_err(|_| gst::FlowError::Eos)?; + + let buffer = sample.buffer().unwrap(); + let meta = buffer.meta::().unwrap(); + + let buffer_source = if let Some(buffer_source) = get_buffer_source(buffer) { + buffer_source + } else { + let planes = (0..meta.n_planes()) + .map(|plane_idx| { + let memory = buffer + .memory(plane_idx) + .unwrap() + .downcast_memory::() + .unwrap(); + + // TODO avoid dup? + let fd = unsafe { BorrowedFd::borrow_raw(memory.fd()) } + .try_clone_to_owned() + .unwrap(); + + Plane { + fd, + plane_idx, + offset: meta.offset()[plane_idx as usize] as u32, + stride: meta.stride()[plane_idx as usize] as u32, + } + }) + .collect(); + + let format = if USE_NV12 { + DrmFourcc::Nv12 + } else { + DrmFourcc::Argb8888 + }; + let dmabuf = Dmabuf { + width: meta.width() as i32, + height: meta.height() as i32, + planes, + // TODO should use dmabuf protocol to get supported formats, + // convert if needed. + format: format as u32, + // TODO modifier negotiation + modifier: DrmModifier::Linear.into(), + }; + + let buffer_source = Arc::new(BufferSource::from(dmabuf)); + set_buffer_source(buffer, buffer_source.clone()); + buffer_source + }; + + let (buffer, new_subsurface_release) = + SubsurfaceBuffer::new(buffer_source); + block_on(sender.send(Event::Frame(buffer))).unwrap(); + + // Wait for server to release other buffer + // TODO is gstreamer using triple buffering? + if let Some(release) = subsurface_release.take() { + block_on(release); + } + subsurface_release = Some(new_subsurface_release); + + Ok(gst::FlowSuccess::Ok) + }) + .build(), + ); + + pipeline.set_state(gst::State::Playing).unwrap(); + let bus = pipeline.bus().unwrap(); + for _msg in bus.iter_timed(gst::ClockTime::NONE) {} +} diff --git a/examples/sctk_todos/Cargo.toml b/examples/sctk_todos/Cargo.toml new file mode 100644 index 0000000000..b21a9c96d7 --- /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", "a11y", "wayland-clipboard"] } +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..f6d95134e6 --- /dev/null +++ b/examples/sctk_todos/src/main.rs @@ -0,0 +1,656 @@ +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::keyboard::key::Named; +use iced_core::layout::Limits; +use iced_core::{id, keyboard}; + +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; + +static INPUT_ID: Lazy = Lazy::new(Id::unique); + +pub fn main() -> iced::Result { + let env = Env::default() + .filter_or("MY_LOG_LEVEL", "info") + .write_style_or("MY_LOG_STYLE", "always"); + + let mut settings = SctkWindowSettings::default(); + settings.size_limits = Limits::NONE.min_height(300.0).min_width(600.0); + env_logger::init_from_env(env); + Todos::run(Settings { + initial_surface: InitialSurface::XdgWindow(settings), + ..Settings::default() + }) +} + +#[derive(Debug)] +enum Todos { + Loading, + Loaded(State), +} + +#[derive(Debug, Default)] +struct State { + window_id_ctr: u128, + input_value: String, + filter: Filter, + tasks: Vec, + dirty: bool, + saving: bool, +} + +#[derive(Clone)] +enum Message { + Loaded(Result), + Saved(Result<(), SaveError>), + InputChanged(String), + CreateTask, + FilterChanged(Filter), + TaskMessage(usize, TaskMessage), + TabPressed { shift: bool }, + CloseRequested(window::Id), + Ignore, +} + +impl Debug for Message { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Message::Loaded(_) => write!(f, "Message::Loaded(_)"), + Message::Saved(_) => write!(f, "Message::Saved(_)"), + Message::InputChanged(_) => write!(f, "Message::InputChanged(_)"), + Message::CreateTask => write!(f, "Message::CreateTask"), + Message::FilterChanged(_) => write!(f, "Message::FilterChanged(_)"), + Message::TaskMessage(_, _) => { + write!(f, "Message::TaskMessage(_, _)") + } + Message::TabPressed { shift: _ } => { + write!(f, "Message::TabPressed {{ shift: _ }}") + } + Message::CloseRequested(_) => { + write!(f, "Message::CloseRequested(_)") + } + + Message::Ignore => write!(f, "Message::Ignore"), + } + } +} + +impl Application for Todos { + type Message = Message; + type Theme = Theme; + type Executor = iced::executor::Default; + type Flags = (); + + fn new(_flags: ()) -> (Todos, Command) { + ( + Todos::Loading, + Command::batch(vec![Command::perform( + SavedState::load(), + Message::Loaded, + )]), + ) + } + + fn title(&self, _id: window::Id) -> String { + let dirty = match self { + Todos::Loading => false, + Todos::Loaded(state) => state.dirty, + }; + + format!("Todos{} - Iced", if dirty { "*" } else { "" }) + } + + fn update(&mut self, message: Message) -> Command { + match self { + Todos::Loading => { + match message { + Message::Loaded(Ok(state)) => { + *self = Todos::Loaded(State { + input_value: state.input_value, + filter: state.filter, + tasks: state.tasks, + window_id_ctr: 1, + ..State::default() + }); + } + Message::Loaded(Err(_)) => { + *self = Todos::Loaded(State::default()); + } + _ => {} + } + + text_input::focus(INPUT_ID.clone()) + } + Todos::Loaded(state) => { + let mut saved = false; + + let command = match message { + Message::InputChanged(value) => { + state.input_value = value; + + Command::none() + } + Message::CreateTask => { + if !state.input_value.is_empty() { + state + .tasks + .push(Task::new(state.input_value.clone())); + state.input_value.clear(); + } + Command::none() + } + Message::FilterChanged(filter) => { + state.filter = filter; + + Command::none() + } + Message::TaskMessage(i, TaskMessage::Delete) => { + state.tasks.remove(i); + + Command::none() + } + Message::TaskMessage(i, task_message) => { + if let Some(task) = state.tasks.get_mut(i) { + let should_focus = + matches!(task_message, TaskMessage::Edit); + + task.update(task_message); + + if should_focus { + let id = Task::text_input_id(i); + Command::batch(vec![ + text_input::focus(id.clone()), + text_input::select_all(id), + ]) + } else { + Command::none() + } + } else { + Command::none() + } + } + Message::Saved(_) => { + state.saving = false; + saved = true; + + Command::none() + } + Message::TabPressed { shift } => { + if shift { + widget::focus_previous() + } else { + widget::focus_next() + } + } + Message::CloseRequested(_) => { + std::process::exit(0); + } + _ => Command::none(), + }; + + if !saved { + state.dirty = true; + } + + let save = if state.dirty && !state.saving { + state.dirty = false; + state.saving = true; + + Command::perform( + SavedState { + input_value: state.input_value.clone(), + filter: state.filter, + tasks: state.tasks.clone(), + } + .save(), + Message::Saved, + ) + } else { + Command::none() + }; + + Command::batch(vec![command, save]) + } + } + } + + fn view(&self, id: window::Id) -> Element { + match self { + Todos::Loading => loading_message(), + Todos::Loaded(State { + input_value, + filter, + tasks, + window_id_ctr, + .. + }) => { + if iced::window::Id::MAIN != id { + panic!("Wrong window id: {:?}", id) + } + + let title = text("todos") + .width(Length::Fill) + .size(100) + .style(Color::from([0.5, 0.5, 0.5])) + .horizontal_alignment(alignment::Horizontal::Center); + + let input = text_input("What needs to be done?", input_value) + .id(INPUT_ID.clone()) + .padding(15) + .size(30) + .on_submit(Message::CreateTask) + .on_input(Message::InputChanged) + .on_paste(Message::InputChanged); + + let controls = view_controls(tasks, *filter); + let filtered_tasks = + tasks.iter().filter(|task| filter.matches(task)); + + let tasks: Element<_> = if filtered_tasks.count() > 0 { + column( + tasks + .iter() + .enumerate() + .filter(|(_, task)| filter.matches(task)) + .map(|(i, task)| { + task.view(i).map(move |message| { + Message::TaskMessage(i, message) + }) + }) + .collect::>(), + ) + .spacing(10) + .into() + } else { + empty_message(match filter { + Filter::All => "You have not created a task yet...", + Filter::Active => "All your tasks are done! :D", + Filter::Completed => { + "You have not completed a task yet..." + } + }) + }; + + let content = column![title, input, controls, tasks] + .spacing(20) + .max_width(800); + + scrollable( + container(content) + .width(Length::Fill) + .padding(40) + .center_x(), + ) + .into() + } + } + } + + fn subscription(&self) -> Subscription { + listen_raw(|event, status| match (event, status) { + ( + Event::Keyboard(keyboard::Event::KeyPressed { + key: keyboard::Key::Named(Named::Tab), + modifiers, + .. + }), + event::Status::Ignored, + ) => Some(Message::TabPressed { + shift: modifiers.shift(), + }), + ( + Event::PlatformSpecific(event::PlatformSpecific::Wayland( + event::wayland::Event::Window(e, s, id), + )), + _, + ) => { + dbg!(&e); + None + } + _ => None, + }) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct Task { + description: String, + completed: bool, + + #[serde(skip)] + state: TaskState, +} + +#[derive(Debug, Clone)] +pub enum TaskState { + Idle, + Editing, +} + +impl Default for TaskState { + fn default() -> Self { + Self::Idle + } +} + +#[derive(Debug, Clone)] +pub enum TaskMessage { + Completed(bool), + Edit, + DescriptionEdited(String), + FinishEdition, + Delete, +} + +impl Task { + fn text_input_id(i: usize) -> id::Id { + id::Id::new(format!("task-{}", i)) + } + + fn new(description: String) -> Self { + Task { + description, + completed: false, + state: TaskState::Idle, + } + } + + fn update(&mut self, message: TaskMessage) { + match message { + TaskMessage::Completed(completed) => { + self.completed = completed; + } + TaskMessage::Edit => { + self.state = TaskState::Editing; + } + TaskMessage::DescriptionEdited(new_description) => { + self.description = new_description; + } + TaskMessage::FinishEdition => { + if !self.description.is_empty() { + self.state = TaskState::Idle; + } + } + TaskMessage::Delete => {} + } + } + + fn view(&self, i: usize) -> Element { + match &self.state { + TaskState::Idle => { + let checkbox = checkbox( + &self.description, + self.completed, + TaskMessage::Completed, + ) + .width(Length::Fill); + + row![ + checkbox, + button(edit_icon()) + .on_press(TaskMessage::Edit) + .padding(10) + .style(theme::Button::Text), + ] + .spacing(20) + .align_items(Alignment::Center) + .into() + } + TaskState::Editing => { + let text_input = + text_input("Describe your task...", &self.description) + .id(Self::text_input_id(i)) + .on_submit(TaskMessage::FinishEdition) + .on_input(TaskMessage::DescriptionEdited) + .on_paste(TaskMessage::DescriptionEdited) + .padding(10); + + row![ + text_input, + button(row![delete_icon(), "Delete"].spacing(10)) + .on_press(TaskMessage::Delete) + .padding(10) + .style(theme::Button::Destructive) + ] + .spacing(20) + .align_items(Alignment::Center) + .into() + } + } + } +} + +fn view_controls(tasks: &[Task], current_filter: Filter) -> Element { + let tasks_left = tasks.iter().filter(|task| !task.completed).count(); + + let filter_button = |label, filter, current_filter| { + let label = text(label).size(16); + + let button = button(label).style(if filter == current_filter { + theme::Button::Primary + } else { + theme::Button::Text + }); + + button.on_press(Message::FilterChanged(filter)).padding(8) + }; + + row![ + text(format!( + "{} {} left", + tasks_left, + if tasks_left == 1 { "task" } else { "tasks" } + )) + .width(Length::Fill) + .size(16), + row![ + filter_button("All", Filter::All, current_filter), + filter_button("Active", Filter::Active, current_filter), + filter_button("Completed", Filter::Completed, current_filter,), + ] + .width(Length::Shrink) + .spacing(10) + ] + .spacing(20) + .align_items(Alignment::Center) + .into() +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum Filter { + All, + Active, + Completed, +} + +impl Default for Filter { + fn default() -> Self { + Filter::All + } +} + +impl Filter { + fn matches(&self, task: &Task) -> bool { + match self { + Filter::All => true, + Filter::Active => !task.completed, + Filter::Completed => task.completed, + } + } +} + +fn loading_message<'a>() -> Element<'a, Message> { + container( + text("Loading...") + .horizontal_alignment(alignment::Horizontal::Center) + .size(50), + ) + .width(Length::Fill) + .height(Length::Fill) + .center_y() + .into() +} + +fn empty_message(message: &str) -> Element<'_, Message> { + container( + text(message) + .width(Length::Fill) + .size(25) + .horizontal_alignment(alignment::Horizontal::Center) + .style(Color::from([0.7, 0.7, 0.7])), + ) + .width(Length::Fill) + .height(Length::Fixed(200.0)) + .center_y() + .into() +} + +// Fonts +const ICONS: Font = Font::with_name("Iced-Todos-Icons"); + +fn icon(unicode: char) -> Text<'static> { + text(unicode.to_string()) + .font(ICONS) + .width(Length::Fixed(20.0)) + .horizontal_alignment(alignment::Horizontal::Center) + .size(20) +} + +fn edit_icon() -> Text<'static> { + icon('\u{F303}') +} + +fn delete_icon() -> Text<'static> { + icon('\u{F1F8}') +} + +// Persistence +#[derive(Debug, Clone, Serialize, Deserialize)] +struct SavedState { + input_value: String, + filter: Filter, + tasks: Vec, +} + +#[derive(Debug, Clone)] +enum LoadError { + File, + Format, +} + +#[derive(Debug, Clone)] +enum SaveError { + File, + Write, + Format, +} + +#[cfg(not(target_arch = "wasm32"))] +impl SavedState { + fn path() -> std::path::PathBuf { + let mut path = if let Some(project_dirs) = + directories_next::ProjectDirs::from("rs", "Iced", "Todos") + { + project_dirs.data_dir().into() + } else { + std::env::current_dir().unwrap_or_default() + }; + + path.push("todos.json"); + + path + } + + async fn load() -> Result { + use async_std::prelude::*; + + let mut contents = String::new(); + + let mut file = async_std::fs::File::open(Self::path()) + .await + .map_err(|_| LoadError::File)?; + + file.read_to_string(&mut contents) + .await + .map_err(|_| LoadError::File)?; + + serde_json::from_str(&contents).map_err(|_| LoadError::Format) + } + + async fn save(self) -> Result<(), SaveError> { + use async_std::prelude::*; + + let json = serde_json::to_string_pretty(&self) + .map_err(|_| SaveError::Format)?; + + let path = Self::path(); + + if let Some(dir) = path.parent() { + async_std::fs::create_dir_all(dir) + .await + .map_err(|_| SaveError::File)?; + } + + { + let mut file = async_std::fs::File::create(path) + .await + .map_err(|_| SaveError::File)?; + + file.write_all(json.as_bytes()) + .await + .map_err(|_| SaveError::Write)?; + } + + // This is a simple way to save at most once every couple seconds + async_std::task::sleep(std::time::Duration::from_secs(2)).await; + + Ok(()) + } +} + +#[cfg(target_arch = "wasm32")] +impl SavedState { + fn storage() -> Option { + let window = web_sys::window()?; + + window.local_storage().ok()? + } + + async fn load() -> Result { + let storage = Self::storage().ok_or(LoadError::File)?; + + let contents = storage + .get_item("state") + .map_err(|_| LoadError::File)? + .ok_or(LoadError::File)?; + + serde_json::from_str(&contents).map_err(|_| LoadError::Format) + } + + async fn save(self) -> Result<(), SaveError> { + let storage = Self::storage().ok_or(SaveError::File)?; + + let json = serde_json::to_string_pretty(&self) + .map_err(|_| SaveError::Format)?; + + storage + .set_item("state", &json) + .map_err(|_| SaveError::Write)?; + + let _ = wasm_timer::Delay::new(std::time::Duration::from_secs(2)).await; + + Ok(()) + } +} diff --git a/examples/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 cc9875d94d..e9de65a8ea 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -180,7 +180,9 @@ mod toast { use iced::advanced::layout::{self, Layout}; use iced::advanced::overlay; use iced::advanced::renderer; - use iced::advanced::widget::{self, Operation, Tree}; + use iced::advanced::widget::{ + self, Operation, OperationOutputWrapper, Tree, + }; use iced::advanced::{Clipboard, Shell, Widget}; use iced::event::{self, Event}; use iced::mouse; @@ -346,7 +348,7 @@ mod toast { .collect() } - fn diff(&self, tree: &mut Tree) { + fn diff(&mut self, tree: &mut Tree) { let instants = tree.state.downcast_mut::>>(); // Invalidating removed instants to None allows us to remove @@ -367,8 +369,8 @@ mod toast { } tree.diff_children( - &std::iter::once(&self.content) - .chain(self.toasts.iter()) + &mut std::iter::once(&mut self.content) + .chain(self.toasts.iter_mut()) .collect::>(), ); } @@ -378,7 +380,7 @@ mod toast { state: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.content.as_widget().operate( @@ -623,7 +625,7 @@ mod toast { &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.toasts diff --git a/examples/todos/Cargo.toml b/examples/todos/Cargo.toml index 3c62bfbc8f..6728c1395e 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", "a11y"] once_cell.workspace = true serde = { version = "1.0", features = ["derive"] } @@ -29,6 +30,14 @@ wasm-timer.workspace = true [package.metadata.deb] assets = [ - ["target/release-opt/todos", "usr/bin/iced-todos", "755"], - ["iced-todos.desktop", "usr/share/applications/", "644"], + [ + "target/release-opt/todos", + "usr/bin/iced-todos", + "755", + ], + [ + "iced-todos.desktop", + "usr/share/applications/", + "644", + ], ] diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 3d79f08789..2289fb7385 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -1,6 +1,7 @@ use iced::alignment::{self, Alignment}; use iced::font::{self, Font}; -use iced::keyboard; +use iced::keyboard::{self, Modifiers}; +use iced::subscription; use iced::theme::{self, Theme}; use iced::widget::{ self, button, checkbox, column, container, keyed_column, row, scrollable, @@ -9,12 +10,13 @@ use iced::widget::{ use iced::window; use iced::{Application, Element}; use iced::{Color, Command, Length, Settings, Size, Subscription}; +use iced_core::widget::Id; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; use uuid::Uuid; -static INPUT_ID: Lazy = Lazy::new(text_input::Id::unique); +static INPUT_ID: Lazy = Lazy::new(Id::unique); pub fn main() -> iced::Result { #[cfg(not(target_arch = "wasm32"))] @@ -249,8 +251,9 @@ impl Application for Todos { } }) }; + let test = row![container(text("0000 0000 00000 000000000000 000000000000000 00000 0000 00000000 000000 000000000 l00000")).width(Length::Fill), container(text("a")).width(Length::Fixed(100.0))]; - let content = column![title, input, controls, tasks] + let content = column![title, input, controls, tasks, test] .spacing(20) .max_width(800); @@ -316,8 +319,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 38a6db1e14..e837e8dd12 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -13,6 +13,7 @@ use once_cell::sync::Lazy; pub fn main() -> iced::Result { WebSocket::run(Settings::default()) } +use iced::id::Id; #[derive(Default)] struct WebSocket { @@ -161,4 +162,4 @@ impl Default for State { } } -static MESSAGE_LOG: Lazy = Lazy::new(scrollable::Id::unique); +static MESSAGE_LOG: Lazy = Lazy::new(|| Id::new("message_log")); diff --git a/futures/Cargo.toml b/futures/Cargo.toml index 69a915e43b..d928228dd6 100644 --- a/futures/Cargo.toml +++ b/futures/Cargo.toml @@ -16,6 +16,7 @@ all-features = true [features] thread-pool = ["futures/thread-pool"] +a11y = ["iced_core/a11y"] [dependencies] iced_core.workspace = true diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs index 7163248dc8..4d5a119293 100644 --- a/futures/src/subscription.rs +++ b/futures/src/subscription.rs @@ -10,6 +10,7 @@ use crate::{BoxStream, MaybeSend}; use futures::channel::mpsc; use futures::never::Never; +use std::any::TypeId; use std::hash::Hash; /// A stream of runtime events. @@ -88,7 +89,10 @@ impl Subscription { } /// Transforms the [`Subscription`] output with the given function. - pub fn map(mut self, f: fn(Message) -> A) -> Subscription + pub fn map( + mut self, + f: impl Fn(Message) -> A + MaybeSend + Clone + 'static, + ) -> Subscription where Message: 'static, A: 'static, @@ -97,8 +101,9 @@ impl Subscription { recipes: self .recipes .drain(..) - .map(|recipe| { - Box::new(Map::new(recipe, f)) as Box> + .map(move |recipe| { + Box::new(Map::new(recipe, f.clone())) + as Box> }) .collect(), } @@ -143,27 +148,39 @@ pub trait Recipe { fn stream(self: Box, input: EventStream) -> BoxStream; } -struct Map { +struct Map +where + F: Fn(A) -> B + 'static, +{ + id: TypeId, recipe: Box>, - mapper: fn(A) -> B, + mapper: F, } -impl Map { - fn new(recipe: Box>, mapper: fn(A) -> B) -> Self { - Map { recipe, mapper } +impl Map +where + F: Fn(A) -> B + 'static, +{ + fn new(recipe: Box>, mapper: F) -> Self { + Map { + id: TypeId::of::(), + recipe, + mapper, + } } } -impl Recipe for Map +impl Recipe for Map where A: 'static, B: 'static, + F: Fn(A) -> B + 'static + MaybeSend, { type Output = B; fn hash(&self, state: &mut Hasher) { + self.id.hash(state); self.recipe.hash(state); - self.mapper.hash(state); } fn stream(self: Box, input: EventStream) -> BoxStream { diff --git a/graphics/src/compositor.rs b/graphics/src/compositor.rs index 0188f4d8b2..030ff27f2a 100644 --- a/graphics/src/compositor.rs +++ b/graphics/src/compositor.rs @@ -111,6 +111,15 @@ pub enum SurfaceError { /// There is no more memory left to allocate a new frame. #[error("There is no more memory left to allocate a new frame")] OutOfMemory, + /// Resize Error + #[error("Resize Error")] + Resize, + /// Invalid dimensions + #[error("Invalid dimensions")] + InvalidDimensions, + /// Present Error + #[error("Present Error")] + Present(String), } /// Contains information about the graphics (e.g. graphics adapter, graphics backend). diff --git a/graphics/src/geometry/text.rs b/graphics/src/geometry/text.rs index d314e85ec9..9cadf71763 100644 --- a/graphics/src/geometry/text.rs +++ b/graphics/src/geometry/text.rs @@ -43,6 +43,7 @@ impl Text { let mut buffer = cosmic_text::BufferLine::new( &self.content, + cosmic_text::LineEnding::default(), cosmic_text::AttrsList::new(text::to_attributes(self.font)), text::to_shaping(self.shaping), ); @@ -50,8 +51,10 @@ impl Text { let layout = buffer.layout( font_system.raw(), self.size.0, - f32::MAX, + None, cosmic_text::Wrap::None, + None, + 8, ); let translation_x = match self.horizontal_alignment { @@ -171,12 +174,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/image.rs b/graphics/src/image.rs index d89caacecb..2a4ed2bb3b 100644 --- a/graphics/src/image.rs +++ b/graphics/src/image.rs @@ -9,7 +9,9 @@ pub use ::image as image_rs; pub fn load(handle: &Handle) -> image_rs::ImageResult { match handle.data() { Data::Path(path) => { - let image = ::image::open(path)?; + let image = ::image::io::Reader::open(&path)? + .with_guessed_format()? + .decode()?; let operation = std::fs::File::open(path) .ok() diff --git a/graphics/src/primitive.rs b/graphics/src/primitive.rs index aed59e1a91..0d7e231e22 100644 --- a/graphics/src/primitive.rs +++ b/graphics/src/primitive.rs @@ -80,6 +80,8 @@ pub enum Primitive { filter_method: image::FilterMethod, /// The bounds of the image bounds: Rectangle, + /// The border radii of the image + border_radius: [f32; 4], }, /// An SVG primitive Svg { diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index cb07c23bc5..147120133f 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -136,6 +136,7 @@ where type Font = Font; type Paragraph = text::Paragraph; type Editor = text::Editor; + type Raw = text::Raw; const ICON_FONT: Font = Font::with_name("Iced-Icons"); const CHECKMARK_ICON: char = '\u{f00c}'; @@ -183,6 +184,10 @@ where }); } + fn fill_raw(&mut self, raw: Self::Raw) { + self.primitives.push(Primitive::RawText(raw)); + } + fn fill_text( &mut self, text: Text<'_, Self::Font>, @@ -220,11 +225,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 7c4b5e31fe..b08b602f6c 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -10,7 +10,7 @@ pub use paragraph::Paragraph; pub use cosmic_text; use crate::core::font::{self, Font}; -use crate::core::text::Shaping; +use crate::core::text::{Shaping, Wrap}; use crate::core::{Color, Point, Rectangle, Size}; use once_cell::sync::OnceCell; @@ -98,11 +98,12 @@ pub fn measure(buffer: &cosmic_text::Buffer) -> Size { (run.line_w.max(width), total_lines + 1) }); - let (max_width, max_height) = buffer.size(); + let (max_width_opt, max_height_opt) = buffer.size(); Size::new( - width.min(max_width), - (total_lines as f32 * buffer.metrics().line_height).min(max_height), + width.min(max_width_opt.unwrap_or(f32::MAX)), + (total_lines as f32 * buffer.metrics().line_height) + .min(max_height_opt.unwrap_or(f32::MAX)), ) } @@ -170,6 +171,16 @@ pub fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping { } } +/// Converts some [`Wrap`] mode to a [`cosmic_text::Wrap`] strategy. +pub fn to_wrap(wrap: Wrap) -> cosmic_text::Wrap { + match wrap { + Wrap::None => cosmic_text::Wrap::None, + Wrap::Glyph => cosmic_text::Wrap::Glyph, + Wrap::Word => cosmic_text::Wrap::Word, + Wrap::WordOrGlyph => cosmic_text::Wrap::WordOrGlyph, + } +} + /// Converts some [`Color`] to a [`cosmic_text::Color`]. pub fn to_color(color: Color) -> cosmic_text::Color { let [r, g, b, a] = color.into_rgba8(); diff --git a/graphics/src/text/cache.rs b/graphics/src/text/cache.rs index 7fb335678d..337295c7c8 100644 --- a/graphics/src/text/cache.rs +++ b/graphics/src/text/cache.rs @@ -52,8 +52,8 @@ impl Cache { buffer.set_size( font_system, - key.bounds.width, - key.bounds.height.max(key.line_height), + Some(key.bounds.width), + Some(key.bounds.height.max(key.line_height)), ); buffer.set_text( font_system, diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index d5262ae820..8610f66f90 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, @@ -30,9 +30,21 @@ impl Editor { Self::default() } - /// Returns the buffer of the [`Editor`]. + /// Runs a closure with the buffer of the [`Editor`]. + pub fn with_buffer T, T>( + &self, + f: F, + ) -> T { + self.internal().editor.with_buffer(f) + } + + /// Returns the buffer of the `Paragraph`. pub fn buffer(&self) -> &cosmic_text::Buffer { - self.internal().editor.buffer() + match self.internal().editor.buffer_ref() { + cosmic_text::BufferRef::Owned(buffer) => buffer, + cosmic_text::BufferRef::Borrowed(buffer) => buffer, + cosmic_text::BufferRef::Arc(buffer) => buffer, + } } /// Creates a [`Weak`] reference to the [`Editor`]. @@ -83,14 +95,16 @@ impl editor::Editor for Editor { } fn line(&self, index: usize) -> Option<&str> { - self.buffer() - .lines - .get(index) - .map(cosmic_text::BufferLine::text) + let buffer = match self.internal().editor.buffer_ref() { + cosmic_text::BufferRef::Owned(buffer) => buffer, + cosmic_text::BufferRef::Borrowed(buffer) => buffer, + cosmic_text::BufferRef::Arc(buffer) => buffer, + }; + buffer.lines.get(index).map(cosmic_text::BufferLine::text) } fn line_count(&self) -> usize { - self.buffer().lines.len() + self.with_buffer(|buffer| buffer.lines.len()) } fn selection(&self) -> Option { @@ -101,133 +115,130 @@ 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 f32) * line_height + + visual_lines_offset, + 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") + .clone(); + + 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_line as f32 * line_height + visual_lines_offset, + )) + } } - } + }) } fn cursor_position(&self) -> (usize, usize) { @@ -252,16 +263,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 +293,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 +317,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 +362,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 +435,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 +466,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 +513,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 +526,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 +546,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(), + Some(new_bounds.width), + Some(new_bounds.height), + ) + }); internal.bounds = new_bounds; } @@ -546,7 +577,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 +592,40 @@ impl editor::Editor for Editor { format_highlight: impl Fn(&H::Highlight) -> highlighter::Format, ) { let internal = self.internal(); - let buffer = internal.editor.buffer(); - - let mut window = buffer.scroll() + buffer.visible_lines(); - - let last_visible_line = buffer - .lines - .iter() - .enumerate() - .find_map(|(i, line)| { - let visible_lines = line - .layout_opt() - .as_ref() - .expect("Line layout should be cached") - .len() as i32; - - if window > visible_lines { - window -= visible_lines; - None - } else { - Some(i) - } - }) - .unwrap_or(buffer.lines.len().saturating_sub(1)); + + let last_visible_line = internal.editor.with_buffer(|buffer| { + let metrics = buffer.metrics(); + let scroll = buffer.scroll(); + let mut window = + scroll.vertical + buffer.size().1.unwrap_or(f32::MAX); + + buffer + .lines + .iter() + .enumerate() + .skip(scroll.line) + .find_map(|(i, line)| { + let layout = line + .layout_opt() + .as_ref() + .expect("Line layout should be cached"); + + let mut layout_height = 0.0; + for layout_line in layout.iter() { + layout_height += layout_line + .line_height_opt + .unwrap_or(metrics.line_height); + } + + if window > layout_height { + window -= layout_height; + None + } else { + Some(i) + } + }) + .unwrap_or(buffer.lines.len().saturating_sub(1)) + }); let current_line = highlighter.current_line(); @@ -599,33 +644,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 +691,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()) } } @@ -745,35 +796,43 @@ fn highlight_line( }) } -fn visual_lines_offset(line: usize, buffer: &cosmic_text::Buffer) -> i32 { - let visual_lines_before_start: usize = buffer +fn visual_lines_offset(line: usize, buffer: &cosmic_text::Buffer) -> f32 { + let metrics = buffer.metrics(); + let scroll = buffer.scroll(); + + let mut height_before_start = 0.0; + buffer .lines .iter() + .skip(scroll.line) .take(line) .map(|line| { - line.layout_opt() + let layout = line + .layout_opt() .as_ref() - .expect("Line layout should be cached") - .len() - }) - .sum(); + .expect("Line layout should be cached"); + for layout_line in layout.iter() { + height_before_start += + layout_line.line_height_opt.unwrap_or(metrics.line_height); + } + }); - visual_lines_before_start as i32 - buffer.scroll() + height_before_start - scroll.vertical } 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/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs index 5d0275427a..ae298877b5 100644 --- a/graphics/src/text/paragraph.rs +++ b/graphics/src/text/paragraph.rs @@ -1,7 +1,7 @@ //! Draw paragraphs. use crate::core; use crate::core::alignment; -use crate::core::text::{Hit, LineHeight, Shaping, Text}; +use crate::core::text::{Hit, LineHeight, Shaping, Text, Wrap}; use crate::core::{Font, Pixels, Point, Size}; use crate::text; @@ -17,6 +17,7 @@ struct Internal { content: String, // TODO: Reuse from `buffer` (?) font: Font, shaping: Shaping, + wrap: Wrap, horizontal_alignment: alignment::Horizontal, vertical_alignment: alignment::Vertical, bounds: Size, @@ -77,10 +78,12 @@ impl core::text::Paragraph for Paragraph { buffer.set_size( font_system.raw(), - text.bounds.width, - text.bounds.height, + Some(text.bounds.width), + Some(text.bounds.height), ); + buffer.set_wrap(font_system.raw(), text::to_wrap(text.wrap)); + buffer.set_text( font_system.raw(), text.content, @@ -97,6 +100,7 @@ impl core::text::Paragraph for Paragraph { horizontal_alignment: text.horizontal_alignment, vertical_alignment: text.vertical_alignment, shaping: text.shaping, + wrap: text.wrap, bounds: text.bounds, min_bounds, version: font_system.version(), @@ -116,8 +120,8 @@ impl core::text::Paragraph for Paragraph { internal.buffer.set_size( font_system.raw(), - new_bounds.width, - new_bounds.height, + Some(new_bounds.width), + Some(new_bounds.height), ); internal.bounds = new_bounds; @@ -141,6 +145,7 @@ impl core::text::Paragraph for Paragraph { horizontal_alignment: internal.horizontal_alignment, vertical_alignment: internal.vertical_alignment, shaping: internal.shaping, + wrap: internal.wrap, }); } } @@ -274,6 +279,7 @@ impl Default for Internal { content: String::new(), font: Font::default(), shaping: Shaping::default(), + wrap: Wrap::default(), horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, bounds: Size::ZERO, diff --git a/graphics/src/viewport.rs b/graphics/src/viewport.rs index 5792555df7..4d31f2d343 100644 --- a/graphics/src/viewport.rs +++ b/graphics/src/viewport.rs @@ -12,6 +12,23 @@ pub struct Viewport { } impl Viewport { + /// Creates a new [`Viewport`] with the given logical dimensions and scale factor + pub fn with_logical_size(size: Size, scale_factor: f64) -> Viewport { + let physical_size = Size::new( + (size.width as f64 * scale_factor).ceil() as u32, + (size.height as f64 * scale_factor).ceil() as u32, + ); + Viewport { + physical_size, + logical_size: size, + scale_factor, + projection: Transformation::orthographic( + physical_size.width, + physical_size.height, + ), + } + } + /// Creates a new [`Viewport`] with the given physical dimensions and scale /// factor. pub fn with_physical_size(size: Size, scale_factor: f64) -> Viewport { diff --git a/renderer/Cargo.toml b/renderer/Cargo.toml index a159978c27..dfbea8061a 100644 --- a/renderer/Cargo.toml +++ b/renderer/Cargo.toml @@ -11,6 +11,7 @@ categories.workspace = true keywords.workspace = true [features] +default = [] wgpu = ["iced_wgpu"] image = ["iced_tiny_skia/image", "iced_wgpu?/image"] svg = ["iced_tiny_skia/svg", "iced_wgpu?/svg"] diff --git a/renderer/src/compositor.rs b/renderer/src/compositor.rs index dc2c50ffd5..6dcd396620 100644 --- a/renderer/src/compositor.rs +++ b/renderer/src/compositor.rs @@ -258,7 +258,7 @@ impl Candidate { } #[cfg(not(feature = "wgpu"))] Self::Wgpu => { - panic!("`wgpu` feature was not enabled in `iced_renderer`") + panic!("wgpu is not enabled") } } } diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index a7df414b9c..b94b88494c 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -23,8 +23,7 @@ pub use geometry::Geometry; use crate::core::renderer; use crate::core::text::{self, Text}; use crate::core::{Background, Color, Font, Pixels, Point, Rectangle, Vector}; -use crate::graphics::text::Editor; -use crate::graphics::text::Paragraph; +use crate::graphics::text::{Editor, Paragraph, Raw}; use crate::graphics::Mesh; use std::borrow::Cow; @@ -150,6 +149,7 @@ impl text::Renderer for Renderer { type Font = Font; type Paragraph = Paragraph; type Editor = Editor; + type Raw = Raw; const ICON_FONT: Font = iced_tiny_skia::Renderer::ICON_FONT; const CHECKMARK_ICON: char = iced_tiny_skia::Renderer::CHECKMARK_ICON; @@ -195,6 +195,10 @@ impl text::Renderer for Renderer { ); } + fn fill_raw(&mut self, raw: Self::Raw) { + delegate!(self, renderer, renderer.fill_raw(raw)); + } + fn fill_text( &mut self, text: Text<'_, Self::Font>, @@ -226,8 +230,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) + ); } } @@ -259,8 +268,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..d867ceb9b0 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -13,10 +13,17 @@ 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 +window_clipboard.workspace = true +dnd.workspace = true diff --git a/runtime/src/clipboard.rs b/runtime/src/clipboard.rs index bc45091266..9978f4ac62 100644 --- a/runtime/src/clipboard.rs +++ b/runtime/src/clipboard.rs @@ -1,4 +1,6 @@ //! Access the clipboard. +use window_clipboard::mime::{AllowedMimeTypes, AsMimeTypes}; + use crate::command::{self, Command}; use crate::futures::MaybeSend; @@ -13,6 +15,26 @@ pub enum Action { /// Write the given contents to the clipboard. Write(String), + + /// Write the given contents to the clipboard. + WriteData(Box), + + #[allow(clippy::type_complexity)] + /// Read the clipboard and produce `T` with the result. + ReadData(Vec, Box, String)>) -> T>), + + /// Read the clipboard and produce `T` with the result. + ReadPrimary(Box) -> T>), + + /// Write the given contents to the clipboard. + WritePrimary(String), + + /// Write the given contents to the clipboard. + WritePrimaryData(Box), + + #[allow(clippy::type_complexity)] + /// Read the clipboard and produce `T` with the result. + ReadPrimaryData(Vec, Box, String)>) -> T>), } impl Action { @@ -27,6 +49,20 @@ impl Action { match self { Self::Read(o) => Action::Read(Box::new(move |s| f(o(s)))), Self::Write(content) => Action::Write(content), + Self::WriteData(content) => Action::WriteData(content), + Self::ReadData(a, o) => { + Action::ReadData(a, Box::new(move |s| f(o(s)))) + } + Self::ReadPrimary(o) => { + Action::ReadPrimary(Box::new(move |s| f(o(s)))) + } + Self::WritePrimary(content) => Action::WritePrimary(content), + Self::WritePrimaryData(content) => { + Action::WritePrimaryData(content) + } + Self::ReadPrimaryData(a, o) => { + Action::ReadPrimaryData(a, Box::new(move |s| f(o(s)))) + } } } } @@ -36,6 +72,12 @@ impl fmt::Debug for Action { match self { Self::Read(_) => write!(f, "Action::Read"), Self::Write(_) => write!(f, "Action::Write"), + Self::WriteData(_) => write!(f, "Action::WriteData"), + Self::ReadData(_, _) => write!(f, "Action::ReadData"), + Self::ReadPrimary(_) => write!(f, "Action::ReadPrimary"), + Self::WritePrimary(_) => write!(f, "Action::WritePrimary"), + Self::WritePrimaryData(_) => write!(f, "Action::WritePrimaryData"), + Self::ReadPrimaryData(_, _) => write!(f, "Action::ReadPrimaryData"), } } } @@ -51,3 +93,58 @@ pub fn read( pub fn write(contents: String) -> Command { Command::single(command::Action::Clipboard(Action::Write(contents))) } + +/// Read the current contents of primary. +pub fn read_primary( + f: impl Fn(Option) -> Message + 'static, +) -> Command { + Command::single(command::Action::Clipboard(Action::ReadPrimary(Box::new( + f, + )))) +} + +/// Write the given contents to primary. +pub fn write_primary(contents: String) -> Command { + Command::single(command::Action::Clipboard(Action::WritePrimary(contents))) +} + +/// Read the current contents of the clipboard. +pub fn read_data( + f: impl Fn(Option) -> Message + 'static, +) -> Command { + Command::single(command::Action::Clipboard(Action::ReadData( + T::allowed().into(), + Box::new(move |d| f(d.and_then(|d| T::try_from(d).ok()))), + ))) +} + +/// Write the given contents to the clipboard. +pub fn write_data( + contents: impl AsMimeTypes + std::marker::Sync + std::marker::Send + 'static, +) -> Command { + Command::single(command::Action::Clipboard(Action::WriteData(Box::new( + contents, + )))) +} + +/// Read the current contents of the clipboard. +pub fn read_primary_data< + T: AllowedMimeTypes + Send + Sync + 'static, + Message, +>( + f: impl Fn(Option) -> Message + 'static, +) -> Command { + Command::single(command::Action::Clipboard(Action::ReadPrimaryData( + T::allowed().into(), + Box::new(move |d| f(d.and_then(|d| T::try_from(d).ok()))), + ))) +} + +/// Write the given contents to the clipboard. +pub fn write_primary_data( + contents: impl AsMimeTypes + std::marker::Sync + std::marker::Send + 'static, +) -> Command { + Command::single(command::Action::Clipboard(Action::WritePrimaryData( + Box::new(contents), + ))) +} diff --git a/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..0f412eddab 100644 --- a/runtime/src/command/action.rs +++ b/runtime/src/command/action.rs @@ -4,6 +4,7 @@ use crate::font; use crate::system; use crate::window; +use dnd::DndAction; use iced_futures::MaybeSend; use std::borrow::Cow; @@ -35,6 +36,9 @@ pub enum Action { /// Run a widget action. Widget(Box>), + /// Run a Dnd action. + Dnd(crate::dnd::DndAction), + /// Load a font from its bytes. LoadFont { /// The bytes of the font to load. @@ -43,6 +47,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 +79,12 @@ impl Action { bytes, tagger: Box::new(move |result| f(tagger(result))), }, + Self::PlatformSpecific(action) => { + Action::PlatformSpecific(action.map(f)) + } + Self::Dnd(a) => Action::Dnd(a.map(f)), + Action::LoadFont { bytes, tagger } => todo!(), + Action::PlatformSpecific(_) => todo!(), } } } @@ -90,6 +103,10 @@ 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) + } + Self::Dnd(action) => write!(f, "Action::Dnd"), } } } 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..1ad5003b40 --- /dev/null +++ b/runtime/src/command/platform_specific/wayland/data_device.rs @@ -0,0 +1,137 @@ +use iced_core::{window::Id, Vector}; +use iced_futures::MaybeSend; +use sctk::reexports::client::protocol::wl_data_device_manager::DndAction; +use std::{any::Any, fmt, marker::PhantomData}; + +/// 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<(DndIcon, Vector)>, + /// 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..c27085a1ac --- /dev/null +++ b/runtime/src/command/platform_specific/wayland/window.rs @@ -0,0 +1,395 @@ +use std::fmt; +use std::marker::PhantomData; + +use iced_core::layout::Limits; +use iced_core::window::Mode; +use iced_core::Size; +use iced_futures::MaybeSend; +use sctk::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge; + +use iced_core::window::Id; + +use crate::window; + +/// window settings +#[derive(Debug, Clone)] +pub struct SctkWindowSettings { + /// 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, + }, + /// 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 } => Action::ShowWindowMenu { id }, + 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 } => write!( + f, + "Action::Window::ShowWindowMenu {{ id: {:?} }}", + 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 + ), + } + } +} + +/// error type for unsupported actions +#[derive(Debug, Clone, thiserror::Error)] +pub enum Error { + /// Not supported + #[error("Not supported")] + NotSupported, +} + +impl TryFrom> for Action { + type Error = Error; + + fn try_from(value: window::Action) -> Result { + match value { + window::Action::Spawn(id, settings) => { + let min = settings.min_size.unwrap_or(Size::new(1., 1.)); + let max = settings.max_size.unwrap_or(Size::INFINITY); + let builder = SctkWindowSettings { + window_id: id, + app_id: Some(settings.platform_specific.application_id), + title: None, + parent: None, + autosize: false, + size_limits: Limits::NONE + .min_width(min.width) + .min_height(min.height) + .max_width(max.width) + .max_height(max.height), + size: ( + settings.size.width.round() as u32, + settings.size.height.round() as u32, + ), + resizable: settings + .resizable + .then_some(settings.resize_border as f64), + client_decorations: !settings.decorations, + transparent: settings.transparent, + xdg_activation_token: None, + }; + Ok(Action::Window { + builder, + _phantom: PhantomData, + }) + } + window::Action::Close(id) => Ok(Action::Destroy(id)), + window::Action::Resize(id, size) => Ok(Action::Size { + id, + width: size.width.round() as u32, + height: size.height.round() as u32, + }), + window::Action::Drag(id) => Ok(Action::InteractiveMove { id }), + window::Action::FetchSize(_, _) + | window::Action::FetchMaximized(_, _) + | window::Action::Move(_, _) + | window::Action::FetchMode(_, _) + | window::Action::ToggleMaximize(_) + | window::Action::ToggleDecorations(_) + | window::Action::RequestUserAttention(_, _) + | window::Action::GainFocus(_) + | window::Action::ChangeLevel(_, _) + | window::Action::FetchId(_, _) + | window::Action::ChangeIcon(_, _) + | window::Action::Screenshot(_, _) + | window::Action::FetchMinimized(_, _) => Err(Error::NotSupported), + window::Action::ShowWindowMenu(id) => { + Ok(Action::ShowWindowMenu { id }) + } + window::Action::Maximize(id, maximized) => { + if maximized { + Ok(Action::Maximize { id }) + } else { + Ok(Action::UnsetMaximize { id }) + } + } + window::Action::Minimize(id, bool) => { + if bool { + Ok(Action::Minimize { id }) + } else { + Err(Error::NotSupported) + } + } + window::Action::ChangeMode(id, mode) => { + Ok(Action::Mode(id, mode.into())) + } + } + } +} diff --git a/runtime/src/dnd.rs b/runtime/src/dnd.rs new file mode 100644 index 0000000000..393bc29c4e --- /dev/null +++ b/runtime/src/dnd.rs @@ -0,0 +1,162 @@ +//! Access the clipboard. + +use std::any::Any; + +use dnd::{DndDestinationRectangle, DndSurface}; +use iced_core::{clipboard::DndSource, Vector}; +use iced_futures::MaybeSend; +use window_clipboard::mime::{AllowedMimeTypes, AsMimeTypes}; + +use crate::{command, Command}; + +/// An action to be performed on the system. +pub enum DndAction { + /// Register a Dnd destination. + RegisterDndDestination { + /// The surface to register. + surface: DndSurface, + /// The rectangles to register. + rectangles: Vec, + }, + /// Start a Dnd operation. + StartDnd { + /// Whether the Dnd operation is internal. + internal: bool, + /// The source surface of the Dnd operation. + source_surface: Option, + /// The icon surface of the Dnd operation. + icon_surface: Option>, + /// The content of the Dnd operation. + content: Box, + /// The actions of the Dnd operation. + actions: dnd::DndAction, + }, + /// End a Dnd operation. + EndDnd, + /// Peek the current Dnd operation. + PeekDnd(String, Box, String)>) -> T>), + /// Set the action of the Dnd operation. + SetAction(dnd::DndAction), +} + +impl std::fmt::Debug for DndAction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::RegisterDndDestination { + surface, + rectangles, + } => f + .debug_struct("RegisterDndDestination") + .field("surface", surface) + .field("rectangles", rectangles) + .finish(), + Self::StartDnd { + internal, + source_surface, + icon_surface, + content: _, + actions, + } => f + .debug_struct("StartDnd") + .field("internal", internal) + .field("source_surface", source_surface) + .field("icon_surface", icon_surface) + .field("actions", actions) + .finish(), + Self::EndDnd => f.write_str("EndDnd"), + Self::PeekDnd(mime, _) => { + f.debug_struct("PeekDnd").field("mime", mime).finish() + } + Self::SetAction(a) => f.debug_tuple("SetAction").field(a).finish(), + } + } +} + +impl DndAction { + /// Maps the output of a [`DndAction`] using the provided closure. + pub fn map( + self, + f: impl Fn(T) -> A + 'static + MaybeSend + Sync, + ) -> DndAction + where + T: 'static, + { + match self { + Self::PeekDnd(m, o) => { + DndAction::PeekDnd(m, Box::new(move |d| f(o(d)))) + } + Self::EndDnd => DndAction::EndDnd, + Self::SetAction(a) => DndAction::SetAction(a), + Self::StartDnd { + internal, + source_surface, + icon_surface, + content, + actions, + } => DndAction::StartDnd { + internal, + source_surface, + icon_surface, + content, + actions, + }, + Self::RegisterDndDestination { + surface, + rectangles, + } => DndAction::RegisterDndDestination { + surface, + rectangles, + }, + } + } +} + +/// Read the current contents of the Dnd operation. +pub fn peek_dnd( + f: impl Fn(Option) -> Message + 'static, +) -> Command { + Command::single(command::Action::Dnd(DndAction::PeekDnd( + T::allowed() + .get(0) + .map_or_else(|| String::new(), |s| s.to_string()), + Box::new(move |d| f(d.and_then(|d| T::try_from(d).ok()))), + ))) +} + +/// Register a Dnd destination. +pub fn register_dnd_destination( + surface: DndSurface, + rectangles: Vec, +) -> Command { + Command::single(command::Action::Dnd(DndAction::RegisterDndDestination { + surface, + rectangles, + })) +} + +/// Start a Dnd operation. +pub fn start_dnd( + internal: bool, + source_surface: Option, + icon_surface: Option>, + content: Box, + actions: dnd::DndAction, +) -> Command { + Command::single(command::Action::Dnd(DndAction::StartDnd { + internal, + source_surface, + icon_surface, + content, + actions, + })) +} + +/// End a Dnd operation. +pub fn end_dnd() -> Command { + Command::single(command::Action::Dnd(DndAction::EndDnd)) +} + +/// Set the action of the Dnd operation. +pub fn set_action(a: dnd::DndAction) -> Command { + Command::single(command::Action::Dnd(DndAction::SetAction(a))) +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 03906f459a..eb1561d2d5 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -18,6 +18,7 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] pub mod clipboard; pub mod command; +pub mod dnd; pub mod font; pub mod keyboard; pub mod overlay; diff --git a/runtime/src/multi_window/state.rs b/runtime/src/multi_window/state.rs index afd0451965..fc6d7265d4 100644 --- a/runtime/src/multi_window/state.rs +++ b/runtime/src/multi_window/state.rs @@ -6,6 +6,7 @@ use crate::core::widget::operation::{self, Operation}; use crate::core::{Clipboard, Size}; use crate::user_interface::{self, UserInterface}; use crate::{Command, Debug, Program}; +use iced_core::widget::OperationOutputWrapper; /// The execution state of a multi-window [`Program`]. It leverages caching, event /// processing, and rendering primitive storage. @@ -205,7 +206,9 @@ where pub fn operate( &mut self, renderer: &mut P::Renderer, - operations: impl Iterator>>, + operations: impl Iterator< + Item = Box>>, + >, bounds: Size, debug: &mut Debug, ) { @@ -227,12 +230,15 @@ where match operation.finish() { operation::Outcome::None => {} - operation::Outcome::Some(message) => { + operation::Outcome::Some( + OperationOutputWrapper::Message(message), + ) => { self.queued_messages.push(message); } operation::Outcome::Chain(next) => { current_operation = Some(next); } + _ => {} }; } } diff --git a/runtime/src/overlay/nested.rs b/runtime/src/overlay/nested.rs index 60e2eb875e..00ea22d626 100644 --- a/runtime/src/overlay/nested.rs +++ b/runtime/src/overlay/nested.rs @@ -1,3 +1,5 @@ +use iced_core::widget::OperationOutputWrapper; + use crate::core::event; use crate::core::layout; use crate::core::mouse; @@ -144,13 +146,15 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn widget::Operation>, ) { fn recurse( element: &mut overlay::Element<'_, Message, Theme, Renderer>, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn widget::Operation< + OperationOutputWrapper, + >, ) where Renderer: renderer::Renderer, { diff --git a/runtime/src/program/state.rs b/runtime/src/program/state.rs index d685b07c86..9df4f4cea3 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 054d56b781..672a4f4445 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -1,4 +1,9 @@ //! Implement your own event loop to drive a user interface. + +use iced_core::clipboard::DndDestinationRectangles; +use iced_core::widget::tree::NAMED; +use iced_core::widget::{Operation, OperationOutputWrapper}; + use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; @@ -92,10 +97,15 @@ 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()); + NAMED.with(|named| { + let mut guard = named.borrow_mut(); + *guard = state.take_all_named(); + }); + + state.diff(root.as_widget_mut()); let base = root.as_widget().layout( &mut state, @@ -103,6 +113,10 @@ where &layout::Limits::new(Size::ZERO, bounds), ); + NAMED.with(|named| { + named.borrow_mut().clear(); + }); + UserInterface { root, base, @@ -562,7 +576,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 +619,40 @@ 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, + ) + } + + /// Find widget with given id + pub fn find(&self, id: &widget::Id) -> Option<&widget::Tree> { + self.state.find(id) + } + + /// Get the destination rectangles for the user interface. + pub fn dnd_rectangles( + &self, + prev_capacity: usize, + renderer: &Renderer, + ) -> DndDestinationRectangles { + let mut ret = DndDestinationRectangles::with_capacity(prev_capacity); + self.root.as_widget().drag_destinations( + &self.state, + Layout::new(&self.base), + renderer, + &mut ret, + ); + ret + } } /// Reusable data of a specific [`UserInterface`]. diff --git a/runtime/src/window.rs b/runtime/src/window.rs index 2136d64dcf..8416cc4b71 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 { - crate::core::Event::Window(_, Event::RedrawRequested(at)) => Some(at), + 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 { + 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, }) } @@ -156,6 +179,11 @@ pub fn change_level(id: Id, level: Level) -> Command { Command::single(command::Action::Window(Action::ChangeLevel(id, level))) } +/// Show window menu at cursor position. +pub fn show_window_menu(id: Id) -> Command { + Command::single(command::Action::Window(Action::ShowWindowMenu(id))) +} + /// Fetches an identifier unique to the window, provided by the underlying windowing system. This is /// not to be confused with [`Id`]. pub fn fetch_id( diff --git a/runtime/src/window/action.rs b/runtime/src/window/action.rs index 8b5325692c..dc168c5d4b 100644 --- a/runtime/src/window/action.rs +++ b/runtime/src/window/action.rs @@ -79,6 +79,11 @@ pub enum Action { GainFocus(Id), /// Change the window [`Level`]. ChangeLevel(Id, Level), + /// Show window menu at cursor position. + /// + /// ## Platform-specific + /// Android / iOS / macOS / Orbital / Wayland / Web / X11: Unsupported. + ShowWindowMenu(Id), /// Fetch the raw identifier unique to the window. FetchId(Id, Box T + 'static>), /// Change the window [`Icon`]. @@ -137,6 +142,7 @@ impl Action { } Self::GainFocus(id) => Action::GainFocus(id), Self::ChangeLevel(id, level) => Action::ChangeLevel(id, level), + Self::ShowWindowMenu(id) => Action::ShowWindowMenu(id), Self::FetchId(id, o) => { Action::FetchId(id, Box::new(move |s| f(o(s)))) } @@ -193,6 +199,9 @@ impl fmt::Debug for Action { Self::ChangeLevel(id, level) => { write!(f, "Action::ChangeLevel({id:?}, {level:?})") } + Self::ShowWindowMenu(id) => { + write!(f, "Action::ShowWindowMenu({id:?})") + } Self::FetchId(id, _) => write!(f, "Action::FetchId({id:?})"), Self::ChangeIcon(id, _icon) => { write!(f, "Action::ChangeIcon({id:?})") diff --git a/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..31036a83c2 --- /dev/null +++ b/sctk/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "iced_sctk" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +debug = ["iced_runtime/debug"] +system = ["sysinfo"] +application = [] +a11y = ["iced_accessibility", "iced_runtime/a11y"] +clipboard = [] + +[dependencies] +tracing = "0.1" +thiserror = "1.0" +sctk.workspace = true +wayland-protocols.workspace = true +window_clipboard.workspace = true +# sctk = { package = "smithay-client-toolkit", path = "../../fork/client-toolkit/" } +raw-window-handle = "0.6" +enum-repr = "0.2" +futures = "0.3" +wayland-backend = { version = "0.3.1", features = ["client_system"] } +float-cmp = "0.9" +xkbcommon-dl = "0.4.1" +xkbcommon = { version = "0.7", features = ["wayland"] } +itertools = "0.12" +xkeysym = "0.2.0" +lazy_static = "1.4.0" + +[dependencies.iced_runtime] +path = "../runtime" +features = ["wayland", "multi-window"] + +[dependencies.iced_style] +path = "../style" + +[dependencies.iced_graphics] +path = "../graphics" + + +[dependencies.iced_futures] +path = "../futures" + +[dependencies.sysinfo] +version = "0.28" +optional = true + +[dependencies.iced_accessibility] +path = "../accessibility" +optional = true +features = ["accesskit_unix"] diff --git a/sctk/LICENSE.md b/sctk/LICENSE.md new file mode 100644 index 0000000000..8dc5b15d9a --- /dev/null +++ b/sctk/LICENSE.md @@ -0,0 +1,359 @@ +Mozilla Public License Version 2.0 +================================== + +## 1. Definitions + +### 1.1. "Contributor" +means each individual or legal entity that creates, contributes to +the creation of, or owns Covered Software. + +### 1.2. "Contributor Version" +means the combination of the Contributions of others (if any) used +by a Contributor and that particular Contributor's Contribution. + +### 1.3. "Contribution" +means Covered Software of a particular Contributor. + +### 1.4. "Covered Software" +means Source Code Form to which the initial Contributor has attached +the notice in Exhibit A, the Executable Form of such Source Code +Form, and Modifications of such Source Code Form, in each case +including portions thereof. + +### 1.5. "Incompatible With Secondary Licenses" +means + ++ (a) that the initial Contributor has attached the notice described +in Exhibit B to the Covered Software; or + ++ (b) that the Covered Software was made available under the terms of +version 1.1 or earlier of the License, but not also under the +terms of a Secondary License. + +### 1.6. "Executable Form" +means any form of the work other than Source Code Form. + +### 1.7. "Larger Work" +means a work that combines Covered Software with other material, in +a separate file or files, that is not Covered Software. + +### 1.8. "License" +means this document. + +### 1.9. "Licensable" +means having the right to grant, to the maximum extent possible, +whether at the time of the initial grant or subsequently, any and +all of the rights conveyed by this License. + +### 1.10. "Modifications" +means any of the following: + ++ (a) any file in Source Code Form that results from an addition to, +deletion from, or modification of the contents of Covered +Software; or + ++ (b) any new file in Source Code Form that contains any Covered +Software. + +### 1.11. "Patent Claims" of a Contributor +means any patent claim(s), including without limitation, method, +process, and apparatus claims, in any patent Licensable by such +Contributor that would be infringed, but for the grant of the +License, by the making, using, selling, offering for sale, having +made, import, or transfer of either its Contributions or its +Contributor Version. + +### 1.12. "Secondary License" +means either the GNU General Public License, Version 2.0, the GNU +Lesser General Public License, Version 2.1, the GNU Affero General +Public License, Version 3.0, or any later versions of those +licenses. + +### 1.13. "Source Code Form" +means the form of the work preferred for making modifications. + +### 1.14. "You" (or "Your") +means an individual or a legal entity exercising rights under this +License. For legal entities, "You" includes any entity that +controls, is controlled by, or is under common control with You. For +purposes of this definition, "control" means (a) the power, direct +or indirect, to cause the direction or management of such entity, +whether by contract or otherwise, or (b) ownership of more than +fifty percent (50%) of the outstanding shares or beneficial +ownership of such entity. + +## 2. License Grants and Conditions + +### 2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + ++ (a) under intellectual property rights (other than patent or trademark) +Licensable by such Contributor to use, reproduce, make available, +modify, display, perform, distribute, and otherwise exploit its +Contributions, either on an unmodified basis, with Modifications, or +as part of a Larger Work; and + ++ (b) under Patent Claims of such Contributor to make, use, sell, offer +for sale, have made, import, and otherwise transfer either its +Contributions or its Contributor Version. + +### 2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +### 2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + ++ (a) for any code that a Contributor has removed from Covered Software; +or + ++ (b) for infringements caused by: (i) Your and any other third party's +modifications of Covered Software, or (ii) the combination of its +Contributions with other software (except as part of its Contributor +Version); or + ++ (c) under Patent Claims infringed by Covered Software in the absence of +its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +### 2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +### 2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +### 2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +### 2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +## 3. Responsibilities + +### 3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +### 3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + ++ (a) such Covered Software must also be made available in Source Code +Form, as described in Section 3.1, and You must inform recipients of +the Executable Form how they can obtain a copy of such Source Code +Form by reasonable means in a timely manner, at a charge no more +than the cost of distribution to the recipient; and + ++ (b) You may distribute such Executable Form under the terms of this +License, or sublicense it under different terms, provided that the +license for the Executable Form does not attempt to limit or alter +the recipients' rights in the Source Code Form under this License. + +### 3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +### 3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +### 3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +## 4. Inability to Comply Due to Statute or Regulation + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +## 5. Termination + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + + +## 6. Disclaimer of Warranty + +**Covered Software is provided under this License on an "as is" +basis, without warranty of any kind, either expressed, implied, or +statutory, including, without limitation, warranties that the +Covered Software is free of defects, merchantable, fit for a +particular purpose or non-infringing. The entire risk as to the +quality and performance of the Covered Software is with You. +Should any Covered Software prove defective in any respect, You +(not any Contributor) assume the cost of any necessary servicing, +repair, or correction. This disclaimer of warranty constitutes an +essential part of this License. No use of any Covered Software is +authorized under this License except under this disclaimer.** + + +#7. Limitation of Liability + +**Under no circumstances and under no legal theory, whether tort +(including negligence), contract, or otherwise, shall any +Contributor, or anyone who distributes Covered Software as +permitted above, be liable to You for any direct, indirect, +special, incidental, or consequential damages of any character +including, without limitation, damages for lost profits, loss of +goodwill, work stoppage, computer failure or malfunction, or any +and all other commercial damages or losses, even if such party +shall have been informed of the possibility of such damages. This +limitation of liability shall not apply to liability for death or +personal injury resulting from such party's negligence to the +extent applicable law prohibits such limitation. Some +jurisdictions do not allow the exclusion or limitation of +incidental or consequential damages, so this exclusion and +limitation may not apply to You.** + + +## 8. Litigation + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +## 9. Miscellaneous + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +## 10. Versions of the License + +### 10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +### 10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +### 10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +### 10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +## Exhibit A - Source Code Form License Notice + + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +## Exhibit B - "Incompatible With Secondary Licenses" Notice + + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. + diff --git a/sctk/src/adaptor.rs b/sctk/src/adaptor.rs new file mode 100644 index 0000000000..c26b8e5cf4 --- /dev/null +++ b/sctk/src/adaptor.rs @@ -0,0 +1,42 @@ +use accesskit::{kurbo::Rect, ActionHandler, TreeUpdate}; +use accesskit_unix::Adapter as UnixAdapter; +use winit::window::Window; + +pub struct Adapter { + adapter: Option, +} + +impl Adapter { + pub fn new( + _: &Window, + source: impl 'static + FnOnce() -> TreeUpdate, + action_handler: Box, + ) -> Self { + let adapter = UnixAdapter::new( + String::new(), + String::new(), + String::new(), + source, + action_handler, + ); + Self { adapter } + } + + pub fn set_root_window_bounds(&self, outer: Rect, inner: Rect) { + if let Some(adapter) = &self.adapter { + adapter.set_root_window_bounds(outer, inner); + } + } + + pub fn update(&self, update: TreeUpdate) { + if let Some(adapter) = &self.adapter { + adapter.update(update); + } + } + + pub fn update_if_active(&self, updater: impl FnOnce() -> TreeUpdate) { + if let Some(adapter) = &self.adapter { + adapter.update(updater()); + } + } +} diff --git a/sctk/src/application.rs b/sctk/src/application.rs new file mode 100644 index 0000000000..d6e6337b62 --- /dev/null +++ b/sctk/src/application.rs @@ -0,0 +1,2382 @@ +#[cfg(feature = "a11y")] +use crate::sctk_event::ActionRequestEvent; +use crate::{ + clipboard::Clipboard, + commands::{layer_surface::get_layer_surface, window::get_window}, + dpi::{LogicalPosition, PhysicalPosition}, + 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 iced_futures::core::Clipboard as IcedClipboard; +use iced_graphics::{compositor, Compositor, Viewport}; +use iced_runtime::{ + clipboard, + command::{ + self, + platform_specific::{ + self, + wayland::{data_device::DndIcon, popup, window}, + }, + }, + core::{mouse::Interaction, touch, Color, Point, Size}, + multi_window::Program, + system, user_interface, + window::Id as SurfaceId, + Command, Debug, UserInterface, +}; +use iced_style::application::{self, StyleSheet}; +use itertools::Itertools; +use raw_window_handle::{ + DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, + RawDisplayHandle, RawWindowHandle, WaylandDisplayHandle, + WaylandWindowHandle, WindowHandle, +}; +use sctk::{ + reexports::client::{protocol::wl_surface::WlSurface, Proxy, QueueHandle}, + seat::{keyboard::Modifiers, pointer::PointerEventKind}, +}; +use std::mem::ManuallyDrop; +use std::{ + collections::HashMap, hash::Hash, marker::PhantomData, os::raw::c_void, + ptr::NonNull, time::Duration, +}; +use wayland_backend::client::ObjectId; +use wayland_protocols::wp::viewporter::client::wp_viewport::WpViewport; +use window_clipboard::mime::ClipboardStoreData; + +use crate::subsurface_widget::{SubsurfaceInstance, SubsurfaceState}; + +pub enum Event { + /// A normal sctk event + SctkEvent(IcedSctkEvent), + /// TODO + // (maybe we should also allow users to listen/react to those internal messages?) + + /// layer surface requests from the client + LayerSurface(platform_specific::wayland::layer_surface::Action), + /// window requests from the client + Window(platform_specific::wayland::window::Action), + /// popup requests from the client + Popup(platform_specific::wayland::popup::Action), + /// data device requests from the client + DataDevice(platform_specific::wayland::data_device::Action), + /// xdg-activation request from the client + Activation(platform_specific::wayland::activation::Action), + /// data session lock requests from the client + SessionLock(platform_specific::wayland::session_lock::Action), + /// request sctk to set the cursor of the active pointer + SetCursor(Interaction), + /// Application Message + Message(Message), +} + +pub struct IcedSctkState; + +#[derive(Debug, Clone)] +pub struct SurfaceDisplayWrapper { + backend: wayland_backend::client::Backend, + wl_surface: WlSurface, +} + +impl HasDisplayHandle for SurfaceDisplayWrapper { + fn display_handle(&self) -> Result { + let ptr = self.backend.display_ptr() as *mut c_void; + let Some(ptr) = NonNull::new(ptr) else { + return Err(HandleError::Unavailable); + }; + let display_handle = WaylandDisplayHandle::new(ptr); + Ok(unsafe { + DisplayHandle::borrow_raw(RawDisplayHandle::Wayland(display_handle)) + }) + } +} + +impl HasWindowHandle for SurfaceDisplayWrapper { + fn window_handle(&self) -> Result { + let ptr = self.wl_surface.id().as_ptr() as *mut c_void; + let Some(ptr) = NonNull::new(ptr) else { + return Err(HandleError::Unavailable); + }; + let window_handle = WaylandWindowHandle::new(ptr); + Ok(unsafe { + WindowHandle::borrow_raw(RawWindowHandle::Wayland(window_handle)) + }) + } +} + +/// An interactive, native, cross-platform, multi-windowed application. +/// +/// This trait is the main entrypoint of multi-window Iced. Once implemented, you can run +/// your GUI application by simply calling [`run`]. It will run in +/// its own window. +/// +/// An [`Application`] can execute asynchronous actions by returning a +/// [`Command`] in some of its methods. +/// +/// When using an [`Application`] with the `debug` feature enabled, a debug view +/// can be toggled by pressing `F12`. +pub trait Application: Program +where + Self::Theme: StyleSheet, +{ + /// The data needed to initialize your [`Application`]. + type Flags; + + /// Initializes the [`Application`] with the flags provided to + /// [`run`] as part of the [`Settings`]. + /// + /// Here is where you should return the initial state of your app. + /// + /// Additionally, you can return a [`Command`] if you need to perform some + /// async action in the background on startup. This is useful if you want to + /// load state from a file, perform an initial HTTP request, etc. + fn new(flags: Self::Flags) -> (Self, Command); + + /// Returns the current title of the [`Application`]. + /// + /// This title can be dynamic! The runtime will automatically update the + /// title of your application when necessary. + fn title(&self, window: SurfaceId) -> String; + + /// Returns the current `Theme` of the [`Application`]. + fn theme(&self, window: SurfaceId) -> Self::Theme; + + /// Returns the `Style` variation of the `Theme`. + fn style(&self) -> ::Style { + Default::default() + } + + /// Returns the event `Subscription` for the current state of the + /// application. + /// + /// The messages produced by the `Subscription` will be handled by + /// [`update`](#tymethod.update). + /// + /// A `Subscription` will be kept alive as long as you keep returning it! + /// + /// By default, it returns an empty subscription. + fn subscription(&self) -> Subscription { + Subscription::none() + } + + /// Returns the scale factor of the window of the [`Application`]. + /// + /// It can be used to dynamically control the size of the UI at runtime + /// (i.e. zooming). + /// + /// For instance, a scale factor of `2.0` will make widgets twice as big, + /// while a scale factor of `0.5` will shrink them to half their size. + /// + /// By default, it returns `1.0`. + #[allow(unused_variables)] + fn scale_factor(&self, window: SurfaceId) -> f64 { + 1.0 + } +} + +/// Runs an [`Application`] with an executor, compositor, and the provided +/// settings. +pub fn run( + settings: settings::Settings, + compositor_settings: C::Settings, +) -> Result<(), error::Error> +where + A: Application + 'static, + E: Executor + 'static, + C: Compositor + 'static, + ::Theme: StyleSheet, +{ + let mut debug = Debug::new(); + debug.startup_started(); + + let exit_on_close_request = settings.exit_on_close_request; + + let mut event_loop = SctkEventLoop::::new(&settings) + .expect("Failed to initialize the event loop"); + + let (runtime, ev_proxy) = { + let ev_proxy = event_loop.proxy(); + let executor = E::new().map_err(Error::ExecutorCreationFailed)?; + + (Runtime::new(executor, ev_proxy.clone()), ev_proxy) + }; + + let (application, init_command) = { + let flags = settings.flags; + + runtime.enter(|| A::new(flags)) + }; + + let init_command = match settings.surface { + settings::InitialSurface::LayerSurface(b) => { + Command::batch(vec![init_command, get_layer_surface(b)]) + } + settings::InitialSurface::XdgWindow(b) => { + Command::batch(vec![init_command, get_window(b)]) + } + settings::InitialSurface::None => init_command, + }; + let wl_surface = event_loop + .state + .compositor_state + .create_surface(&event_loop.state.queue_handle); + + // let (display, context, config, surface) = init_egl(&wl_surface, 100, 100); + let backend = event_loop + .wayland_dispatcher + .as_source_ref() + .connection() + .backend(); + let qh = event_loop.state.queue_handle.clone(); + let wrapper = SurfaceDisplayWrapper { + backend: backend.clone(), + wl_surface, + }; + + #[allow(unsafe_code)] + let compositor = C::new(compositor_settings, wrapper.clone()).unwrap(); + let renderer = compositor.create_renderer(); + + let auto_size_surfaces = HashMap::new(); + + let surface_ids = Default::default(); + let subsurface_ids = Default::default(); + + let (mut sender, receiver) = mpsc::unbounded::>(); + let (control_sender, mut control_receiver) = mpsc::unbounded(); + + let mut instance = Box::pin(run_instance::( + application, + compositor, + renderer, + runtime, + ev_proxy, + debug, + receiver, + control_sender, + surface_ids, + subsurface_ids, + auto_size_surfaces, + // display, + // context, + // config, + backend, + init_command, + exit_on_close_request, + qh, + settings.control_flow_timeout, + )); + + let mut context = task::Context::from_waker(task::noop_waker_ref()); + + let _ = event_loop.run_return(move |event, _, control_flow| { + if let ControlFlow::ExitWithCode(_) = control_flow { + return; + } + + sender.start_send(event).expect("Failed to send event"); + + let poll = instance.as_mut().poll(&mut context); + + match poll { + task::Poll::Pending => { + if let Ok(Some(flow)) = control_receiver.try_next() { + *control_flow = flow + } + } + task::Poll::Ready(_) => { + *control_flow = ControlFlow::ExitWithCode(1) + } + }; + }); + + Ok(()) +} + +fn subscription_map(e: A::Message) -> Event +where + A: Application + 'static, + E: Executor + 'static, + C: Compositor + 'static, + ::Theme: StyleSheet, +{ + Event::SctkEvent(IcedSctkEvent::UserEvent(e)) +} + +// XXX Ashley careful, A, E, C must be exact same as in update, or the subscription map type will have a different hash +async fn run_instance( + mut application: A, + mut compositor: C, + mut renderer: A::Renderer, + mut runtime: Runtime>, Event>, + mut ev_proxy: proxy::Proxy>, + mut debug: Debug, + mut receiver: mpsc::UnboundedReceiver>, + mut control_sender: mpsc::UnboundedSender, + mut surface_ids: HashMap, + mut subsurface_ids: HashMap, + mut auto_size_surfaces: HashMap, + backend: wayland_backend::client::Backend, + init_command: Command, + exit_on_close_request: bool, + queue_handle: QueueHandle::Message>>, + wait: Option, +) -> 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(); + + let mut subsurface_state = None::>; + + { + run_command( + &application, + &mut cache, + None::<&State>, + &mut renderer, + init_command, + &mut runtime, + &mut ev_proxy, + &mut debug, + || compositor.fetch_information(), + &mut auto_size_surfaces, + &mut Vec::new(), + &mut simple_clipboard, + ); + } + runtime.track( + application + .subscription() + .map(subscription_map::) + .into_recipes(), + ); + + let mut mouse_interaction = Interaction::default(); + let mut sctk_events: Vec = Vec::new(); + #[cfg(feature = "a11y")] + let mut a11y_events: Vec = + Vec::new(); + #[cfg(feature = "a11y")] + let mut a11y_enabled = false; + #[cfg(feature = "a11y")] + let mut adapters: HashMap< + SurfaceId, + crate::event_loop::adapter::IcedSctkAdapter, + > = HashMap::new(); + + let mut messages: Vec = Vec::new(); + #[cfg(feature = "a11y")] + let mut commands: Vec> = Vec::new(); + let mut redraw_pending = false; + + debug.startup_finished(); + + // let mut current_context_window = init_id_inner; + + let mut kbd_surface_id: Option = None; + let mut mods: Modifiers = Modifiers::default(); + let mut destroyed_surface_ids: HashMap = + Default::default(); + + 'main: while let Some(event) = receiver.next().await { + match event { + IcedSctkEvent::NewEvents(start_cause) => { + redraw_pending = matches!( + start_cause, + StartCause::Init + | StartCause::Poll + | StartCause::ResumeTimeReached { .. } + ); + } + IcedSctkEvent::UserEvent(message) => { + messages.push(message); + } + IcedSctkEvent::SctkEvent(event) => { + sctk_events.push(event.clone()); + match event { + SctkEvent::SeatEvent { .. } => {} // TODO Ashley: handle later possibly if multiseat support is wanted + SctkEvent::PointerEvent { + variant, + .. + } => { + let mut offset = (0., 0.); + let (state, _native_id) = match surface_ids + .get(&variant.surface.id()) + .and_then(|id| states.get_mut(&id.inner()).map(|state| (state, id))) + { + Some(s) => s, + None => { + if let Some((x_offset, y_offset, id)) = subsurface_ids.get(&variant.surface.id()) { + offset = (f64::from(*x_offset), f64::from(*y_offset)); + states.get_mut(&id.inner()).map(|state| (state, id)).unwrap() + } else { + continue + } + }, + }; + match variant.kind { + PointerEventKind::Enter { .. } => { + state.set_cursor_position(Some(LogicalPosition { x: variant.position.0 + offset.0, y: variant.position.1 + offset.1 })); + } + PointerEventKind::Leave { .. } => { + state.set_cursor_position(None); + } + PointerEventKind::Motion { .. } => { + state.set_cursor_position(Some(LogicalPosition { x: variant.position.0 + offset.0, y: variant.position.1 + offset.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::TouchEvent { variant, surface, .. } => { + let mut offset = (0., 0.); + let (state, _native_id) = match surface_ids + .get(&surface.id()) + .and_then(|id| states.get_mut(&id.inner()).map(|state| (state, id))) + { + Some(s) => s, + None => { + if let Some((x_offset, y_offset, id)) = subsurface_ids.get(&surface.id()) { + offset = (f64::from(*x_offset), f64::from(*y_offset)); + states.get_mut(&id.inner()).map(|state| (state, id)).unwrap() + } else { + continue + } + }, + }; + let position = match variant { + touch::Event::FingerPressed { position, .. } => position, + touch::Event::FingerLifted { position, .. } => position, + touch::Event::FingerMoved { position, .. } => position, + touch::Event::FingerLost { position, .. } => position, + }; + state.set_cursor_position(Some(LogicalPosition { x: position.x as f64 + offset.0, y: position.y as f64 + offset.1 })); + }, + SctkEvent::WindowEvent { variant, id: wl_surface } => match variant { + crate::sctk_event::WindowEventVariant::Created(id, native_id) => { + surface_ids.insert(id, SurfaceIdWrapper::Window(native_id)); + states.insert(native_id, State::new(&application, SurfaceIdWrapper::Window(native_id), SurfaceDisplayWrapper { + backend: backend.clone(), + wl_surface + })); + } + crate::sctk_event::WindowEventVariant::Close => { + if let Some(surface_id) = surface_ids.remove(&wl_surface.id()) { + // drop(compositor_surfaces.remove(&surface_id.inner())); + auto_size_surfaces.remove(&surface_id); + interfaces.remove(&surface_id.inner()); + states.remove(&surface_id.inner()); + destroyed_surface_ids.insert(wl_surface.id(), surface_id); + if exit_on_close_request && states.is_empty() { + break 'main; + } + } + } + crate::sctk_event::WindowEventVariant::WmCapabilities(_) + | crate::sctk_event::WindowEventVariant::ConfigureBounds { .. } => {} + crate::sctk_event::WindowEventVariant::Configure( + current_size, + _, + wl_surface, + first, + ) | crate::sctk_event::WindowEventVariant::Size( + current_size, + wl_surface, + first, + ) => { + if let Some(id) = surface_ids.get(&wl_surface.id()) { + let Some(state) = states.get_mut(&id.inner()) else { + continue; + }; + let (w, h) = auto_size_surfaces.get(id).map_or_else(|| (current_size.0.get(), current_size.1.get()), |(w, h, _, _)| (*w, *h)); + if state.surface.is_none() { + let wrapper = SurfaceDisplayWrapper { + backend: backend.clone(), + wl_surface + }; + if matches!(simple_clipboard.state(), crate::clipboard::State::Unavailable) { + if let Ok(h) = wrapper.display_handle() { + simple_clipboard = unsafe {Clipboard::connect(&h)}; + } + } + let mut c_surface = compositor.create_surface(wrapper.clone(), w, h); + compositor.configure_surface(&mut c_surface, w, h); + state.surface = Some(c_surface); + } + state.set_logical_size(w as f32, h as f32); + + 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); + } + } + } + crate::sctk_event::WindowEventVariant::ScaleFactorChanged(sf, viewport) => { + if let Some(state) = surface_ids + .get(&wl_surface.id()) + .and_then(|id| states.get_mut(&id.inner())) + { + state.wp_viewport = viewport; + state.set_scale_factor(sf); + } + }, + // handled by the application + crate::sctk_event::WindowEventVariant::StateChanged(_) => {}, + }, + SctkEvent::LayerSurfaceEvent { variant, id: wl_surface } => match variant { + LayerSurfaceEventVariant::Created(id, native_id) => { + surface_ids.insert(id, SurfaceIdWrapper::LayerSurface(native_id)); + states.insert(native_id, State::new(&application, SurfaceIdWrapper::LayerSurface(native_id), SurfaceDisplayWrapper { + backend: backend.clone(), + wl_surface: wl_surface.clone() + })); + + } + LayerSurfaceEventVariant::Done => { + if let Some(surface_id) = surface_ids.remove(&wl_surface.id()) { + if kbd_surface_id == Some(wl_surface.id()) { + kbd_surface_id = None; + } + auto_size_surfaces.remove(&surface_id); + interfaces.remove(&surface_id.inner()); + states.remove(&surface_id.inner()); + destroyed_surface_ids.insert(wl_surface.id(), surface_id); + if exit_on_close_request && states.is_empty() { + break 'main; + } + } + } + LayerSurfaceEventVariant::Configure(configure, wl_surface, first) => { + if let Some(id) = surface_ids.get(&wl_surface.id()) { + let Some(state) = states.get_mut(&id.inner()) else { + continue; + }; + if state.surface.is_none() { + let wrapper = SurfaceDisplayWrapper { + backend: backend.clone(), + wl_surface + }; + if matches!(simple_clipboard.state(), crate::clipboard::State::Unavailable) { + if let Ok(h) = wrapper.display_handle() { + simple_clipboard = unsafe {Clipboard::connect(&h)}; + } + } + let mut c_surface = compositor.create_surface(wrapper.clone(), configure.new_size.0, configure.new_size.1); + compositor.configure_surface(&mut c_surface, configure.new_size.0, configure.new_size.1); + state.surface = Some(c_surface); + }; + if let Some((w, h, _, is_dirty)) = auto_size_surfaces.get_mut(id) { + *is_dirty = first || *w != configure.new_size.0 || *h != configure.new_size.1; + state.set_logical_size(*w as f32, *h as f32); + } else { + state.set_logical_size( + configure.new_size.0 as f32, + configure.new_size.1 as f32, + ); + } + if first { + let user_interface = build_user_interface( + &application, + 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); + } + } + } + LayerSurfaceEventVariant::ScaleFactorChanged(sf, viewport) => { + if let Some(state) = surface_ids + .get(&wl_surface.id()) + .and_then(|id| states.get_mut(&id.inner())) + { + state.wp_viewport = viewport; + state.set_scale_factor(sf); + } + }, + }, + SctkEvent::PopupEvent { + variant, + toplevel_id: _, + parent_id: _, + id: wl_surface, + } => match variant { + PopupEventVariant::Created(id, native_id) => { + surface_ids.insert(id, SurfaceIdWrapper::Popup(native_id)); + states.insert(native_id, State::new(&application, SurfaceIdWrapper::Popup(native_id),SurfaceDisplayWrapper { + backend: backend.clone(), + wl_surface + })); + } + PopupEventVariant::Done => { + if let Some(surface_id) = surface_ids.remove(&wl_surface.id()) { + auto_size_surfaces.remove(&surface_id); + interfaces.remove(&surface_id.inner()); + states.remove(&surface_id.inner()); + destroyed_surface_ids.insert(wl_surface.id(), surface_id); + } + } + PopupEventVariant::Configure(configure, wl_surface, first) => { + if let Some(id) = surface_ids.get(&wl_surface.id()) { + let Some(state) = states.get_mut(&id.inner()) else { + continue; + }; + if state.surface.is_none() { + let wrapper = SurfaceDisplayWrapper { + backend: backend.clone(), + wl_surface + }; + let c_surface = compositor.create_surface(wrapper.clone(), configure.width as u32, configure.height as u32); + + state.surface = Some(c_surface); + } + if let Some((w, h, _, is_dirty)) = auto_size_surfaces.get_mut(id) { + *is_dirty |= first || *w != configure.width as u32 || *h != configure.height as u32; + state.set_logical_size(*w as f32, *h as f32); + } else { + state.set_logical_size( + configure.width as f32, + configure.height as f32, + ); + }; + if first { + let user_interface = build_user_interface( + &application, + 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); + } + } + } + PopupEventVariant::RepositionionedPopup { .. } => {} + PopupEventVariant::Size(width, height) => { + if let Some(id) = surface_ids.get(&wl_surface.id()) { + if let Some(state) = states.get_mut(&id.inner()) { + if let Some((w, h, _, is_dirty)) = auto_size_surfaces.get_mut(id) { + *is_dirty = *w != width || *h != height; + state.set_logical_size(*w as f32, *h as f32); + } else { + state.set_logical_size( + width as f32, + height as f32, + ); + } + } + } + }, + PopupEventVariant::ScaleFactorChanged(sf, viewport) => { + if let Some(id) = surface_ids.get(&wl_surface.id()) { + if let Some(state) = states.get_mut(&id.inner()) { + state.wp_viewport = viewport; + state.set_scale_factor(sf); + } + } + }, + }, + // TODO forward these events to an application which requests them? + SctkEvent::NewOutput { .. } => { + } + SctkEvent::UpdateOutput { .. } => { + } + SctkEvent::RemovedOutput( ..) => { + } + SctkEvent::ScaleFactorChanged { .. } => {} + SctkEvent::DataSource(DataSourceEvent::DndFinished) | SctkEvent::DataSource(DataSourceEvent::DndCancelled)=> { + surface_ids.retain(|id, surface_id| { + match surface_id { + SurfaceIdWrapper::Dnd(inner) => { + interfaces.remove(inner); + states.remove(inner); + destroyed_surface_ids.insert(id.clone(), *surface_id); + false + }, + _ => true, + } + }) + } + SctkEvent::SessionLockSurfaceCreated { surface, native_id } => { + surface_ids.insert(surface.id(), SurfaceIdWrapper::SessionLock(native_id)); + states.insert(native_id, State::new(&application, SurfaceIdWrapper::SessionLock(native_id), SurfaceDisplayWrapper { + backend: backend.clone(), + wl_surface: surface.clone() + } + )); + } + SctkEvent::SessionLockSurfaceConfigure { surface, configure, first } => { + if let Some(id) = surface_ids.get(&surface.id()) { + let Some(state) = states.get_mut(&id.inner()) else { + continue; + }; + if state.surface.is_none() { + let c_surface = compositor.create_surface(state.wrapper.clone(), configure.new_size.0, configure.new_size.1); + + state.surface = Some(c_surface); + } + if first { + let user_interface = build_user_interface( + &application, + user_interface::Cache::default(), + &mut renderer, + state.logical_size(), + &state.title, + &mut debug, + *id, + &mut auto_size_surfaces, + &mut ev_proxy + ); + interfaces.insert(id.inner(), user_interface); + } + + state.set_logical_size(configure.new_size.0 as f32 , configure.new_size.1 as f32); + } + + } + SctkEvent::SessionLockSurfaceDone { surface } => { + if let Some(surface_id) = surface_ids.remove(&surface.id()) { + if kbd_surface_id == Some(surface.id()) { + kbd_surface_id = None; + } + auto_size_surfaces.remove(&surface_id); + interfaces.remove(&surface_id.inner()); + states.remove(&surface_id.inner()); + destroyed_surface_ids.insert(surface.id(), surface_id); + } + } + SctkEvent::SessionLockSurfaceScaleFactorChanged { surface, scale_factor, viewport } => { + if let Some(state) = surface_ids + .get(&surface.id()) + .and_then(|id| states.get_mut(&id.inner())) + { + state.wp_viewport = viewport; + state.set_scale_factor(scale_factor); + } + } + _ => {} + } + } + 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.ceil()) as u32, + (bounds.height.ceil()) as u32, + ); + if w == 0 || h == 0 { + error!("Dnd surface has zero size, ignoring"); + continue; + } + let parent_size = states + .get(&origin_id) + .map(|s| s.logical_size()) + .unwrap_or_else(|| Size::new(1024.0, 1024.0)); + if w > parent_size.width as u32 || h > parent_size.height as u32 + { + error!("Dnd surface is too large, ignoring"); + continue; + } + let wrapper = SurfaceDisplayWrapper { + backend: backend.clone(), + wl_surface, + }; + let mut c_surface = + compositor.create_surface(wrapper.clone(), w, h); + compositor.configure_surface(&mut c_surface, w, h); + let mut state = State::new( + &application, + SurfaceIdWrapper::Dnd(native_id), + wrapper, + ); + state.surface = Some(c_surface); + state.set_logical_size(w as f32, h as f32); + 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); + + // Subsurface list should always be empty before `view` + assert!(crate::subsurface_widget::take_subsurfaces().is_empty()); + + // just draw here immediately and never again for dnd icons + // TODO handle scale factor? + let new_mouse_interaction = user_interface.draw( + &mut renderer, + state.theme(), + &Style { + icon_color: state.icon_color(), + text_color: state.text_color(), + scale_factor: state.scale_factor(), + }, + state.cursor(), + ); + + mouse_interaction = new_mouse_interaction; + ev_proxy.send_event(Event::SetCursor(mouse_interaction)); + // Pre-emptively remove cursor focus from other surface so they won't set cursor + for state in states.values_mut() { + state.cursor_position = None; + } + + let subsurfaces = crate::subsurface_widget::take_subsurfaces(); + if let Some(subsurface_state) = subsurface_state.as_mut() { + subsurface_state.update_subsurfaces( + &mut subsurface_ids, + &state.wrapper.wl_surface, + state.id, + &mut state.subsurfaces, + &subsurfaces, + ); + } + + let _ = compositor.present( + &mut renderer, + state.surface.as_mut().unwrap(), + &state.viewport, + Color::TRANSPARENT, + &debug.overlay(), + ); + + surface_ids.insert(id, SurfaceIdWrapper::Dnd(native_id)); + + states.insert(native_id, state); + interfaces.insert(native_id, user_interface); + } + IcedSctkEvent::MainEventsCleared => { + if !redraw_pending + && sctk_events.is_empty() + && messages.is_empty() + { + continue; + } + + 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_all_surfaces(&sctk_events[i]) { + filtered_sctk.push(sctk_events[i].clone()); + i += 1; + } else if event_is_for_surface( + &sctk_events[i], + object_id, + state, + 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, + &subsurface_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_pending || 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.ceil() as u32 != *w + || height.ceil() 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 { + if let Some(d) = wait { + let mut wait_until = Instant::now(); + wait_until += d; + _ = control_sender + .start_send(ControlFlow::WaitUntil(wait_until)); + } else { + _ = control_sender.start_send(ControlFlow::Wait); + } + } + redraw_pending = false; + } + + let mut i = 0; + while i < sctk_events.len() { + let remove = matches!( + sctk_events[i], + SctkEvent::NewOutput { .. } + | SctkEvent::UpdateOutput { .. } + | SctkEvent::RemovedOutput(_) + | SctkEvent::SessionLocked + | SctkEvent::SessionLockFinished + | SctkEvent::SessionUnlocked + | SctkEvent::PopupEvent { .. } + | SctkEvent::LayerSurfaceEvent { .. } + | SctkEvent::WindowEvent { .. } + ); + if remove { + let event = sctk_events.remove(i); + for native_event in event.to_native( + &mut mods, + &surface_ids, + &destroyed_surface_ids, + &subsurface_ids, + ) { + runtime.broadcast(native_event, Status::Ignored); + } + } else { + i += 1; + } + } + + sctk_events.clear(); + // clear the destroyed surfaces after they have been handled + destroyed_surface_ids.clear(); + } + IcedSctkEvent::RedrawRequested(object_id) => { + if let Some(( + native_id, + Some(mut user_interface), + Some(state), + )) = surface_ids.get(&object_id).and_then(|id| { + if matches!(id, SurfaceIdWrapper::Dnd(_)) { + return None; + } + let interface = interfaces.remove(&id.inner()); + let state = states.get_mut(&id.inner()); + Some((*id, interface, state)) + }) { + let Some(mut comp_surface) = state.surface.take() else { + error!("missing surface!"); + continue; + }; + + debug.render_started(); + #[cfg(feature = "a11y")] + if let Some(Some(adapter)) = a11y_enabled + .then(|| adapters.get_mut(&native_id.inner())) + { + use iced_accessibility::{ + accesskit::{Role, Tree, TreeUpdate}, + A11yTree, + }; + // TODO send a11y tree + let child_tree = + user_interface.a11y_nodes(state.cursor()); + let mut root = NodeBuilder::new(Role::Window); + root.set_name(state.title.to_string()); + let window_tree = A11yTree::node_with_child_tree( + A11yNode::new(root, adapter.id), + child_tree, + ); + let tree = Tree::new(NodeId(adapter.id)); + let mut current_operation = + Some(Box::new(OperationWrapper::Id(Box::new( + operation::focusable::find_focused(), + )))); + let mut focus = None; + while let Some(mut operation) = current_operation.take() + { + user_interface + .operate(&renderer, operation.as_mut()); + + match operation.finish() { + operation::Outcome::None => { + } + operation::Outcome::Some(message) => { + match message { + operation::OperationOutputWrapper::Message(_) => { + unimplemented!(); + } + operation::OperationOutputWrapper::Id(id) => { + focus = Some(A11yId::from(id)); + }, + } + } + operation::Outcome::Chain(next) => { + current_operation = Some(Box::new(OperationWrapper::Wrapper(next))); + } + } + } + tracing::debug!( + "focus: {:?}\ntree root: {:?}\n children: {:?}", + &focus, + window_tree + .root() + .iter() + .map(|n| (n.node().role(), n.id())) + .collect::>(), + window_tree + .children() + .iter() + .map(|n| (n.node().role(), n.id())) + .collect::>() + ); + let focus = focus + .filter(|f_id| window_tree.contains(f_id)) + .map(|id| id.into()) + .unwrap_or_else(|| tree.root); + adapter.adapter.update_if_active(|| TreeUpdate { + nodes: window_tree.into(), + tree: Some(tree), + focus, + }); + } + + if state.viewport_changed() { + let physical_size = state.physical_size(); + let mut logical_size = state.logical_size(); + compositor.configure_surface( + &mut comp_surface, + physical_size.width, + physical_size.height, + ); + + debug.layout_started(); + // XXX must add a small number to the autosize surface size here + if auto_size_surfaces.contains_key(&native_id) { + logical_size.width += 0.001; + logical_size.height += 0.001; + } + user_interface = user_interface + .relayout(logical_size, &mut renderer); + debug.layout_finished(); + state.viewport_changed = false; + } + + // Subsurface list should always be empty before `view` + assert!( + crate::subsurface_widget::take_subsurfaces().is_empty() + ); + + debug.draw_started(); + let new_mouse_interaction = user_interface.draw( + &mut renderer, + state.theme(), + &Style { + icon_color: state.icon_color(), + text_color: state.text_color(), + scale_factor: state.scale_factor(), + }, + state.cursor(), + ); + + // Update subsurfaces based on what view requested. + let subsurfaces = + crate::subsurface_widget::take_subsurfaces(); + if let Some(subsurface_state) = subsurface_state.as_mut() { + subsurface_state.update_subsurfaces( + &mut subsurface_ids, + &state.wrapper.wl_surface, + state.id, + &mut state.subsurfaces, + &subsurfaces, + ); + } + + debug.draw_finished(); + + // Set cursor if mouse interaction has changed, and surface has pointer focus + if state.cursor_position.is_some() + && new_mouse_interaction != mouse_interaction + { + mouse_interaction = new_mouse_interaction; + ev_proxy + .send_event(Event::SetCursor(mouse_interaction)); + } + + let _ = + interfaces.insert(native_id.inner(), user_interface); + + if state.frame_pending { + // request a new frame + state.wrapper.wl_surface.frame( + &queue_handle, + state.wrapper.wl_surface.clone(), + ); + } + let _ = compositor.present( + &mut renderer, + &mut comp_surface, + state.viewport(), + state.background_color(), + &debug.overlay(), + ); + // Need commit to get frame event, and update subsurfaces, even if main surface wasn't changed + state.wrapper.wl_surface.commit(); + state.frame_pending = false; + state.surface = Some(comp_surface); + debug.render_finished(); + } + } + 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::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, time) => { + if let Some(id) = surface_ids + .get(&surface.id()) + .or_else(|| Some(&subsurface_ids.get(&surface.id())?.2)) + { + if let Some(state) = states.get_mut(&id.inner()) { + state.set_frame(time); + } + } + } + IcedSctkEvent::Subcompositor(state) => { + subsurface_state = Some(state); + } + } + } + + Ok(()) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum SurfaceIdWrapper { + LayerSurface(SurfaceId), + Window(SurfaceId), + Popup(SurfaceId), + Dnd(SurfaceId), + SessionLock(SurfaceId), +} + +impl SurfaceIdWrapper { + pub fn inner(&self) -> SurfaceId { + match self { + SurfaceIdWrapper::LayerSurface(id) => *id, + SurfaceIdWrapper::Window(id) => *id, + SurfaceIdWrapper::Popup(id) => *id, + SurfaceIdWrapper::Dnd(id) => *id, + SurfaceIdWrapper::SessionLock(id) => *id, + } + } +} + +/// Builds a [`UserInterface`] for the provided [`Application`], logging +/// [`struct@Debug`] information accordingly. +pub fn build_user_interface<'a, A: Application>( + application: &'a A, + cache: user_interface::Cache, + renderer: &mut A::Renderer, + size: Size, + _title: &str, + debug: &mut Debug, + id: SurfaceIdWrapper, + auto_size_surfaces: &mut HashMap< + SurfaceIdWrapper, + (u32, u32, Limits, bool), + >, + ev_proxy: &mut proxy::Proxy>, +) -> UserInterface<'a, A::Message, A::Theme, A::Renderer> +where + ::Theme: StyleSheet, +{ + debug.view_started(); + let mut view = application.view(id.inner()); + debug.view_finished(); + + let size = if let Some((auto_size_w, auto_size_h, limits, dirty)) = + auto_size_surfaces.remove(&id) + { + // TODO would it be ok to diff against the current cache? + let mut tree = Tree::new(view.as_widget_mut()); + let bounds = view + .as_widget() + .layout(&mut tree, renderer, &limits) + .bounds() + .size(); + let (w, h) = ( + (bounds.width.ceil()).max(1.0) as u32, + (bounds.height.ceil()).max(1.0) as u32, + ); + let dirty = dirty + || w != size.width.ceil() as u32 + || h != size.height.ceil() 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(_) => {} + }; + } + + // XXX must add a small amount to the size. + // Layout seems to sometimes build the interface slightly + // differently when given a size versus just limits + // this is problematic for autosize surfaces that rely on the size previously calculated + Size::new(w as f32 + 0.001, h as f32 + 0.001) + } else { + size + }; + + 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, + // Time of last frame event, or 0 + frame_pending: bool, + last_frame_time: u32, + needs_redraw: bool, + first: bool, + wp_viewport: Option, + interface_state: user_interface::State, + surface: Option, + wrapper: SurfaceDisplayWrapper, + subsurfaces: Vec, +} + +impl State +where + ::Theme: application::StyleSheet, +{ + /// Creates a new [`State`] for the provided [`Application`] + pub fn new( + application: &A, + id: SurfaceIdWrapper, + wrapper: SurfaceDisplayWrapper, + ) -> Self { + let title = application.title(id.inner()); + let scale_factor = application.scale_factor(id.inner()); + let theme = application.theme(id.inner()); + let appearance = theme.appearance(&application.style()); + let viewport = Viewport::with_physical_size(Size::new(1, 1), 1.0); + + Self { + id, + title, + application_scale_factor: scale_factor, + surface_scale_factor: 1.0, // assumed to be 1.0 at first + viewport, + viewport_changed: true, + // TODO: Encode cursor availability in the type-system + cursor_position: None, + modifiers: Modifiers::default(), + theme, + appearance, + application: PhantomData, + frame_pending: false, + last_frame_time: 0, + needs_redraw: false, + first: true, + wp_viewport: None, + interface_state: user_interface::State::Outdated, + surface: None, + wrapper, + subsurfaces: Vec::new(), + } + } + + 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 + } + + fn set_frame(&mut self, time: u32) { + // If we get frame events from mulitple subsurface, should have same time. So ignore if + // time isn't newer. + if time == 0 || time > self.last_frame_time { + self.frame_pending = true; + self.last_frame_time = time; + } + } + + pub(crate) fn first(&self) -> bool { + 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: f32, h: f32) { + let old_size = self.viewport.logical_size(); + if !approx_eq!(f32, w as f32, old_size.width, F32Margin::default()) + || !approx_eq!(f32, h as f32, old_size.height, F32Margin::default()) + { + let logical_size = Size::::new(w, h); + self.viewport_changed = true; + self.viewport = + Viewport::with_logical_size(logical_size, self.scale_factor()); + if let Some(wp_viewport) = self.wp_viewport.as_ref() { + wp_viewport.set_destination( + logical_size.width.ceil() as i32, + logical_size.height.ceil() 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(); + self.surface_scale_factor = scale_factor; + self.viewport = Viewport::with_logical_size( + logical_size, + self.application_scale_factor * self.surface_scale_factor, + ); + if let Some(wp_viewport) = self.wp_viewport.as_ref() { + wp_viewport.set_destination( + logical_size.width.ceil() as i32, + logical_size.height.ceil() as i32, + ); + } + } + } + + // TODO use a type to encode cursor availability + /// Returns the current cursor position of the [`State`]. + pub fn cursor(&self) -> mouse::Cursor { + self.cursor_position + .map(|cursor_position| { + let scale_factor = self.application_scale_factor; + assert!( + scale_factor.is_sign_positive() && scale_factor.is_normal() + ); + let logical: LogicalPosition = + cursor_position.to_logical(scale_factor); + + Point { + x: logical.x as f32, + y: logical.y as f32, + } + }) + .map(mouse::Cursor::Available) + .unwrap_or(mouse::Cursor::Unavailable) + } + + /// Returns the current keyboard modifiers of the [`State`]. + pub fn modifiers(&self) -> Modifiers { + self.modifiers + } + + /// Returns the current theme of the [`State`]. + pub fn theme(&self) -> &::Theme { + &self.theme + } + + /// Returns the current background [`Color`] of the [`State`]. + pub fn background_color(&self) -> Color { + self.appearance.background_color + } + + /// Returns the current text [`Color`] of the [`State`]. + pub fn text_color(&self) -> Color { + self.appearance.text_color + } + + /// Returns the current icon [`Color`] of the [`State`]. + pub fn icon_color(&self) -> Color { + self.appearance.icon_color + } + + pub fn set_cursor_position(&mut self, p: Option>) { + self.cursor_position = + p.map(|p| p.to_physical(self.application_scale_factor)); + } + + fn synchronize(&mut self, application: &A) { + // Update theme and appearance + self.theme = application.theme(self.id.inner()); + self.appearance = self.theme.appearance(&application.style()); + } +} + +// XXX Ashley careful, A, E, C must be exact same as in run_instance, or the subscription map type will have a different hash +/// Updates an [`Application`] by feeding it the provided messages, spawning any +/// resulting [`Command`], and tracking its [`Subscription`] +pub(crate) fn update( + application: &mut A, + cache: &mut user_interface::Cache, + state: Option<&State>, + renderer: &mut A::Renderer, + runtime: MyRuntime, + proxy: &mut proxy::Proxy>, + debug: &mut Debug, + messages: &mut Vec, + actions: &mut Vec>, + graphics_info: impl FnOnce() -> compositor::Information + Copy, + auto_size_surfaces: &mut HashMap< + SurfaceIdWrapper, + (u32, u32, Limits, bool), + >, + clipboard: &mut Clipboard, +) where + A: Application + 'static, + E: Executor + 'static, + C: iced_graphics::Compositor + 'static, + ::Theme: StyleSheet, +{ + let actions_ = std::mem::take(actions); + for a in actions_ { + if let Some(a) = handle_actions( + application, + cache, + state, + renderer, + a, + runtime, + proxy, + debug, + graphics_info, + auto_size_surfaces, + clipboard, + ) { + actions.push(a); + } + } + for message in messages.drain(..) { + debug.log_message(&message); + + debug.update_started(); + let command = runtime.enter(|| application.update(message)); + debug.update_finished(); + + run_command( + application, + cache, + state, + renderer, + command, + runtime, + proxy, + debug, + graphics_info, + auto_size_surfaces, + actions, + clipboard, + ) + } + + runtime.track( + application + .subscription() + .map(subscription_map::) + .into_recipes(), + ); +} + +type MyRuntime<'a, E, M> = &'a mut Runtime>, Event>; + +/// Runs the actions of a [`Command`]. +fn run_command( + application: &A, + cache: &mut user_interface::Cache, + state: Option<&State>, + renderer: &mut A::Renderer, + command: Command, + runtime: MyRuntime, + proxy: &mut proxy::Proxy>, + debug: &mut Debug, + graphics_info: impl FnOnce() -> compositor::Information + Copy, + auto_size_surfaces: &mut HashMap< + SurfaceIdWrapper, + (u32, u32, Limits, bool), + >, + actions: &mut Vec>, + clipboard: &mut Clipboard, +) where + A: Application, + E: Executor, + ::Theme: StyleSheet, + C: Compositor, +{ + for action in command.actions() { + if let Some(a) = handle_actions( + application, + cache, + state, + renderer, + action, + runtime, + proxy, + debug, + graphics_info, + auto_size_surfaces, + clipboard, + ) { + actions.push(a); + } + } +} + +fn handle_actions( + application: &A, + cache: &mut user_interface::Cache, + state: Option<&State>, + renderer: &mut A::Renderer, + action: command::Action, + runtime: MyRuntime, + proxy: &mut proxy::Proxy>, + debug: &mut Debug, + _graphics_info: impl FnOnce() -> compositor::Information + Copy, + auto_size_surfaces: &mut HashMap< + SurfaceIdWrapper, + (u32, u32, Limits, bool), + >, + clipboard: &mut Clipboard, +) -> Option> +where + A: Application, + E: Executor, + ::Theme: StyleSheet, + C: Compositor, +{ + match action { + command::Action::Future(future) => { + runtime + .spawn(Box::pin(future.map(|e| { + Event::SctkEvent(IcedSctkEvent::UserEvent(e)) + }))); + } + command::Action::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) => { + let contents = clipboard.read(); + let message = s_to_msg(contents); + proxy.send_event(Event::Message(message)); + } + clipboard::Action::Write(contents) => { + clipboard.write(contents) + } + clipboard::Action::WriteData(contents) => { + clipboard.write_data(ClipboardStoreData(contents)) + }, + clipboard::Action::ReadData(allowed, to_msg) => { + let contents = clipboard.read_data(allowed); + let message = to_msg(contents); + proxy.send_event(Event::Message(message)); + }, + clipboard::Action::ReadPrimary(s_to_msg) => { + let contents = clipboard.read_primary(); + let message = s_to_msg(contents); + proxy.send_event(Event::Message(message)); + }, + clipboard::Action::WritePrimary(content) => { + clipboard.write_primary(content) + }, + clipboard::Action::WritePrimaryData(content) => { + clipboard.write_primary_data(ClipboardStoreData(content)) + }, + clipboard::Action::ReadPrimaryData(a, to_msg) => { + let contents = clipboard.read_primary_data(a); + let message = to_msg(contents); + proxy.send_event(Event::Message(message)); + }, + }, + command::Action::Window(action) => { + if let Ok(a) = action.try_into() { + return handle_actions(application, cache, state, renderer, command::Action::PlatformSpecific(platform_specific::Action::Wayland(command::platform_specific::wayland::Action::Window(a))), runtime, proxy, debug, _graphics_info, auto_size_surfaces, clipboard); + } + } + command::Action::Window(action) => {} + command::Action::System(action) => match action { + system::Action::QueryInformation(_tag) => { + #[cfg(feature = "system")] + { + 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.ceil()).max(1.0) as u32, (bounds.height.ceil()).max(1.0) as u32); + auto_size_surfaces.insert(SurfaceIdWrapper::LayerSurface(builder.id), (w, h, builder.size_limits, false)); + builder.size = Some((Some(bounds.width as u32), Some(bounds.height as u32))); + } + 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.ceil()).max(1.0) as u32, (bounds.height.ceil()).max(1.0) as u32); + auto_size_surfaces.insert(SurfaceIdWrapper::Window(builder.window_id), (w, h, builder.size_limits, false)); + builder.size = (bounds.width as u32, bounds.height as u32); + } + 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.ceil().max(1.0) as u32, bounds.height.ceil().max(1.0) as u32); + auto_size_surfaces.insert(SurfaceIdWrapper::Popup(popup.id), (w, h, popup.positioner.size_limits, false)); + popup.positioner.size = Some((w, h)); + } + proxy.send_event(Event::Popup(popup::Action::Popup{popup, _phantom})); + } else { + proxy.send_event(Event::Popup(popup_action)); + } + } + command::Action::PlatformSpecific(platform_specific::Action::Wayland(platform_specific::wayland::Action::DataDevice(data_device_action))) => { + proxy.send_event(Event::DataDevice(data_device_action)); + } + command::Action::PlatformSpecific( + platform_specific::Action::Wayland( + platform_specific::wayland::Action::Activation(activation_action) + ) + ) => { + proxy.send_event(Event::Activation(activation_action)); + } + command::Action::PlatformSpecific(platform_specific::Action::Wayland(platform_specific::wayland::Action::SessionLock(session_lock_action))) => { + proxy.send_event(Event::SessionLock(session_lock_action)); + } + _ => {} + }; + None +} + +pub fn build_user_interfaces<'a, A, C>( + application: &'a A, + renderer: &mut A::Renderer, + debug: &mut Debug, + states: &HashMap>, + mut pure_states: HashMap, + auto_size_surfaces: &mut HashMap< + SurfaceIdWrapper, + (u32, u32, Limits, bool), + >, + ev_proxy: &mut proxy::Proxy>, +) -> HashMap< + SurfaceId, + UserInterface< + 'a, + ::Message, + ::Theme, + ::Renderer, + >, +> +where + A: Application + 'static, + ::Theme: StyleSheet, + C: Compositor, +{ + let mut interfaces = HashMap::new(); + + // TODO ASHLEY make sure Ids are iterated in the same order every time for a11y + for (id, pure_state) in pure_states.drain().sorted_by(|a, b| a.0.cmp(&b.0)) + { + let state = &states.get(&id).unwrap(); + + let user_interface = build_user_interface( + application, + pure_state, + renderer, + state.logical_size(), + &state.title, + debug, + state.id, + auto_size_surfaces, + ev_proxy, + ); + + let _ = interfaces.insert(id, user_interface); + } + + interfaces +} + +fn event_is_for_all_surfaces(evt: &SctkEvent) -> bool { + match evt { + SctkEvent::DataSource(_) => true, + _ => false, + } +} + +// Determine if `SctkEvent` is for surface with given object id. +fn event_is_for_surface<'a, A, C>( + evt: &SctkEvent, + object_id: &ObjectId, + state: &State, + has_kbd_focus: bool, +) -> bool +where + A: Application + 'static, + ::Theme: StyleSheet, + C: Compositor, +{ + match evt { + SctkEvent::SeatEvent { id, .. } => &id.id() == object_id, + SctkEvent::PointerEvent { variant, .. } => { + let event_object_id = variant.surface.id(); + &event_object_id == object_id + || state + .subsurfaces + .iter() + .any(|s| s.wl_surface.id() == event_object_id) + } + SctkEvent::KeyboardEvent { variant, .. } => match variant { + KeyboardEventVariant::Leave(id) => &id.id() == object_id, + _ => has_kbd_focus, + }, + SctkEvent::TouchEvent { surface, .. } => { + let event_object_id = surface.id(); + &event_object_id == object_id + || state + .subsurfaces + .iter() + .any(|s| s.wl_surface.id() == event_object_id) + } + SctkEvent::WindowEvent { id, .. } => &id.id() == object_id, + SctkEvent::LayerSurfaceEvent { id, .. } => &id.id() == object_id, + SctkEvent::PopupEvent { id, .. } => &id.id() == object_id, + SctkEvent::NewOutput { .. } + | SctkEvent::UpdateOutput { .. } + | SctkEvent::RemovedOutput(_) => false, + SctkEvent::ScaleFactorChanged { id, .. } => &id.id() == object_id, + SctkEvent::DndOffer { surface, .. } => { + let event_object_id = surface.id(); + &event_object_id == object_id + || state + .subsurfaces + .iter() + .any(|s| s.wl_surface.id() == event_object_id) + } + SctkEvent::DataSource(_) => true, + SctkEvent::SessionLocked => false, + SctkEvent::SessionLockFinished => false, + SctkEvent::SessionLockSurfaceCreated { surface, .. } => { + &surface.id() == object_id + } + SctkEvent::SessionLockSurfaceConfigure { surface, .. } => { + &surface.id() == object_id + } + SctkEvent::SessionLockSurfaceDone { surface } => { + &surface.id() == object_id + } + SctkEvent::SessionLockSurfaceScaleFactorChanged { surface, .. } => { + &surface.id() == object_id + } + SctkEvent::SessionUnlocked => false, + } +} diff --git a/sctk/src/clipboard/clipboard.rs b/sctk/src/clipboard/clipboard.rs new file mode 100644 index 0000000000..17941aa461 --- /dev/null +++ b/sctk/src/clipboard/clipboard.rs @@ -0,0 +1,133 @@ +//! Access the clipboard. +pub use iced_runtime::clipboard::Action; + +use iced_runtime::command::{self, Command}; +use raw_window_handle::HasDisplayHandle; +use window_clipboard::mime::{self, ClipboardStoreData}; + +/// 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(window_clipboard::Clipboard), + Unavailable, +} + +impl Clipboard { + pub unsafe fn connect(display: &impl HasDisplayHandle) -> Clipboard { + let context = window_clipboard::Clipboard::connect(display); + + Clipboard { + state: context.map(State::Connected).unwrap_or(State::Unavailable), + } + } + + pub(crate) fn state(&self) -> &State { + &self.state + } + + /// Creates a new [`Clipboard`] that isn't associated with a window. + /// This clipboard will never contain a copied value. + pub fn unconnected() -> Clipboard { + Clipboard { + state: State::Unavailable, + } + } +} + +impl iced_runtime::core::clipboard::Clipboard for Clipboard { + fn read(&self) -> Option { + match &self.state { + State::Connected(clipboard) => clipboard.read().ok(), + State::Unavailable => None, + } + } + + fn write(&mut self, contents: String) { + match &mut self.state { + State::Connected(clipboard) => _ = clipboard.write(contents), + State::Unavailable => {} + } + } + + fn read_primary(&self) -> Option { + match &self.state { + State::Connected(clipboard) => { + clipboard.read_primary().and_then(|res| res.ok()) + } + State::Unavailable => None, + } + } + + fn write_primary(&mut self, contents: String) { + match &mut self.state { + State::Connected(clipboard) => { + _ = clipboard.write_primary(contents) + } + State::Unavailable => {} + } + } + + fn read_data(&self, mimes: Vec) -> Option<(Vec, String)> { + match &self.state { + State::Connected(clipboard) => { + clipboard.read_raw(mimes).and_then(|res| res.ok()) + } + State::Unavailable => None, + } + } + + fn write_data( + &mut self, + contents: ClipboardStoreData< + Box, + >, + ) { + match &mut self.state { + State::Connected(clipboard) => _ = clipboard.write_data(contents), + State::Unavailable => {} + } + } + + fn read_primary_data( + &self, + mimes: Vec, + ) -> Option<(Vec, String)> { + match &self.state { + State::Connected(clipboard) => { + clipboard.read_primary_raw(mimes).and_then(|res| res.ok()) + } + State::Unavailable => None, + } + } + + fn write_primary_data( + &mut self, + contents: ClipboardStoreData< + Box, + >, + ) { + match &mut self.state { + State::Connected(clipboard) => { + _ = clipboard.write_primary_data(contents) + } + State::Unavailable => {} + } + } +} + +/// 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/clipboard/mod.rs b/sctk/src/clipboard/mod.rs new file mode 100644 index 0000000000..dc75d699af --- /dev/null +++ b/sctk/src/clipboard/mod.rs @@ -0,0 +1,43 @@ +#[cfg(feature = "clipboard")] +mod clipboard; + +#[cfg(not(feature = "clipboard"))] +mod clipboard { + use std::ffi::c_void; + /// A buffer for short-term storage and transfer within and between + /// applications. + #[allow(missing_debug_implementations)] + pub struct Clipboard; + + pub(crate) enum State { + Connected(()), + Unavailable, + } + + impl Clipboard { + pub unsafe fn connect( + _display: &impl raw_window_handle::HasDisplayHandle, + ) -> Clipboard { + Clipboard + } + + pub(crate) fn state(&self) -> &State { + &State::Connected(()) + } + + /// Creates a new [`Clipboard`] + pub fn unconnected() -> Clipboard { + Clipboard + } + } + + impl iced_runtime::core::clipboard::Clipboard for Clipboard { + fn read(&self) -> Option { + None + } + + fn write(&mut self, _contents: String) {} + } +} + +pub use clipboard::*; 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..4bc0c92ba9 --- /dev/null +++ b/sctk/src/commands/data_device.rs @@ -0,0 +1,121 @@ +//! 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; + +use crate::core::Vector; + +/// start an internal drag and drop operation. Events will only be delivered to the same client. +/// The client is responsible for data transfer. +pub fn start_internal_drag( + 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<(DndIcon, Vector)>, + 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..dc591cdd73 --- /dev/null +++ b/sctk/src/commands/window.rs @@ -0,0 +1,99 @@ +//! 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 maximize(id: window::Id, maximized: bool) -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::Window( + if maximized { + wayland::window::Action::Maximize { id } + } else { + wayland::window::Action::UnsetMaximize { id } + }, + )), + )) +} + +pub fn toggle_maximize(id: window::Id) -> Command { + Command::single(command::Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::Window( + 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..f1f46d99f9 --- /dev/null +++ b/sctk/src/conversion.rs @@ -0,0 +1,96 @@ +use iced_futures::core::mouse::Interaction; +use iced_runtime::core::{ + keyboard, + mouse::{self, ScrollDelta}, +}; +use sctk::{ + reexports::client::protocol::wl_pointer::AxisSource, + seat::{ + keyboard::Modifiers, + pointer::{ + AxisScroll, CursorIcon, BTN_EXTRA, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT, + BTN_SIDE, + }, + }, +}; + +/// 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..ae662f3cac --- /dev/null +++ b/sctk/src/event_loop/adapter.rs @@ -0,0 +1,31 @@ +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::sync::{Arc, Mutex}; + +pub enum A11yWrapper { + Enabled, + Event(ActionRequestEvent), +} + +pub struct IcedSctkAdapter { + pub(crate) id: u64, + 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(&mut 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..f9994d0656 --- /dev/null +++ b/sctk/src/event_loop/mod.rs @@ -0,0 +1,1500 @@ +#[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::{LogicalPosition, LogicalSize, PhysicalPosition}, + handlers::{ + activation::IcedRequestData, + wp_fractional_scaling::FractionalScalingManager, + wp_viewporter::ViewporterState, + }, + sctk_event::{ + DataSourceEvent, DndOfferEvent, IcedSctkEvent, + LayerSurfaceEventVariant, PopupEventVariant, SctkEvent, StartCause, + WindowEventVariant, + }, + settings, + subsurface_widget::SubsurfaceState, +}; +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, + globals::GlobalData, + 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, LayerSurface}, + 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, + touch_points: HashMap::new(), + 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( + 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()); + let root = NodeId(node_id); + TreeUpdate { + nodes: vec![(root, node)], + tree: Some(Tree::new(root)), + focus: root, + } + }, + Box::new(adapter::IcedSctkActionHandler { + wl_surface: surface.clone(), + event_list: self.a11y_events.clone(), + }), + ), + 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; + + let mut cursor_position = HashMap::<_, LogicalPosition>::new(); + + callback( + IcedSctkEvent::NewEvents(StartCause::Init), + &self.state, + &mut control_flow, + ); + + // XXX don't re-bind? + let wl_compositor = self + .state + .registry_state + .bind_one(&self.state.queue_handle, 1..=6, GlobalData) + .unwrap(); + let wl_subcompositor = self.state.registry_state.bind_one( + &self.state.queue_handle, + 1..=1, + GlobalData, + ); + let wp_viewporter = self.state.registry_state.bind_one( + &self.state.queue_handle, + 1..=1, + GlobalData, + ); + let wl_shm = self + .state + .registry_state + .bind_one(&self.state.queue_handle, 1..=1, GlobalData) + .unwrap(); + let wp_dmabuf = self + .state + .registry_state + .bind_one(&self.state.queue_handle, 2..=4, GlobalData) + .ok(); + let wp_alpha_modifier = self + .state + .registry_state + .bind_one(&self.state.queue_handle, 1..=1, ()) + .ok(); + if let Ok(wl_subcompositor) = wl_subcompositor { + if let Ok(wp_viewporter) = wp_viewporter { + callback( + IcedSctkEvent::Subcompositor(SubsurfaceState { + wl_compositor, + wl_subcompositor, + wp_viewporter, + wl_shm, + wp_dmabuf, + wp_alpha_modifier, + qh: self.state.queue_handle.clone(), + buffers: HashMap::new(), + unmapped_subsurfaces: Vec::new(), + }), + &self.state, + &mut control_flow, + ); + } else { + tracing::warn!( + "No `wp_viewporter`. Subsurfaces not supported." + ); + } + } else { + tracing::warn!("No `wl_subcompositor`. Subsurfaces not supported."); + } + + let mut sctk_event_sink_back_buffer = Vec::new(); + let mut compositor_event_back_buffer = Vec::new(); + let mut frame_event_back_buffer = Vec::new(); + + // 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(..), + .. + } + | SctkEvent::SessionLockSurfaceScaleFactorChanged { + .. + } => 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.0, event.1), + &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::PointerEvent { ref variant, .. } => { + let surface_id = variant.surface.id(); + + cursor_position.insert( + surface_id, + LogicalPosition::new( + variant.position.0, + variant.position.1, + ), + ); + } + + 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, + } => { + cursor_position.remove(&id.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, + ); + } + + continue; + } + + 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, + ); + } + + continue; + } + _ => (), + } + + 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(seat) = self.state.seats.get_mut(0) { + let icon = conversion::cursor_icon(iced_icon); + seat.icon = Some(icon); + seat.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(height).unwrap_or(NonZeroU32::new(1).unwrap()))); + // TODO Ashley maybe don't force window size? + pending_redraws.push(window.window.wl_surface().id()); + if window.last_configure.is_some() { + sticky_exit_callback( + IcedSctkEvent::SctkEvent(SctkEvent::WindowEvent { variant: WindowEventVariant::Size(window.current_size, window.window.wl_surface().clone(), false), id: window.window.wl_surface().clone()}), + &self.state, + &mut control_flow, + &mut callback, + ); + } + } + }, + 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 } => { + if let (Some(window), Some((seat, last_press))) = (self.state.windows.iter_mut().find(|w| w.id == id), self.state.seats.first().and_then(|seat| seat.last_ptr_press.map(|p| (&seat.seat, p.2)))) { + let surface_id = window.window.wl_surface().id(); + + let cursor_position = cursor_position.get(&surface_id) + .cloned() + .unwrap_or_default(); + + // Cursor position does not need to be scaled here. + let PhysicalPosition { x, y } = cursor_position.to_physical::(1.0); + + window.window.xdg_toplevel().show_window_menu(seat, last_press, x as i32, y as i32); + to_commit.insert(id, window.window.wl_surface().clone()); + } + }, + 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().and_then(|o| o.offer.as_ref()) { + Some(d) => d, + None => continue, + }; + drag_offer.accept_mime_type(drag_offer.serial, mime_type); + } + platform_specific::wayland::data_device::ActionInner::StartInternalDnd { origin_id, icon_id } => { + let qh = &self.state.queue_handle.clone(); + 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, + }; + // Get last pointer press or touch down serial, whichever is newer + let Some(serial) = seat.last_ptr_press.map(|s| s.2).max(seat.last_touch_down.map(|s| s.2)) else { + continue; + }; + + let origin = match self + .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, offset)) = icon_id{ + let icon_native_id = match &icon_id { + DndIcon::Custom(icon_id) => *icon_id, + DndIcon::Widget(icon_id, _) => *icon_id, + }; + let wl_surface = self.state.compositor_state.create_surface(qh); + if offset != crate::core::Vector::ZERO { + wl_surface.offset(offset.x as i32, offset.y as i32); + } + source.start_drag(device, &origin, Some(&wl_surface), serial); + sticky_exit_callback( + IcedSctkEvent::DndSurfaceCreated( + 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().filter(|o| o.offer.is_some()) { + if offer.dropped { + offer.offer.unwrap().finish(); + } + else { + self.state.dnd_offer = Some(offer); + } + } + }, + platform_specific::wayland::data_device::ActionInner::DndCancelled => { + if let Some(mut source) = self.state.dnd_source.take() { + if let Some(s) = source.icon_surface.take() { + s.0.destroy(); + } + sticky_exit_callback( + IcedSctkEvent::SctkEvent(SctkEvent::DataSource(DataSourceEvent::DndCancelled)), + &self.state, + &mut control_flow, + &mut callback + ); + } + }, + platform_specific::wayland::data_device::ActionInner::RequestDndData (mime_type) => { + if let Some(dnd_offer) = self.state.dnd_offer.as_mut() { + let Some(offer) = dnd_offer.offer.as_ref() else { + continue; + }; + let read_pipe = match offer.receive(mime_type.clone()) { + Ok(p) => p, + Err(_) => continue, // TODO error handling + }; + 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 Some(offer) = dnd_offer.offer.as_ref() else { + return PostAction::Remove; + }; + let (mime_type, data, token) = match dnd_offer.cur_read.take() { + Some(s) => s, + None => return PostAction::Continue, + }; + 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.surface.clone() }); + if dnd_offer.dropped { + 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().and_then(|o| o.offer.as_ref()) { + 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 => { + if let Some(session_lock) = self.state.session_lock.take() { + session_lock.unlock(); + } + // Make sure server processes unlock before client exits + let _ = self.state.connection.roundtrip(); + sticky_exit_callback( + 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 + }) + { + let surface = self.state.lock_surfaces.remove(i); + sticky_exit_callback( + IcedSctkEvent::SctkEvent(SctkEvent::SessionLockSurfaceDone { + surface: surface.session_lock_surface.wl_surface().clone() + }), + &self.state, + &mut control_flow, + &mut callback, + ); + } + } + } + } + } + + // 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..b86eacab2c --- /dev/null +++ b/sctk/src/event_loop/state.rs @@ -0,0 +1,920 @@ +use std::{ + collections::HashMap, + fmt::{Debug, Formatter}, + num::NonZeroU32, +}; + +use crate::{ + application::Event, + dpi::LogicalSize, + handlers::{ + shell::layer, 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, + }, + }, + core::{touch, Point}, + 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::{ + delegate_noop, + protocol::{ + wl_keyboard::WlKeyboard, + wl_output::WlOutput, + wl_region::WlRegion, + wl_seat::WlSeat, + wl_subsurface::WlSubsurface, + 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) last_touch_down: Option<(u32, i32, u32)>, // (time, point, serial) + pub(crate) _modifiers: Modifiers, + pub(crate) data_device: DataDevice, + // Cursor icon currently set (by CSDs, or application) + pub(crate) active_icon: Option, + // Cursor icon set by application + pub(crate) icon: Option, +} + +impl SctkSeat { + pub(crate) fn set_cursor(&mut self, conn: &Connection, icon: CursorIcon) { + if let Some(ptr) = self.ptr.as_ref() { + ptr.set_cursor(conn, icon); + self.active_icon = Some(icon); + } + } +} + +#[derive(Debug, Clone)] +pub struct SctkWindow { + pub(crate) id: window::Id, + pub(crate) window: Window, + pub(crate) scale_factor: Option, + pub(crate) requested_size: Option<(NonZeroU32, NonZeroU32)>, + pub(crate) current_size: (NonZeroU32, NonZeroU32), + pub(crate) last_configure: Option, + pub(crate) resizable: Option, + /// Requests that SCTK window should perform. + 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, logical_size.height)); + self.update_size((Some(logical_size.width), Some(logical_size.height))) + } + + pub(crate) fn update_size( + &mut self, + (width, height): (Option, Option), + ) { + let (width, height) = ( + width.unwrap_or_else(|| self.current_size.0), + height.unwrap_or_else(|| self.current_size.1), + ); + if self.current_size == (width, height) { + return; + } + self.window + .set_window_geometry(0, 0, width.get(), height.get()); + self.current_size = (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(crate) scale_factor: Option, + pub(crate) wp_fractional_scale: Option, + pub(crate) wp_viewport: 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: Option, + pub(crate) cur_read: Option<(String, Vec, RegistrationToken)>, + pub(crate) surface: WlSurface, +} + +#[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, + pub(crate) touch_points: HashMap, + + /// Window updates, which are coming from SCTK or the compositor, which require + /// calling back to the sctk's downstream. They are handled right in the event loop, + /// 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<(WlSurface, u32)>, + + /// 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(), + }); + } + + if let Some(l) = self + .lock_surfaces + .iter_mut() + .find(|l| l.session_lock_surface.wl_surface() == surface) + { + if legacy && self.fractional_scaling_manager.is_some() { + return; + } + l.scale_factor = Some(scale_factor); + let wl_surface = l.session_lock_surface.wl_surface(); + if legacy { + let _ = wl_surface.set_buffer_scale(scale_factor as i32); + } + self.compositor_updates.push( + SctkEvent::SessionLockSurfaceScaleFactorChanged { + surface: wl_surface.clone(), + scale_factor, + viewport: l.wp_viewport.clone(), + }, + ); + } + // TODO winit sets cursor size after handling the change for the window, so maybe that should be done as well. + } +} + +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, + ); + if let Ok(constraint_adjustment) = + settings.positioner.constraint_adjustment.try_into() + { + positioner.set_constraint_adjustment(constraint_adjustment); + } + positioner.set_gravity(settings.positioner.gravity); + positioner.set_offset( + settings.positioner.offset.0, + 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 Some(parent_layer_surface) = self + .layer_surfaces + .iter() + .find(|w| w.surface.wl_surface() == parent) + else { + return Err(PopupCreationError::ParentMissing); + }; + 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 Some(parent_window) = self + .windows + .iter() + .find(|w| w.window.wl_surface() == parent) + else { + return Err(PopupCreationError::ParentMissing); + }; + ( + 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 Some(parent_xdg) = self.popups.iter().find_map(|p| { + (p.popup.wl_surface() == parent) + .then(|| p.popup.xdg_surface()) + }) else { + return Err(PopupCreationError::ParentMissing); + }; + + ( + &toplevel, + 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) + }); + + let w = NonZeroU32::new(size.0 as u32) + .unwrap_or_else(|| NonZeroU32::new(1).unwrap()); + let h = NonZeroU32::new(size.1 as u32) + .unwrap_or_else(|| NonZeroU32::new(1).unwrap()); + self.windows.push(SctkWindow { + id: window_id, + window, + scale_factor: None, + requested_size: Some((w, h)), + current_size: (w, h), + 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 { + let region = self + .compositor_state + .wl_compositor() + .create_region(&self.queue_handle, ()); + layer_surface.set_input_region(Some(®ion)); + region.destroy(); + } + 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, + ); + let wp_viewport = self.viewporter_state.as_ref().map(|state| { + state.get_viewport( + session_lock_surface.wl_surface(), + &self.queue_handle, + ) + }); + let wp_fractional_scale = + self.fractional_scaling_manager.as_ref().map(|fsm| { + fsm.fractional_scaling( + session_lock_surface.wl_surface(), + &self.queue_handle, + ) + }); + self.lock_surfaces.push(SctkLockSurface { + id, + session_lock_surface, + last_configure: None, + wp_viewport, + wp_fractional_scale, + scale_factor: None, + }); + Some(wl_surface) + } else { + None + } + } +} + +delegate_noop!(@ SctkState: ignore WlSubsurface); +delegate_noop!(@ SctkState: ignore WlRegion); 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..dd5e3b429f --- /dev/null +++ b/sctk/src/handlers/compositor.rs @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MPL-2.0-only +use sctk::{ + compositor::CompositorHandler, + delegate_compositor, + reexports::client::{ + protocol::{wl_output, 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, + ) { + // TODO time; map subsurface to parent:w + self.frame_events.push((surface.clone(), 0)); + } + + fn transform_changed( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _surface: &wl_surface::WlSurface, + _new_transform: wl_output::Transform, + ) { + // TODO + // this is not required + } + + fn surface_enter( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &wl_surface::WlSurface, + _: &wl_output::WlOutput, + ) { + } + + fn surface_leave( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &wl_surface::WlSurface, + _: &wl_output::WlOutput, + ) { + } +} + +delegate_compositor!(@ SctkState); 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..e7d950363e --- /dev/null +++ b/sctk/src/handlers/data_device/data_device.rs @@ -0,0 +1,148 @@ +use sctk::{ + data_device_manager::{ + data_device::DataDeviceHandler, data_offer::DragOffer, + }, + reexports::client::{ + protocol::{wl_data_device, wl_surface::WlSurface}, + 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, + x: f64, + y: f64, + s: &WlSurface, + ) { + 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(); + let mime_types = drag_offer + .as_ref() + .map(|offer| offer.with_mime_types(|types| types.to_vec())) + .unwrap_or_default(); + self.dnd_offer = Some(SctkDragOffer { + dropped: false, + offer: drag_offer.clone(), + cur_read: None, + surface: s.clone(), + }); + self.sctk_events.push(SctkEvent::DndOffer { + event: DndOfferEvent::Enter { mime_types, x, y }, + surface: s.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.surface.clone(), + }); + } + } + + fn motion( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + wl_data_device: &wl_data_device::WlDataDevice, + x: f64, + y: f64, + ) { + 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().and_then(|o| o.offer.as_ref()) + { + return; + } + + let Some(surface) = offer.as_ref().map(|o| o.surface.clone()) else { + return; + }; + + self.sctk_events.push(SctkEvent::DndOffer { + event: DndOfferEvent::Motion { x, y }, + surface, + }); + } + + 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(dnd_offer) = self.dnd_offer.as_mut() { + if data_device.data().drag_offer() != dnd_offer.offer { + return; + } + self.sctk_events.push(SctkEvent::DndOffer { + event: DndOfferEvent::DropPerformed, + surface: dnd_offer.surface.clone(), + }); + dnd_offer.dropped = true; + } + } +} diff --git a/sctk/src/handlers/data_device/data_offer.rs b/sctk/src/handlers/data_device/data_offer.rs new file mode 100644 index 0000000000..5ef6381ceb --- /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.as_ref().map(|o| o.inner()) == Some(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.as_ref().map(|o| o.inner()) == Some(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..be989e6bf0 --- /dev/null +++ b/sctk/src/handlers/mod.rs @@ -0,0 +1,42 @@ +// 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 subcompositor; +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..a7a865a4a3 --- /dev/null +++ b/sctk/src/handlers/seat/keyboard.rs @@ -0,0 +1,201 @@ +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, + layout: u32, + ) { + 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..eec170964e --- /dev/null +++ b/sctk/src/handlers/seat/pointer.rs @@ -0,0 +1,145 @@ +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.and_then(|border| { + let (width, height) = w.current_size; + let (width, height) = + (width.get() as f64, height.get() as f64); + let (x, y) = e.position; + let left_edge = x < border; + let top_edge = y < border; + let right_edge = x > width - border; + let bottom_edge = y > height - border; + + if left_edge && top_edge { + Some((ResizeEdge::TopLeft, w)) + } else if left_edge && bottom_edge { + Some((ResizeEdge::BottomLeft, w)) + } else if right_edge && top_edge { + Some((ResizeEdge::TopRight, w)) + } else if right_edge && bottom_edge { + Some((ResizeEdge::BottomRight, w)) + } else if left_edge { + Some((ResizeEdge::Left, w)) + } else if right_edge { + Some((ResizeEdge::Right, w)) + } else if top_edge { + Some((ResizeEdge::Top, w)) + } else if bottom_edge { + Some((ResizeEdge::Bottom, w)) + } else { + None + } + }) + }) + { + 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.active_icon != Some(icon) { + let _ = my_seat.set_cursor(conn, icon); + } + return; + } + PointerEventKind::Enter { .. } => {} + PointerEventKind::Leave { .. } => {} + _ => {} + } + if my_seat.active_icon != Some(icon) { + my_seat.set_cursor(conn, icon); + } + } else if my_seat.active_icon != my_seat.icon { + // Restore cursor that was set by appliction, or default + my_seat.set_cursor( + conn, + my_seat.icon.unwrap_or(CursorIcon::Default), + ); + } + + if is_active { + 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.active_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..ce90b7d86f --- /dev/null +++ b/sctk/src/handlers/seat/seat.rs @@ -0,0 +1,211 @@ +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, + last_touch_down: None, + icon: None, + active_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, + last_touch_down: None, + icon: None, + active_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 => { + if let Some(touch) = self.seat_state.get_touch(qh, &seat).ok() { + self.sctk_events.push(SctkEvent::SeatEvent { + variant: SeatEventVariant::NewCapability( + capability, + touch.id(), + ), + id: seat.clone(), + }); + my_seat.touch.replace(touch); + } + } + _ => unimplemented!(), + } + } + + 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 => { + if let Some(touch) = my_seat.touch.take() { + self.sctk_events.push(SctkEvent::SeatEvent { + variant: SeatEventVariant::RemoveCapability( + capability, + touch.id(), + ), + id: seat.clone(), + }); + } + } + _ => 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..3c6920898b --- /dev/null +++ b/sctk/src/handlers/seat/touch.rs @@ -0,0 +1,145 @@ +// TODO handle multiple seats? + +use crate::{event_loop::state::SctkState, sctk_event::SctkEvent}; +use iced_runtime::core::{touch, Point}; +use sctk::{ + delegate_touch, + reexports::client::{ + protocol::{wl_surface::WlSurface, wl_touch::WlTouch}, + Connection, QueueHandle, + }, + seat::touch::TouchHandler, +}; +use std::fmt::Debug; + +impl TouchHandler for SctkState { + fn down( + &mut self, + _: &Connection, + _: &QueueHandle, + touch: &WlTouch, + serial: u32, + time: u32, + surface: WlSurface, + id: i32, + position: (f64, f64), + ) { + let Some(my_seat) = self + .seats + .iter_mut() + .find(|s| s.touch.as_ref() == Some(touch)) + else { + return; + }; + + my_seat.last_touch_down.replace((time, id, serial)); + + let id = touch::Finger(id as u64); + let position = Point::new(position.0 as f32, position.1 as f32); + self.touch_points.insert(id, (surface.clone(), position)); + self.sctk_events.push(SctkEvent::TouchEvent { + variant: touch::Event::FingerPressed { id, position }, + touch_id: touch.clone(), + seat_id: my_seat.seat.clone(), + surface, + }); + } + + fn up( + &mut self, + _: &Connection, + _: &QueueHandle, + touch: &WlTouch, + _serial: u32, + _time: u32, + id: i32, + ) { + let Some(my_seat) = + self.seats.iter().find(|s| s.touch.as_ref() == Some(touch)) + else { + return; + }; + + let id = touch::Finger(id as u64); + if let Some((surface, position)) = self.touch_points.get(&id).cloned() { + self.sctk_events.push(SctkEvent::TouchEvent { + variant: touch::Event::FingerLifted { id, position }, + touch_id: touch.clone(), + seat_id: my_seat.seat.clone(), + surface, + }); + } + } + fn motion( + &mut self, + _: &Connection, + _: &QueueHandle, + touch: &WlTouch, + _time: u32, + id: i32, + position: (f64, f64), + ) { + let Some(my_seat) = + self.seats.iter().find(|s| s.touch.as_ref() == Some(touch)) + else { + return; + }; + + let id = touch::Finger(id as u64); + let position = Point::new(position.0 as f32, position.1 as f32); + if let Some((surface, position_ref)) = self.touch_points.get_mut(&id) { + *position_ref = position; + self.sctk_events.push(SctkEvent::TouchEvent { + variant: touch::Event::FingerMoved { id, position }, + touch_id: touch.clone(), + seat_id: my_seat.seat.clone(), + surface: surface.clone(), + }); + } + } + + fn shape( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlTouch, + _: i32, + _: f64, + _: f64, + ) { + } + + fn orientation( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlTouch, + _: i32, + _: f64, + ) { + } + + fn cancel( + &mut self, + _: &Connection, + _: &QueueHandle, + touch: &WlTouch, + ) { + let Some(my_seat) = + self.seats.iter().find(|s| s.touch.as_ref() == Some(touch)) + else { + return; + }; + + for (id, (surface, position)) in self.touch_points.drain() { + self.sctk_events.push(SctkEvent::TouchEvent { + variant: touch::Event::FingerLost { id, position }, + touch_id: touch.clone(), + seat_id: my_seat.seat.clone(), + surface, + }); + } + } +} + +delegate_touch!(@ SctkState); diff --git a/sctk/src/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..89c3ddfd32 --- /dev/null +++ b/sctk/src/handlers/shell/layer.rs @@ -0,0 +1,114 @@ +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(), 0)); + } +} + +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..79deb0a549 --- /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(), 0)); + } + + 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..6dd9da14f4 --- /dev/null +++ b/sctk/src/handlers/shell/xdg_window.rs @@ -0,0 +1,92 @@ +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, + 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(), + }); + } + + window.update_size(configure.new_size); + + 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( + window.current_size, + configure, + wl_surface.clone(), + first, + ), + id, + }); + self.frame_events.push((wl_surface.clone(), 0)); + } +} + +delegate_xdg_window!(@ SctkState); +delegate_xdg_shell!(@ SctkState); diff --git a/sctk/src/handlers/subcompositor.rs b/sctk/src/handlers/subcompositor.rs new file mode 100644 index 0000000000..a5c9fdab3a --- /dev/null +++ b/sctk/src/handlers/subcompositor.rs @@ -0,0 +1,5 @@ +use crate::handlers::SctkState; +use sctk::delegate_subcompositor; +use std::fmt::Debug; + +delegate_subcompositor!(@ SctkState); diff --git a/sctk/src/handlers/wp_fractional_scaling.rs b/sctk/src/handlers/wp_fractional_scaling.rs new file mode 100644 index 0000000000..aed95e2087 --- /dev/null +++ b/sctk/src/handlers/wp_fractional_scaling.rs @@ -0,0 +1,97 @@ +// From: https://github.com/rust-windowing/winit/blob/master/src/platform_impl/linux/wayland/types/wp_fractional_scaling.rs +//! Handling of the fractional scaling. + +use std::marker::PhantomData; + +use sctk::reexports::client::globals::{BindError, GlobalList}; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::Dispatch; +use sctk::reexports::client::{delegate_dispatch, Connection, Proxy, QueueHandle}; +use sctk::reexports::protocols::wp::fractional_scale::v1::client::wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1; +use sctk::reexports::protocols::wp::fractional_scale::v1::client::wp_fractional_scale_v1::Event as FractionalScalingEvent; +use sctk::reexports::protocols::wp::fractional_scale::v1::client::wp_fractional_scale_v1::WpFractionalScaleV1; + +use sctk::globals::GlobalData; + +use crate::event_loop::state::SctkState; + +/// The scaling factor denominator. +const SCALE_DENOMINATOR: f64 = 120.; + +/// Fractional scaling manager. +#[derive(Debug)] +pub struct FractionalScalingManager { + manager: WpFractionalScaleManagerV1, + + _phantom: PhantomData, +} + +pub struct FractionalScaling { + /// The surface used for scaling. + surface: WlSurface, +} + +impl FractionalScalingManager { + /// Create new viewporter. + pub fn new( + globals: &GlobalList, + queue_handle: &QueueHandle>, + ) -> Result { + let manager = globals.bind(queue_handle, 1..=1, GlobalData)?; + Ok(Self { + manager, + _phantom: PhantomData, + }) + } + + pub fn fractional_scaling( + &self, + surface: &WlSurface, + queue_handle: &QueueHandle>, + ) -> WpFractionalScaleV1 { + let data = FractionalScaling { + surface: surface.clone(), + }; + self.manager + .get_fractional_scale(surface, queue_handle, data) + } +} + +impl Dispatch> + for FractionalScalingManager +{ + fn event( + _: &mut SctkState, + _: &WpFractionalScaleManagerV1, + _: ::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle>, + ) { + // No events. + } +} + +impl Dispatch> + for FractionalScalingManager +{ + fn event( + state: &mut SctkState, + _: &WpFractionalScaleV1, + event: ::Event, + data: &FractionalScaling, + _: &Connection, + _: &QueueHandle>, + ) { + if let FractionalScalingEvent::PreferredScale { scale } = event { + state.scale_factor_changed( + &data.surface, + scale as f64 / SCALE_DENOMINATOR, + false, + ); + } + } +} + +delegate_dispatch!(@ SctkState: [WpFractionalScaleManagerV1: GlobalData] => FractionalScalingManager); +delegate_dispatch!(@ SctkState: [WpFractionalScaleV1: FractionalScaling] => FractionalScalingManager); diff --git a/sctk/src/handlers/wp_viewporter.rs b/sctk/src/handlers/wp_viewporter.rs new file mode 100644 index 0000000000..31ca68777b --- /dev/null +++ b/sctk/src/handlers/wp_viewporter.rs @@ -0,0 +1,80 @@ +//! Handling of the wp-viewporter. + +use std::marker::PhantomData; + +use sctk::reexports::client::globals::{BindError, GlobalList}; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::Dispatch; +use sctk::reexports::client::{ + delegate_dispatch, Connection, Proxy, QueueHandle, +}; +use sctk::reexports::protocols::wp::viewporter::client::wp_viewport::WpViewport; +use sctk::reexports::protocols::wp::viewporter::client::wp_viewporter::WpViewporter; + +use sctk::globals::GlobalData; + +use crate::event_loop::state::SctkState; + +/// Viewporter. +#[derive(Debug)] +pub struct ViewporterState { + viewporter: WpViewporter, + _phantom: PhantomData, +} + +impl ViewporterState { + /// Create new viewporter. + pub fn new( + globals: &GlobalList, + queue_handle: &QueueHandle>, + ) -> Result { + let viewporter = globals.bind(queue_handle, 1..=1, GlobalData)?; + Ok(Self { + viewporter, + _phantom: PhantomData, + }) + } + + /// Get the viewport for the given object. + pub fn get_viewport( + &self, + surface: &WlSurface, + queue_handle: &QueueHandle>, + ) -> WpViewport { + self.viewporter + .get_viewport(surface, queue_handle, GlobalData) + } +} + +impl Dispatch> + for ViewporterState +{ + fn event( + _: &mut SctkState, + _: &WpViewporter, + _: ::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle>, + ) { + // No events. + } +} + +impl Dispatch> + for ViewporterState +{ + fn event( + _: &mut SctkState, + _: &WpViewport, + _: ::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle>, + ) { + // No events. + } +} + +delegate_dispatch!(@ SctkState: [WpViewporter: GlobalData] => ViewporterState); +delegate_dispatch!(@ SctkState: [WpViewport: GlobalData] => ViewporterState); diff --git a/sctk/src/keymap.rs b/sctk/src/keymap.rs new file mode 100644 index 0000000000..fe02f37bb7 --- /dev/null +++ b/sctk/src/keymap.rs @@ -0,0 +1,475 @@ +// Borrowed from winit + +pub fn keysym_to_key(keysym: u32) -> Key { + use xkbcommon_dl::keysyms; + Key::Named(match keysym { + // TTY function keys + keysyms::BackSpace => Named::Backspace, + keysyms::Tab => Named::Tab, + // keysyms::Linefeed => Named::Linefeed, + keysyms::Clear => Named::Clear, + keysyms::Return => Named::Enter, + keysyms::Pause => Named::Pause, + keysyms::Scroll_Lock => Named::ScrollLock, + keysyms::Sys_Req => Named::PrintScreen, + keysyms::Escape => Named::Escape, + keysyms::Delete => Named::Delete, + + // IME keys + keysyms::Multi_key => Named::Compose, + keysyms::Codeinput => Named::CodeInput, + keysyms::SingleCandidate => Named::SingleCandidate, + keysyms::MultipleCandidate => Named::AllCandidates, + keysyms::PreviousCandidate => Named::PreviousCandidate, + + // Japanese key + keysyms::Kanji => Named::KanjiMode, + keysyms::Muhenkan => Named::NonConvert, + keysyms::Henkan_Mode => Named::Convert, + keysyms::Romaji => Named::Romaji, + keysyms::Hiragana => Named::Hiragana, + keysyms::Hiragana_Katakana => Named::HiraganaKatakana, + keysyms::Zenkaku => Named::Zenkaku, + keysyms::Hankaku => Named::Hankaku, + keysyms::Zenkaku_Hankaku => Named::ZenkakuHankaku, + // keysyms::Touroku => Named::Touroku, + // keysyms::Massyo => Named::Massyo, + keysyms::Kana_Lock => Named::KanaMode, + keysyms::Kana_Shift => Named::KanaMode, + keysyms::Eisu_Shift => Named::Alphanumeric, + keysyms::Eisu_toggle => Named::Alphanumeric, + // NOTE: The next three items are aliases for values we've already mapped. + // keysyms::Kanji_Bangou => Named::CodeInput, + // keysyms::Zen_Koho => Named::AllCandidates, + // keysyms::Mae_Koho => Named::PreviousCandidate, + + // Cursor control & motion + keysyms::Home => Named::Home, + keysyms::Left => Named::ArrowLeft, + keysyms::Up => Named::ArrowUp, + keysyms::Right => Named::ArrowRight, + keysyms::Down => Named::ArrowDown, + // keysyms::Prior => Named::PageUp, + keysyms::Page_Up => Named::PageUp, + // keysyms::Next => Named::PageDown, + keysyms::Page_Down => Named::PageDown, + keysyms::End => Named::End, + // keysyms::Begin => Named::Begin, + + // Misc. functions + keysyms::Select => Named::Select, + keysyms::Print => Named::PrintScreen, + keysyms::Execute => Named::Execute, + keysyms::Insert => Named::Insert, + keysyms::Undo => Named::Undo, + keysyms::Redo => Named::Redo, + keysyms::Menu => Named::ContextMenu, + keysyms::Find => Named::Find, + keysyms::Cancel => Named::Cancel, + keysyms::Help => Named::Help, + keysyms::Break => Named::Pause, + keysyms::Mode_switch => Named::ModeChange, + // keysyms::script_switch => Named::ModeChange, + keysyms::Num_Lock => Named::NumLock, + + // Keypad keys + // keysyms::KP_Space => return Key::Character(" "), + keysyms::KP_Tab => Named::Tab, + keysyms::KP_Enter => Named::Enter, + keysyms::KP_F1 => Named::F1, + keysyms::KP_F2 => Named::F2, + keysyms::KP_F3 => Named::F3, + keysyms::KP_F4 => Named::F4, + keysyms::KP_Home => Named::Home, + keysyms::KP_Left => Named::ArrowLeft, + keysyms::KP_Up => Named::ArrowUp, + keysyms::KP_Right => Named::ArrowRight, + keysyms::KP_Down => Named::ArrowDown, + // keysyms::KP_Prior => Named::PageUp, + keysyms::KP_Page_Up => Named::PageUp, + // keysyms::KP_Next => Named::PageDown, + keysyms::KP_Page_Down => Named::PageDown, + keysyms::KP_End => Named::End, + // This is the key labeled "5" on the numpad when NumLock is off. + // keysyms::KP_Begin => Named::Begin, + keysyms::KP_Insert => Named::Insert, + keysyms::KP_Delete => Named::Delete, + // keysyms::KP_Equal => Named::Equal, + // keysyms::KP_Multiply => Named::Multiply, + // keysyms::KP_Add => Named::Add, + // keysyms::KP_Separator => Named::Separator, + // keysyms::KP_Subtract => Named::Subtract, + // keysyms::KP_Decimal => Named::Decimal, + // keysyms::KP_Divide => Named::Divide, + + // keysyms::KP_0 => return Key::Character("0"), + // keysyms::KP_1 => return Key::Character("1"), + // keysyms::KP_2 => return Key::Character("2"), + // keysyms::KP_3 => return Key::Character("3"), + // keysyms::KP_4 => return Key::Character("4"), + // keysyms::KP_5 => return Key::Character("5"), + // keysyms::KP_6 => return Key::Character("6"), + // keysyms::KP_7 => return Key::Character("7"), + // keysyms::KP_8 => return Key::Character("8"), + // keysyms::KP_9 => return Key::Character("9"), + + // Function keys + keysyms::F1 => Named::F1, + keysyms::F2 => Named::F2, + keysyms::F3 => Named::F3, + keysyms::F4 => Named::F4, + keysyms::F5 => Named::F5, + keysyms::F6 => Named::F6, + keysyms::F7 => Named::F7, + keysyms::F8 => Named::F8, + keysyms::F9 => Named::F9, + keysyms::F10 => Named::F10, + keysyms::F11 => Named::F11, + keysyms::F12 => Named::F12, + keysyms::F13 => Named::F13, + keysyms::F14 => Named::F14, + keysyms::F15 => Named::F15, + keysyms::F16 => Named::F16, + keysyms::F17 => Named::F17, + keysyms::F18 => Named::F18, + keysyms::F19 => Named::F19, + keysyms::F20 => Named::F20, + keysyms::F21 => Named::F21, + keysyms::F22 => Named::F22, + keysyms::F23 => Named::F23, + keysyms::F24 => Named::F24, + keysyms::F25 => Named::F25, + keysyms::F26 => Named::F26, + keysyms::F27 => Named::F27, + keysyms::F28 => Named::F28, + keysyms::F29 => Named::F29, + keysyms::F30 => Named::F30, + keysyms::F31 => Named::F31, + keysyms::F32 => Named::F32, + keysyms::F33 => Named::F33, + keysyms::F34 => Named::F34, + keysyms::F35 => Named::F35, + + // Modifiers + keysyms::Shift_L => Named::Shift, + keysyms::Shift_R => Named::Shift, + keysyms::Control_L => Named::Control, + keysyms::Control_R => Named::Control, + keysyms::Caps_Lock => Named::CapsLock, + // keysyms::Shift_Lock => Named::ShiftLock, + + // keysyms::Meta_L => Named::Meta, + // keysyms::Meta_R => Named::Meta, + keysyms::Alt_L => Named::Alt, + keysyms::Alt_R => Named::Alt, + keysyms::Super_L => Named::Super, + keysyms::Super_R => Named::Super, + keysyms::Hyper_L => Named::Hyper, + keysyms::Hyper_R => Named::Hyper, + + // XKB function and modifier keys + // keysyms::ISO_Lock => Named::IsoLock, + // keysyms::ISO_Level2_Latch => Named::IsoLevel2Latch, + keysyms::ISO_Level3_Shift => Named::AltGraph, + keysyms::ISO_Level3_Latch => Named::AltGraph, + keysyms::ISO_Level3_Lock => Named::AltGraph, + // keysyms::ISO_Level5_Shift => Named::IsoLevel5Shift, + // keysyms::ISO_Level5_Latch => Named::IsoLevel5Latch, + // keysyms::ISO_Level5_Lock => Named::IsoLevel5Lock, + // keysyms::ISO_Group_Shift => Named::IsoGroupShift, + // keysyms::ISO_Group_Latch => Named::IsoGroupLatch, + // keysyms::ISO_Group_Lock => Named::IsoGroupLock, + keysyms::ISO_Next_Group => Named::GroupNext, + // keysyms::ISO_Next_Group_Lock => Named::GroupNextLock, + keysyms::ISO_Prev_Group => Named::GroupPrevious, + // keysyms::ISO_Prev_Group_Lock => Named::GroupPreviousLock, + keysyms::ISO_First_Group => Named::GroupFirst, + // keysyms::ISO_First_Group_Lock => Named::GroupFirstLock, + keysyms::ISO_Last_Group => Named::GroupLast, + // keysyms::ISO_Last_Group_Lock => Named::GroupLastLock, + // + keysyms::ISO_Left_Tab => Named::Tab, + // keysyms::ISO_Move_Line_Up => Named::IsoMoveLineUp, + // keysyms::ISO_Move_Line_Down => Named::IsoMoveLineDown, + // keysyms::ISO_Partial_Line_Up => Named::IsoPartialLineUp, + // keysyms::ISO_Partial_Line_Down => Named::IsoPartialLineDown, + // keysyms::ISO_Partial_Space_Left => Named::IsoPartialSpaceLeft, + // keysyms::ISO_Partial_Space_Right => Named::IsoPartialSpaceRight, + // keysyms::ISO_Set_Margin_Left => Named::IsoSetMarginLeft, + // keysyms::ISO_Set_Margin_Right => Named::IsoSetMarginRight, + // keysyms::ISO_Release_Margin_Left => Named::IsoReleaseMarginLeft, + // keysyms::ISO_Release_Margin_Right => Named::IsoReleaseMarginRight, + // keysyms::ISO_Release_Both_Margins => Named::IsoReleaseBothMargins, + // keysyms::ISO_Fast_Cursor_Left => Named::IsoFastCursorLeft, + // keysyms::ISO_Fast_Cursor_Right => Named::IsoFastCursorRight, + // keysyms::ISO_Fast_Cursor_Up => Named::IsoFastCursorUp, + // keysyms::ISO_Fast_Cursor_Down => Named::IsoFastCursorDown, + // keysyms::ISO_Continuous_Underline => Named::IsoContinuousUnderline, + // keysyms::ISO_Discontinuous_Underline => Named::IsoDiscontinuousUnderline, + // keysyms::ISO_Emphasize => Named::IsoEmphasize, + // keysyms::ISO_Center_Object => Named::IsoCenterObject, + keysyms::ISO_Enter => Named::Enter, + + // dead_grave..dead_currency + + // dead_lowline..dead_longsolidusoverlay + + // dead_a..dead_capital_schwa + + // dead_greek + + // First_Virtual_Screen..Terminate_Server + + // AccessX_Enable..AudibleBell_Enable + + // Pointer_Left..Pointer_Drag5 + + // Pointer_EnableKeys..Pointer_DfltBtnPrev + + // ch..C_H + + // 3270 terminal keys + // keysyms::3270_Duplicate => Named::Duplicate, + // keysyms::3270_FieldMark => Named::FieldMark, + // keysyms::3270_Right2 => Named::Right2, + // keysyms::3270_Left2 => Named::Left2, + // keysyms::3270_BackTab => Named::BackTab, + keysyms::_3270_EraseEOF => Named::EraseEof, + // keysyms::3270_EraseInput => Named::EraseInput, + // keysyms::3270_Reset => Named::Reset, + // keysyms::3270_Quit => Named::Quit, + // keysyms::3270_PA1 => Named::Pa1, + // keysyms::3270_PA2 => Named::Pa2, + // keysyms::3270_PA3 => Named::Pa3, + // keysyms::3270_Test => Named::Test, + keysyms::_3270_Attn => Named::Attn, + // keysyms::3270_CursorBlink => Named::CursorBlink, + // keysyms::3270_AltCursor => Named::AltCursor, + // keysyms::3270_KeyClick => Named::KeyClick, + // keysyms::3270_Jump => Named::Jump, + // keysyms::3270_Ident => Named::Ident, + // keysyms::3270_Rule => Named::Rule, + // keysyms::3270_Copy => Named::Copy, + keysyms::_3270_Play => Named::Play, + // keysyms::3270_Setup => Named::Setup, + // keysyms::3270_Record => Named::Record, + // keysyms::3270_ChangeScreen => Named::ChangeScreen, + // keysyms::3270_DeleteWord => Named::DeleteWord, + keysyms::_3270_ExSelect => Named::ExSel, + keysyms::_3270_CursorSelect => Named::CrSel, + keysyms::_3270_PrintScreen => Named::PrintScreen, + keysyms::_3270_Enter => Named::Enter, + + keysyms::space => Named::Space, + // exclam..Sinh_kunddaliya + + // XFree86 + // keysyms::XF86_ModeLock => Named::ModeLock, + + // XFree86 - Backlight controls + keysyms::XF86_MonBrightnessUp => Named::BrightnessUp, + keysyms::XF86_MonBrightnessDown => Named::BrightnessDown, + // keysyms::XF86_KbdLightOnOff => Named::LightOnOff, + // keysyms::XF86_KbdBrightnessUp => Named::KeyboardBrightnessUp, + // keysyms::XF86_KbdBrightnessDown => Named::KeyboardBrightnessDown, + + // XFree86 - "Internet" + keysyms::XF86_Standby => Named::Standby, + keysyms::XF86_AudioLowerVolume => Named::AudioVolumeDown, + keysyms::XF86_AudioRaiseVolume => Named::AudioVolumeUp, + keysyms::XF86_AudioPlay => Named::MediaPlay, + keysyms::XF86_AudioStop => Named::MediaStop, + keysyms::XF86_AudioPrev => Named::MediaTrackPrevious, + keysyms::XF86_AudioNext => Named::MediaTrackNext, + keysyms::XF86_HomePage => Named::BrowserHome, + keysyms::XF86_Mail => Named::LaunchMail, + // keysyms::XF86_Start => Named::Start, + keysyms::XF86_Search => Named::BrowserSearch, + keysyms::XF86_AudioRecord => Named::MediaRecord, + + // XFree86 - PDA + keysyms::XF86_Calculator => Named::LaunchApplication2, + // keysyms::XF86_Memo => Named::Memo, + // keysyms::XF86_ToDoList => Named::ToDoList, + keysyms::XF86_Calendar => Named::LaunchCalendar, + keysyms::XF86_PowerDown => Named::Power, + // keysyms::XF86_ContrastAdjust => Named::AdjustContrast, + // keysyms::XF86_RockerUp => Named::RockerUp, + // keysyms::XF86_RockerDown => Named::RockerDown, + // keysyms::XF86_RockerEnter => Named::RockerEnter, + + // XFree86 - More "Internet" + keysyms::XF86_Back => Named::BrowserBack, + keysyms::XF86_Forward => Named::BrowserForward, + // keysyms::XF86_Stop => Named::Stop, + keysyms::XF86_Refresh => Named::BrowserRefresh, + keysyms::XF86_PowerOff => Named::Power, + keysyms::XF86_WakeUp => Named::WakeUp, + keysyms::XF86_Eject => Named::Eject, + keysyms::XF86_ScreenSaver => Named::LaunchScreenSaver, + keysyms::XF86_WWW => Named::LaunchWebBrowser, + keysyms::XF86_Sleep => Named::Standby, + keysyms::XF86_Favorites => Named::BrowserFavorites, + keysyms::XF86_AudioPause => Named::MediaPause, + // keysyms::XF86_AudioMedia => Named::AudioMedia, + keysyms::XF86_MyComputer => Named::LaunchApplication1, + // keysyms::XF86_VendorHome => Named::VendorHome, + // keysyms::XF86_LightBulb => Named::LightBulb, + // keysyms::XF86_Shop => Named::BrowserShop, + // keysyms::XF86_History => Named::BrowserHistory, + // keysyms::XF86_OpenURL => Named::OpenUrl, + // keysyms::XF86_AddFavorite => Named::AddFavorite, + // keysyms::XF86_HotLinks => Named::HotLinks, + // keysyms::XF86_BrightnessAdjust => Named::BrightnessAdjust, + // keysyms::XF86_Finance => Named::BrowserFinance, + // keysyms::XF86_Community => Named::BrowserCommunity, + keysyms::XF86_AudioRewind => Named::MediaRewind, + // keysyms::XF86_BackForward => Key::???, + // XF86_Launch0..XF86_LaunchF + + // XF86_ApplicationLeft..XF86_CD + keysyms::XF86_Calculater => Named::LaunchApplication2, // Nice typo, libxkbcommon :) + // XF86_Clear + keysyms::XF86_Close => Named::Close, + keysyms::XF86_Copy => Named::Copy, + keysyms::XF86_Cut => Named::Cut, + // XF86_Display..XF86_Documents + keysyms::XF86_Excel => Named::LaunchSpreadsheet, + // XF86_Explorer..XF86iTouch + keysyms::XF86_LogOff => Named::LogOff, + // XF86_Market..XF86_MenuPB + keysyms::XF86_MySites => Named::BrowserFavorites, + keysyms::XF86_New => Named::New, + // XF86_News..XF86_OfficeHome + keysyms::XF86_Open => Named::Open, + // XF86_Option + keysyms::XF86_Paste => Named::Paste, + keysyms::XF86_Phone => Named::LaunchPhone, + // XF86_Q + keysyms::XF86_Reply => Named::MailReply, + keysyms::XF86_Reload => Named::BrowserRefresh, + // XF86_RotateWindows..XF86_RotationKB + keysyms::XF86_Save => Named::Save, + // XF86_ScrollUp..XF86_ScrollClick + keysyms::XF86_Send => Named::MailSend, + keysyms::XF86_Spell => Named::SpellCheck, + keysyms::XF86_SplitScreen => Named::SplitScreenToggle, + // XF86_Support..XF86_User2KB + keysyms::XF86_Video => Named::LaunchMediaPlayer, + // XF86_WheelButton + keysyms::XF86_Word => Named::LaunchWordProcessor, + // XF86_Xfer + keysyms::XF86_ZoomIn => Named::ZoomIn, + keysyms::XF86_ZoomOut => Named::ZoomOut, + + // XF86_Away..XF86_Messenger + keysyms::XF86_WebCam => Named::LaunchWebCam, + keysyms::XF86_MailForward => Named::MailForward, + // XF86_Pictures + keysyms::XF86_Music => Named::LaunchMusicPlayer, + + // XF86_Battery..XF86_UWB + // + keysyms::XF86_AudioForward => Named::MediaFastForward, + // XF86_AudioRepeat + keysyms::XF86_AudioRandomPlay => Named::RandomToggle, + keysyms::XF86_Subtitle => Named::Subtitle, + keysyms::XF86_AudioCycleTrack => Named::MediaAudioTrack, + // XF86_CycleAngle..XF86_Blue + // + keysyms::XF86_Suspend => Named::Standby, + keysyms::XF86_Hibernate => Named::Hibernate, + // XF86_TouchpadToggle..XF86_TouchpadOff + // + keysyms::XF86_AudioMute => Named::AudioVolumeMute, + + // XF86_Switch_VT_1..XF86_Switch_VT_12 + + // XF86_Ungrab..XF86_ClearGrab + keysyms::XF86_Next_VMode => Named::VideoModeNext, + // keysyms::XF86_Prev_VMode => Named::VideoModePrevious, + // XF86_LogWindowTree..XF86_LogGrabInfo + + // SunFA_Grave..SunFA_Cedilla + + // keysyms::SunF36 => Named::F36 | Named::F11, + // keysyms::SunF37 => Named::F37 | Named::F12, + + // keysyms::SunSys_Req => Named::PrintScreen, + // The next couple of xkb (until SunStop) are already handled. + // SunPrint_Screen..SunPageDown + + // SunUndo..SunFront + keysyms::SUN_Copy => Named::Copy, + keysyms::SUN_Open => Named::Open, + keysyms::SUN_Paste => Named::Paste, + keysyms::SUN_Cut => Named::Cut, + + // SunPowerSwitch + keysyms::SUN_AudioLowerVolume => Named::AudioVolumeDown, + keysyms::SUN_AudioMute => Named::AudioVolumeMute, + keysyms::SUN_AudioRaiseVolume => Named::AudioVolumeUp, + // SUN_VideoDegauss + keysyms::SUN_VideoLowerBrightness => Named::BrightnessDown, + keysyms::SUN_VideoRaiseBrightness => Named::BrightnessUp, + // SunPowerSwitchShift + // + _ => return Key::Unidentified, + }) +} + +use iced_runtime::keyboard::{key::Named, Key, Location}; + +pub fn keysym_location(keysym: u32) -> Location { + use xkbcommon_dl::keysyms; + match keysym { + xkeysym::key::Shift_L + | keysyms::Control_L + | keysyms::Meta_L + | keysyms::Alt_L + | keysyms::Super_L + | keysyms::Hyper_L => Location::Left, + keysyms::Shift_R + | keysyms::Control_R + | keysyms::Meta_R + | keysyms::Alt_R + | keysyms::Super_R + | keysyms::Hyper_R => Location::Right, + keysyms::KP_0 + | keysyms::KP_1 + | keysyms::KP_2 + | keysyms::KP_3 + | keysyms::KP_4 + | keysyms::KP_5 + | keysyms::KP_6 + | keysyms::KP_7 + | keysyms::KP_8 + | keysyms::KP_9 + | keysyms::KP_Space + | keysyms::KP_Tab + | keysyms::KP_Enter + | keysyms::KP_F1 + | keysyms::KP_F2 + | keysyms::KP_F3 + | keysyms::KP_F4 + | keysyms::KP_Home + | keysyms::KP_Left + | keysyms::KP_Up + | keysyms::KP_Right + | keysyms::KP_Down + | keysyms::KP_Page_Up + | keysyms::KP_Page_Down + | keysyms::KP_End + | keysyms::KP_Begin + | keysyms::KP_Insert + | keysyms::KP_Delete + | keysyms::KP_Equal + | keysyms::KP_Multiply + | keysyms::KP_Add + | keysyms::KP_Separator + | keysyms::KP_Subtract + | keysyms::KP_Decimal + | keysyms::KP_Divide => Location::Numpad, + _ => Location::Standard, + } +} diff --git a/sctk/src/lib.rs b/sctk/src/lib.rs new file mode 100644 index 0000000000..beed0700d6 --- /dev/null +++ b/sctk/src/lib.rs @@ -0,0 +1,26 @@ +pub mod application; +pub mod clipboard; +pub mod commands; +pub mod conversion; +pub mod dpi; +pub mod error; +pub mod event_loop; +mod handlers; +pub mod keymap; +pub mod result; +pub mod sctk_event; +pub mod settings; +pub mod subsurface_widget; +#[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..0d89347596 --- /dev/null +++ b/sctk/src/sctk_event.rs @@ -0,0 +1,1018 @@ +use crate::{ + application::SurfaceIdWrapper, + conversion::{ + modifiers_to_native, pointer_axis_to_native, pointer_button_to_native, + }, + dpi::PhysicalSize, + keymap::{self, keysym_to_key}, + subsurface_widget::SubsurfaceState, +}; + +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, touch, window, Point}, + keyboard::{key, Key, Location}, + window::Id as SurfaceId, +}; +use sctk::{ + output::OutputInfo, + reexports::client::{ + backend::ObjectId, + protocol::{ + wl_data_device_manager::DndAction, wl_keyboard::WlKeyboard, + wl_output::WlOutput, wl_pointer::WlPointer, wl_seat::WlSeat, + wl_surface::WlSurface, wl_touch::WlTouch, + }, + 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, num::NonZeroU32, time::Instant}; +use wayland_protocols::wp::viewporter::client::wp_viewport::WpViewport; +use xkeysym::Keysym; + +pub enum IcedSctkEvent { + /// Emitted when new events arrive from the OS to be processed. + /// + /// 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, u32), + + Subcompositor(SubsurfaceState), +} + +#[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, + }, + TouchEvent { + variant: touch::Event, + touch_id: WlTouch, + seat_id: WlSeat, + surface: WlSurface, + }, + // 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, + }, + SessionLockSurfaceDone { + surface: WlSurface, + }, + SessionLockSurfaceScaleFactorChanged { + surface: WlSurface, + scale_factor: f64, + viewport: Option, + }, + 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((NonZeroU32, NonZeroU32), WindowConfigure, WlSurface, bool), + Size((NonZeroU32, NonZeroU32), 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, + subsurface_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 { .. } => { + let offset = if let Some((x_offset, y_offset, _)) = + subsurface_ids.get(&variant.surface.id()) + { + (*x_offset, *y_offset) + } else { + (0, 0) + }; + vec![iced_runtime::core::Event::Mouse( + mouse::Event::CursorMoved { + position: Point::new( + variant.position.0 as f32 + offset.0 as f32, + variant.position.1 as f32 + offset.1 as f32, + ), + }, + )] + } + PointerEventKind::Press { + time: _, + button, + serial: _, + } => pointer_button_to_native(button) + .map(|b| { + iced_runtime::core::Event::Mouse( + mouse::Event::ButtonPressed(b), + ) + }) + .into_iter() + .collect(), // TODO Ashley: conversion + PointerEventKind::Release { + time: _, + button, + serial: _, + } => pointer_button_to_native(button) + .map(|b| { + iced_runtime::core::Event::Mouse( + mouse::Event::ButtonReleased(b), + ) + }) + .into_iter() + .collect(), // TODO Ashley: conversion + PointerEventKind::Axis { + time: _, + horizontal, + vertical, + source, + } => pointer_axis_to_native(source, horizontal, vertical) + .map(|a| { + iced_runtime::core::Event::Mouse( + mouse::Event::WheelScrolled { delta: a }, + ) + }) + .into_iter() + .collect(), // TODO Ashley: conversion + }, + SctkEvent::KeyboardEvent { + variant, + kbd_id: _, + seat_id, + } => match variant { + KeyboardEventVariant::Leave(surface) => surface_ids + .get(&surface.id()) + .and_then(|id| match id { + SurfaceIdWrapper::LayerSurface(_id) => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::Layer( + LayerEvent::Unfocused, + surface, + id.inner(), + ), + ), + )) + } + SurfaceIdWrapper::Window(id) => { + Some(iced_runtime::core::Event::Window( + *id, + window::Event::Unfocused, + )) + } + SurfaceIdWrapper::Popup(_id) => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::Popup( + PopupEvent::Unfocused, + surface, + id.inner(), + ), + ), + )) + } + SurfaceIdWrapper::Dnd(_) => None, + SurfaceIdWrapper::SessionLock(_) => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::SessionLock( + SessionLockEvent::Unfocused( + surface, + id.inner(), + ), + ), + ), + )) + } + }) + .into_iter() + .chain([iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::Seat( + wayland::SeatEvent::Leave, + seat_id, + )), + )]) + .collect(), + KeyboardEventVariant::Enter(surface) => surface_ids + .get(&surface.id()) + .and_then(|id| match id { + SurfaceIdWrapper::LayerSurface(_id) => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::Layer( + LayerEvent::Focused, + surface, + id.inner(), + ), + ), + )) + } + SurfaceIdWrapper::Window(id) => { + Some(iced_runtime::core::Event::Window( + *id, + window::Event::Focused, + )) + } + SurfaceIdWrapper::Popup(_id) => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::Popup( + PopupEvent::Focused, + surface, + id.inner(), + ), + ), + )) + } + SurfaceIdWrapper::Dnd(_) => None, + SurfaceIdWrapper::SessionLock(_) => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::SessionLock( + SessionLockEvent::Focused( + surface, + id.inner(), + ), + ), + ), + )) + } + }) + .into_iter() + .chain([iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::Seat( + wayland::SeatEvent::Enter, + seat_id, + )), + )]) + .collect(), + KeyboardEventVariant::Press(ke) => { + let (key, location) = keysym_to_vkey_location(ke.keysym); + Some(iced_runtime::core::Event::Keyboard( + keyboard::Event::KeyPressed { + key: key, + location: location, + text: ke.utf8.map(|s| s.into()), + modifiers: modifiers_to_native(*modifiers), + }, + )) + .into_iter() + .collect() + } + KeyboardEventVariant::Repeat(KeyEvent { + keysym, utf8, .. + }) => { + let (key, location) = keysym_to_vkey_location(keysym); + Some(iced_runtime::core::Event::Keyboard( + keyboard::Event::KeyPressed { + key: key, + location: location, + text: utf8.map(|s| s.into()), + modifiers: modifiers_to_native(*modifiers), + }, + )) + .into_iter() + .collect() + } + KeyboardEventVariant::Release(ke) => { + let (k, location) = keysym_to_vkey_location(ke.keysym); + Some(iced_runtime::core::Event::Keyboard( + keyboard::Event::KeyReleased { + key: k, + location: location, + modifiers: modifiers_to_native(*modifiers), + }, + )) + .into_iter() + .collect() + } + KeyboardEventVariant::Modifiers(new_mods) => { + *modifiers = new_mods; + vec![iced_runtime::core::Event::Keyboard( + keyboard::Event::ModifiersChanged(modifiers_to_native( + new_mods, + )), + )] + } + }, + SctkEvent::TouchEvent { + variant, + touch_id: _, + seat_id: _, + surface: _, + } => { + vec![iced_runtime::core::Event::Touch(variant)] + } + 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::Closed, + ) + }) + .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( + (new_width, new_height), + configure, + surface, + _, + ) => surface_ids + .get(&surface.id()) + .map(|id| { + if configure.is_resizing() { + vec![iced_runtime::core::Event::Window( + id.inner(), + window::Event::Resized { + width: new_width.get(), + height: new_height.get(), + }, + )] + } else { + vec![ + iced_runtime::core::Event::Window( + id.inner(), + window::Event::Resized { + width: new_width.get(), + height: new_height.get(), + }, + ), + iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::Window( + wayland::WindowEvent::Configure( + configure, + ), + surface, + id.inner(), + ), + ), + ), + ] + } + }) + .unwrap_or_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(), + WindowEventVariant::Size(_, _, _) => vec![], + }, + 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, surface } => 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 } => { + let offset = if let Some((x_offset, y_offset, _)) = + subsurface_ids.get(&surface.id()) + { + (*x_offset, *y_offset) + } else { + (0, 0) + }; + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::DndOffer( + wayland::DndOfferEvent::Motion { + x: x + offset.0 as f64, + y: y + offset.1 as f64, + }, + )), + )) + .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::SessionLockSurfaceDone { .. } => vec![], + SctkEvent::SessionUnlocked => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::SessionLock( + wayland::SessionLockEvent::Unlocked, + )), + )) + .into_iter() + .collect() + } + SctkEvent::SessionLockSurfaceScaleFactorChanged { .. } => vec![], + } + } +} + +fn keysym_to_vkey_location(keysym: Keysym) -> (Key, Location) { + let raw = keysym.raw(); + let mut key = keysym_to_key(raw); + if matches!(key, key::Key::Unidentified) { + // XXX is there a better way to do this? + // we need to be able to determine the actual character for the key + // not the combination, so this seems to be correct + let mut utf8 = xkbcommon::xkb::keysym_to_utf8(keysym); + // remove null terminator + utf8.pop(); + if utf8.len() > 0 { + key = Key::Character(utf8.into()); + } + } + + let location = keymap::keysym_location(raw); + (key, location) +} diff --git a/sctk/src/settings.rs b/sctk/src/settings.rs new file mode 100644 index 0000000000..8fd5456a59 --- /dev/null +++ b/sctk/src/settings.rs @@ -0,0 +1,36 @@ +use std::time::Duration; + +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, + /// event loop dispatch timeout + pub control_flow_timeout: Option, +} + +#[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/subsurface_widget.rs b/sctk/src/subsurface_widget.rs new file mode 100644 index 0000000000..77d63d34ab --- /dev/null +++ b/sctk/src/subsurface_widget.rs @@ -0,0 +1,698 @@ +// TODO z-order option? + +use crate::application::SurfaceIdWrapper; +use crate::core::{ + layout::{self, Layout}, + mouse, renderer, + widget::{self, Widget}, + ContentFit, Element, Length, Rectangle, Size, +}; +use std::{ + cell::RefCell, + collections::HashMap, + fmt::Debug, + future::Future, + hash::{Hash, Hasher}, + mem, + os::unix::io::{AsFd, OwnedFd}, + pin::Pin, + ptr, + sync::{Arc, Mutex, Weak}, + task, +}; + +use futures::channel::oneshot; +use sctk::{ + compositor::SurfaceData, + globals::GlobalData, + reexports::client::{ + delegate_noop, + protocol::{ + wl_buffer::{self, WlBuffer}, + wl_compositor::WlCompositor, + wl_shm::{self, WlShm}, + wl_shm_pool::{self, WlShmPool}, + wl_subcompositor::WlSubcompositor, + wl_subsurface::WlSubsurface, + wl_surface::WlSurface, + }, + Connection, Dispatch, Proxy, QueueHandle, + }, +}; +use wayland_backend::client::ObjectId; +use wayland_protocols::wp::{ + alpha_modifier::v1::client::{ + wp_alpha_modifier_surface_v1::WpAlphaModifierSurfaceV1, + wp_alpha_modifier_v1::WpAlphaModifierV1, + }, + linux_dmabuf::zv1::client::{ + zwp_linux_buffer_params_v1::{self, ZwpLinuxBufferParamsV1}, + zwp_linux_dmabuf_v1::{self, ZwpLinuxDmabufV1}, + }, + viewporter::client::{ + wp_viewport::WpViewport, wp_viewporter::WpViewporter, + }, +}; + +use crate::event_loop::state::SctkState; + +#[derive(Debug)] +pub struct Plane { + pub fd: OwnedFd, + pub plane_idx: u32, + pub offset: u32, + pub stride: u32, +} + +#[derive(Debug)] +pub struct Dmabuf { + pub width: i32, + pub height: i32, + pub planes: Vec, + pub format: u32, + pub modifier: u64, +} + +#[derive(Debug)] +pub struct Shmbuf { + pub fd: OwnedFd, + pub offset: i32, + pub width: i32, + pub height: i32, + pub stride: i32, + pub format: wl_shm::Format, +} + +#[derive(Debug)] +pub enum BufferSource { + Shm(Shmbuf), + Dma(Dmabuf), +} + +impl From for BufferSource { + fn from(buf: Shmbuf) -> Self { + Self::Shm(buf) + } +} + +impl From for BufferSource { + fn from(buf: Dmabuf) -> Self { + Self::Dma(buf) + } +} + +#[derive(Debug)] +struct SubsurfaceBufferInner { + source: Arc, + _sender: oneshot::Sender<()>, +} + +/// Refcounted type containing a `BufferSource` with a sender that is signaled +/// all references are dropped and `wl_buffer`s created from the source are +/// released. +#[derive(Clone, Debug)] +pub struct SubsurfaceBuffer(Arc); + +pub struct BufferData { + source: WeakBufferSource, + // This reference is held until the surface `release`s the buffer + subsurface_buffer: Mutex>, +} + +impl BufferData { + fn for_buffer(buffer: &WlBuffer) -> Option<&Self> { + buffer.data::() + } +} + +/// Future signalled when subsurface buffer is released +pub struct SubsurfaceBufferRelease(oneshot::Receiver<()>); + +impl SubsurfaceBufferRelease { + /// Non-blocking check if buffer is released yet, without awaiting + pub fn released(&mut self) -> bool { + self.0.try_recv() == Ok(None) + } +} + +impl Future for SubsurfaceBufferRelease { + type Output = (); + + fn poll( + mut self: Pin<&mut Self>, + cx: &mut task::Context, + ) -> task::Poll<()> { + Pin::new(&mut self.0).poll(cx).map(|_| ()) + } +} + +impl SubsurfaceBuffer { + pub fn new(source: Arc) -> (Self, SubsurfaceBufferRelease) { + let (_sender, receiver) = oneshot::channel(); + let subsurface_buffer = + SubsurfaceBuffer(Arc::new(SubsurfaceBufferInner { + source, + _sender, + })); + (subsurface_buffer, SubsurfaceBufferRelease(receiver)) + } + + // Behavior of `wl_buffer::released` is undefined if attached to multiple surfaces. To allow + // things like that, create a new `wl_buffer` each time. + fn create_buffer( + &self, + shm: &WlShm, + dmabuf: Option<&ZwpLinuxDmabufV1>, + qh: &QueueHandle>, + ) -> Option { + // create reference to source, that is dropped on release + match self.0.source.as_ref() { + BufferSource::Shm(buf) => { + let pool = shm.create_pool( + buf.fd.as_fd(), + buf.offset + buf.height * buf.stride, + qh, + GlobalData, + ); + let buffer = pool.create_buffer( + buf.offset, + buf.width, + buf.height, + buf.stride, + buf.format, + qh, + BufferData { + source: WeakBufferSource(Arc::downgrade( + &self.0.source, + )), + subsurface_buffer: Mutex::new(Some(self.clone())), + }, + ); + pool.destroy(); + Some(buffer) + } + BufferSource::Dma(buf) => { + if let Some(dmabuf) = dmabuf { + let params = dmabuf.create_params(qh, GlobalData); + for plane in &buf.planes { + let modifier_hi = (buf.modifier >> 32) as u32; + let modifier_lo = (buf.modifier & 0xffffffff) as u32; + params.add( + plane.fd.as_fd(), + plane.plane_idx, + plane.offset, + plane.stride, + modifier_hi, + modifier_lo, + ); + } + // Will cause protocol error if format is not supported + Some(params.create_immed( + buf.width, + buf.height, + buf.format, + zwp_linux_buffer_params_v1::Flags::empty(), + qh, + BufferData { + source: WeakBufferSource(Arc::downgrade( + &self.0.source, + )), + subsurface_buffer: Mutex::new(Some(self.clone())), + }, + )) + } else { + None + } + } + } + } +} + +impl PartialEq for SubsurfaceBuffer { + fn eq(&self, rhs: &Self) -> bool { + Arc::ptr_eq(&self.0, &rhs.0) + } +} + +impl Dispatch for SctkState { + fn event( + _: &mut SctkState, + _: &WlShmPool, + _: wl_shm_pool::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle>, + ) { + unreachable!() + } +} + +impl Dispatch for SctkState { + fn event( + _: &mut SctkState, + _: &ZwpLinuxDmabufV1, + _: zwp_linux_dmabuf_v1::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle>, + ) { + } +} + +impl Dispatch for SctkState { + fn event( + _: &mut SctkState, + _: &ZwpLinuxBufferParamsV1, + _: zwp_linux_buffer_params_v1::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle>, + ) { + } +} + +impl Dispatch for SctkState { + fn event( + _: &mut SctkState, + _: &WlBuffer, + event: wl_buffer::Event, + data: &BufferData, + _: &Connection, + _: &QueueHandle>, + ) { + match event { + wl_buffer::Event::Release => { + // Release reference to `SubsurfaceBuffer` + data.subsurface_buffer.lock().unwrap().take(); + } + _ => unreachable!(), + } + } +} + +#[doc(hidden)] +#[derive(Clone, Debug)] +pub(crate) struct WeakBufferSource(Weak); + +impl PartialEq for WeakBufferSource { + fn eq(&self, rhs: &Self) -> bool { + Weak::ptr_eq(&self.0, &rhs.0) + } +} + +impl Eq for WeakBufferSource {} + +impl Hash for WeakBufferSource { + fn hash(&self, state: &mut H) { + ptr::hash::(self.0.as_ptr(), state) + } +} + +// create wl_buffer from BufferSource (avoid create_immed?) +// release +#[doc(hidden)] +pub struct SubsurfaceState { + pub wl_compositor: WlCompositor, + pub wl_subcompositor: WlSubcompositor, + pub wp_viewporter: WpViewporter, + pub wl_shm: WlShm, + pub wp_dmabuf: Option, + pub wp_alpha_modifier: Option, + pub qh: QueueHandle>, + pub(crate) buffers: HashMap>, + pub unmapped_subsurfaces: Vec, +} + +impl SubsurfaceState { + fn create_subsurface(&self, parent: &WlSurface) -> SubsurfaceInstance { + let wl_surface = self + .wl_compositor + .create_surface(&self.qh, SurfaceData::new(None, 1)); + + // Use empty input region so parent surface gets pointer events + let region = self.wl_compositor.create_region(&self.qh, ()); + wl_surface.set_input_region(Some(®ion)); + region.destroy(); + + let wl_subsurface = self.wl_subcompositor.get_subsurface( + &wl_surface, + parent, + &self.qh, + (), + ); + + let wp_viewport = self.wp_viewporter.get_viewport( + &wl_surface, + &self.qh, + sctk::globals::GlobalData, + ); + + let wp_alpha_modifier_surface = + self.wp_alpha_modifier.as_ref().map(|wp_alpha_modifier| { + wp_alpha_modifier.get_surface(&wl_surface, &self.qh, ()) + }); + + SubsurfaceInstance { + wl_surface, + wl_subsurface, + wp_viewport, + wp_alpha_modifier_surface, + wl_buffer: None, + bounds: None, + } + } + + // Update `subsurfaces` from `view_subsurfaces` + pub(crate) fn update_subsurfaces( + &mut self, + subsurface_ids: &mut HashMap, + parent: &WlSurface, + parent_id: SurfaceIdWrapper, + subsurfaces: &mut Vec, + view_subsurfaces: &[SubsurfaceInfo], + ) { + // Subsurfaces aren't destroyed immediately to sync removal with parent + // surface commit. Since `destroy` is immediate. + // + // They should be safe to destroy by the next time `update_subsurfaces` + // is run. + self.unmapped_subsurfaces.clear(); + + // Remove cached `wl_buffers` for any `BufferSource`s that no longer exist. + self.buffers.retain(|k, v| { + let retain = k.0.strong_count() > 0; + if !retain { + v.iter().for_each(|b| b.destroy()); + } + retain + }); + + // If view requested fewer subsurfaces than there currently are, + // unmap excess. + while view_subsurfaces.len() < subsurfaces.len() { + let subsurface = subsurfaces.pop().unwrap(); + subsurface.unmap(); + self.unmapped_subsurfaces.push(subsurface); + } + // Create new subsurfaces if there aren't enough. + while subsurfaces.len() < view_subsurfaces.len() { + subsurfaces.push(self.create_subsurface(parent)); + } + // Attach buffers to subsurfaces, set viewports, and commit. + for (subsurface_data, subsurface) in + view_subsurfaces.iter().zip(subsurfaces.iter_mut()) + { + subsurface.attach_and_commit( + parent_id, + subsurface_ids, + subsurface_data, + self, + ); + } + + if let Some(backend) = parent.backend().upgrade() { + subsurface_ids.retain(|k, _| backend.info(k.clone()).is_ok()); + } + } + + // Cache `wl_buffer` for use when `BufferSource` is used in future + // (Avoid creating wl_buffer each buffer swap) + fn insert_cached_wl_buffer(&mut self, buffer: WlBuffer) { + let source = BufferData::for_buffer(&buffer).unwrap().source.clone(); + self.buffers.entry(source).or_default().push(buffer); + } + + // Gets a cached `wl_buffer` for the `SubsurfaceBuffer`, if any. And stores `SubsurfaceBuffer` + // reference to be releated on `wl_buffer` release. + // + // If `wl_buffer` isn't released, it is destroyed instead. + fn get_cached_wl_buffer( + &mut self, + subsurface_buffer: &SubsurfaceBuffer, + ) -> Option { + let buffers = self.buffers.get_mut(&WeakBufferSource( + Arc::downgrade(&subsurface_buffer.0.source), + ))?; + while let Some(buffer) = buffers.pop() { + let mut subsurface_buffer_ref = buffer + .data::() + .unwrap() + .subsurface_buffer + .lock() + .unwrap(); + if subsurface_buffer_ref.is_none() { + *subsurface_buffer_ref = Some(subsurface_buffer.clone()); + drop(subsurface_buffer_ref); + return Some(buffer); + } else { + buffer.destroy(); + } + } + None + } +} + +impl Drop for SubsurfaceState { + fn drop(&mut self) { + self.buffers + .values() + .flatten() + .for_each(|buffer| buffer.destroy()); + } +} + +pub(crate) struct SubsurfaceInstance { + pub(crate) wl_surface: WlSurface, + wl_subsurface: WlSubsurface, + wp_viewport: WpViewport, + wp_alpha_modifier_surface: Option, + wl_buffer: Option, + bounds: Option>, +} + +impl SubsurfaceInstance { + // TODO correct damage? no damage/commit if unchanged? + fn attach_and_commit( + &mut self, + parent_id: SurfaceIdWrapper, + subsurface_ids: &mut HashMap, + info: &SubsurfaceInfo, + state: &mut SubsurfaceState, + ) { + let buffer_changed; + + let old_buffer = self.wl_buffer.take(); + let old_buffer_data = + old_buffer.as_ref().and_then(|b| BufferData::for_buffer(&b)); + let buffer = if old_buffer_data.is_some_and(|b| { + b.subsurface_buffer.lock().unwrap().as_ref() == Some(&info.buffer) + }) { + // Same "BufferSource" is already attached to this subsurface. Don't create new `wl_buffer`. + buffer_changed = false; + old_buffer.unwrap() + } else { + if let Some(old_buffer) = old_buffer { + state.insert_cached_wl_buffer(old_buffer); + } + + buffer_changed = true; + + if let Some(buffer) = state.get_cached_wl_buffer(&info.buffer) { + buffer + } else if let Some(buffer) = info.buffer.create_buffer( + &state.wl_shm, + state.wp_dmabuf.as_ref(), + &state.qh, + ) { + buffer + } else { + // TODO log error + self.wl_surface.attach(None, 0, 0); + return; + } + }; + + // XXX scale factor? + let bounds_changed = self.bounds != Some(info.bounds); + // wlroots seems to have issues changing buffer without running this + if bounds_changed || buffer_changed { + self.wl_subsurface + .set_position(info.bounds.x as i32, info.bounds.y as i32); + self.wp_viewport.set_destination( + info.bounds.width as i32, + info.bounds.height as i32, + ); + } + if buffer_changed { + self.wl_surface.attach(Some(&buffer), 0, 0); + self.wl_surface.damage(0, 0, i32::MAX, i32::MAX); + } + if buffer_changed || bounds_changed { + self.wl_surface.frame(&state.qh, self.wl_surface.clone()); + self.wl_surface.commit(); + } + + if let Some(wp_alpha_modifier_surface) = &self.wp_alpha_modifier_surface + { + let alpha = (info.alpha.clamp(0.0, 1.0) * u32::MAX as f32) as u32; + wp_alpha_modifier_surface.set_multiplier(alpha); + } + + subsurface_ids.insert( + self.wl_surface.id(), + (info.bounds.x as i32, info.bounds.y as i32, parent_id), + ); + + self.wl_buffer = Some(buffer); + self.bounds = Some(info.bounds); + } + + pub fn unmap(&self) { + self.wl_surface.attach(None, 0, 0); + self.wl_surface.commit(); + } +} + +impl Drop for SubsurfaceInstance { + fn drop(&mut self) { + self.wp_viewport.destroy(); + self.wl_subsurface.destroy(); + self.wl_surface.destroy(); + if let Some(wl_buffer) = self.wl_buffer.as_ref() { + wl_buffer.destroy(); + } + } +} + +pub(crate) struct SubsurfaceInfo { + pub buffer: SubsurfaceBuffer, + pub bounds: Rectangle, + pub alpha: f32, +} + +thread_local! { + static SUBSURFACES: RefCell> = RefCell::new(Vec::new()); +} + +pub(crate) fn take_subsurfaces() -> Vec { + SUBSURFACES.with(|subsurfaces| mem::take(&mut *subsurfaces.borrow_mut())) +} + +#[must_use] +pub struct Subsurface<'a> { + buffer_size: Size, + buffer: &'a SubsurfaceBuffer, + width: Length, + height: Length, + content_fit: ContentFit, + alpha: f32, +} + +impl<'a, Message, Theme, Renderer> Widget + for Subsurface<'a> +where + Renderer: renderer::Renderer, +{ + fn size(&self) -> Size { + Size::new(self.width, self.height) + } + + // Based on image widget + fn layout( + &self, + _tree: &mut widget::Tree, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let raw_size = + limits.resolve(self.width, self.height, self.buffer_size); + + let full_size = self.content_fit.fit(self.buffer_size, raw_size); + + let final_size = Size { + width: match self.width { + Length::Shrink => f32::min(raw_size.width, full_size.width), + _ => raw_size.width, + }, + height: match self.height { + Length::Shrink => f32::min(raw_size.height, full_size.height), + _ => raw_size.height, + }, + }; + + layout::Node::new(final_size) + } + + fn draw( + &self, + _state: &widget::Tree, + _renderer: &mut Renderer, + _theme: &Theme, + _style: &renderer::Style, + layout: Layout<'_>, + _cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + // Instead of using renderer, we need to add surface to a list that is + // read by the iced-sctk shell. + SUBSURFACES.with(|subsurfaces| { + subsurfaces.borrow_mut().push(SubsurfaceInfo { + buffer: self.buffer.clone(), + bounds: layout.bounds(), + alpha: self.alpha, + }) + }); + } +} + +impl<'a> Subsurface<'a> { + pub fn new( + buffer_width: u32, + buffer_height: u32, + buffer: &'a SubsurfaceBuffer, + ) -> Self { + Self { + buffer_size: Size::new(buffer_width as f32, buffer_height as f32), + buffer, + // Matches defaults of image widget + width: Length::Shrink, + height: Length::Shrink, + content_fit: ContentFit::Contain, + alpha: 1., + } + } + + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } + + pub fn content_fit(mut self, content_fit: ContentFit) -> Self { + self.content_fit = content_fit; + self + } + + pub fn alpha(mut self, alpha: f32) -> Self { + self.alpha = alpha; + self + } +} + +impl<'a, Message, Theme, Renderer> From> + for Element<'a, Message, Theme, Renderer> +where + Message: Clone + 'a, + Renderer: renderer::Renderer, +{ + fn from(subsurface: Subsurface<'a>) -> Self { + Self::new(subsurface) + } +} + +delegate_noop!(@ SctkState: ignore WpAlphaModifierV1); +delegate_noop!(@ SctkState: ignore WpAlphaModifierSurfaceV1); 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 01b2032f43..629c874fdc 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/ /// @@ -101,7 +97,7 @@ pub trait Application: Sized { type Executor: Executor; /// The type of __messages__ your [`Application`] will produce. - type Message: std::fmt::Debug + Send; + type Message: std::fmt::Debug + Send + 'static; /// The theme of your [`Application`]. type Theme: Default + StyleSheet; diff --git a/src/error.rs b/src/error.rs index 111bedf245..a1d2640057 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,6 @@ use crate::futures; use crate::graphics; +#[cfg(any(feature = "winit", feature = "wayland"))] use crate::shell; /// An error that occurred while running an application. @@ -18,15 +19,21 @@ pub enum Error { GraphicsCreationFailed(graphics::Error), } +#[cfg(any(feature = "winit", feature = "wayland"))] impl From for Error { fn from(error: shell::Error) -> Error { match error { shell::Error::ExecutorCreationFailed(error) => { Error::ExecutorCreationFailed(error) } + #[cfg(feature = "winit")] shell::Error::WindowCreationFailed(error) => { Error::WindowCreationFailed(Box::new(error)) } + #[cfg(feature = "wayland")] + shell::Error::WindowCreationFailed(error) => { + Error::WindowCreationFailed(error) + } shell::Error::GraphicsCreationFailed(error) => { Error::GraphicsCreationFailed(error) } diff --git a/src/lib.rs b/src/lib.rs index 86207d6e2b..5178165e8e 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,54 +151,85 @@ #![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; pub use crate::core::alignment; -pub use crate::core::border; +pub use crate::core::border::{self, Border, Radius}; pub use crate::core::color; pub use crate::core::gradient; pub use crate::core::{ - Alignment, Background, Border, Color, ContentFit, Degrees, Gradient, - Length, Padding, Pixels, Point, Radians, Rectangle, Shadow, Size, Vector, + id, layout::Limits, Alignment, Background, Color, ContentFit, Degrees, + Gradient, Length, Padding, Pixels, Point, Radians, Rectangle, Size, Vector, }; pub mod clipboard { //! Access the clipboard. - pub use crate::runtime::clipboard::{read, write}; + pub use crate::runtime::clipboard::{ + read, read_primary, write, write_primary, + }; + pub use dnd; + pub use iced_core::clipboard::{read_data, read_primary_data}; + pub use mime; } pub mod executor { @@ -226,6 +257,8 @@ pub mod font { pub mod event { //! Handle events of a user interface. + #[cfg(feature = "wayland")] + pub use crate::core::event::wayland; pub use crate::core::event::{Event, MacOS, PlatformSpecific, Status}; pub use iced_futures::event::{listen, listen_raw, listen_with}; } @@ -300,6 +333,7 @@ pub mod widget { mod runtime {} } +#[cfg(feature = "winit")] pub use application::Application; pub use command::Command; pub use error::Error; @@ -307,6 +341,7 @@ pub use event::Event; pub use executor::Executor; pub use font::Font; pub use renderer::Renderer; +#[cfg(any(feature = "winit", feature = "wayland"))] pub use sandbox::Sandbox; pub use settings::Settings; pub use subscription::Subscription; @@ -322,7 +357,5 @@ pub type Element< Renderer = crate::Renderer, > = crate::core::Element<'a, Message, Theme, Renderer>; -/// The result of running an [`Application`]. -/// -/// [`Application`]: crate::Application +/// The result of running an application. pub type Result = std::result::Result<(), Error>; diff --git a/src/multi_window/application.rs b/src/multi_window/application.rs index ac625281c6..56db2e20bf 100644 --- a/src/multi_window/application.rs +++ b/src/multi_window/application.rs @@ -73,7 +73,7 @@ pub trait Application: Sized { type Executor: Executor; /// The type of __messages__ your [`Application`] will produce. - type Message: std::fmt::Debug + Send; + type Message: std::fmt::Debug + Send + 'static; /// The theme of your [`Application`]. type Theme: Default + StyleSheet; diff --git a/src/sandbox.rs b/src/sandbox.rs index 825a0b60e2..9ccbebb4dc 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!" @@ -86,7 +84,7 @@ use crate::{Application, Command, Element, Error, Settings, Subscription}; /// ``` pub trait Sandbox { /// The type of __messages__ your [`Sandbox`] will produce. - type Message: std::fmt::Debug + Send; + type Message: std::fmt::Debug + Send + Sync + 'static; /// Initializes the [`Sandbox`]. /// @@ -157,6 +155,7 @@ pub trait Sandbox { impl Application for T where T: Sandbox, + T::Message: Send + Sync + 'static, { type Executor = iced_futures::backend::null::Executor; type Flags = (); diff --git a/src/settings.rs b/src/settings.rs index d9476b614e..d257455a7f 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,57 @@ 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, + control_flow_timeout: Some(std::time::Duration::from_millis(250)), + } + } +} diff --git a/src/wayland/mod.rs b/src/wayland/mod.rs new file mode 100644 index 0000000000..5926699080 --- /dev/null +++ b/src/wayland/mod.rs @@ -0,0 +1,190 @@ +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 { + Default::default() + } + + /// Returns the event [`Subscription`] for the current state of the + /// application. + /// + /// A [`Subscription`] will be kept alive as long as you keep returning it, + /// and the __messages__ produced will be handled by + /// [`update`](#tymethod.update). + /// + /// By default, this method returns an empty [`Subscription`]. + fn subscription(&self) -> Subscription { + Subscription::none() + } + + /// Returns the widgets to display in the [`Application`]. + /// + /// These widgets can produce __messages__ based on user interaction. + fn view( + &self, + id: Id, + ) -> Element<'_, Self::Message, Self::Theme, crate::Renderer>; + + /// Returns the scale factor of the [`Application`]. + /// + /// It can be used to dynamically control the size of the UI at runtime + /// (i.e. zooming). + /// + /// For instance, a scale factor of `2.0` will make widgets twice as big, + /// while a scale factor of `0.5` will shrink them to half their size. + /// + /// By default, it returns `1.0`. + fn scale_factor(&self, _id: Id) -> f64 { + 1.0 + } + + /// Runs the [`Application`]. + /// + /// On native platforms, this method will take control of the current thread + /// until the [`Application`] exits. + /// + /// On the web platform, this method __will NOT return__ unless there is an + /// [`Error`] during startup. + /// + /// [`Error`]: crate::Error + fn run(settings: Settings_) -> crate::Result + where + Self: 'static, + { + #[allow(clippy::needless_update)] + let renderer_settings = crate::renderer::Settings { + default_font: settings.default_font, + default_text_size: settings.default_text_size, + antialiasing: if settings.antialiasing { + Some(Antialiasing::MSAAx4) + } else { + None + }, + ..crate::renderer::Settings::default() + }; + + Ok(crate::shell::application::run::< + Instance, + Self::Executor, + crate::renderer::Compositor, + >(settings.into(), renderer_settings)?) + } +} + +struct Instance(A); + +impl crate::runtime::multi_window::Program for Instance +where + A: Application, +{ + type Theme = A::Theme; + type Renderer = crate::Renderer; + type Message = A::Message; + + fn update(&mut self, message: Self::Message) -> Command { + self.0.update(message) + } + + fn view( + &self, + id: Id, + ) -> Element<'_, Self::Message, Self::Theme, Self::Renderer> { + self.0.view(id) + } +} + +impl crate::shell::Application for Instance +where + A: Application, +{ + type Flags = A::Flags; + + fn new(flags: Self::Flags) -> (Self, Command) { + let (app, command) = A::new(flags); + + (Instance(app), command) + } + + fn title(&self, window: Id) -> String { + self.0.title(window) + } + + fn theme(&self, id: Id) -> A::Theme { + self.0.theme(id) + } + + fn style(&self) -> ::Style { + self.0.style() + } + fn subscription(&self) -> Subscription { + self.0.subscription() + } + + fn scale_factor(&self, window: Id) -> f64 { + self.0.scale_factor(window) + } +} diff --git a/src/wayland/sandbox.rs b/src/wayland/sandbox.rs new file mode 100644 index 0000000000..74c6f85879 --- /dev/null +++ b/src/wayland/sandbox.rs @@ -0,0 +1,202 @@ +use iced_core::window::Id; + +use crate::style::Theme; +use crate::theme::{self}; +use crate::{ + wayland::Application, Command, Element, Error, Settings, Subscription, +}; + +/// A sandboxed [`Application`]. +/// +/// If you are a just getting started with the library, this trait offers a +/// simpler interface than [`Application`]. +/// +/// Unlike an [`Application`], a [`Sandbox`] cannot run any asynchronous +/// actions or be initialized with some external flags. However, both traits +/// are very similar and upgrading from a [`Sandbox`] is very straightforward. +/// +/// Therefore, it is recommended to always start by implementing this trait and +/// upgrade only once necessary. +/// +/// # Examples +/// [The repository has a bunch of examples] that use the [`Sandbox`] trait: +/// +/// - [`bezier_tool`], a Paint-like tool for drawing Bézier curves using the +/// [`Canvas widget`]. +/// - [`counter`], the classic counter example explained in [the overview]. +/// - [`custom_widget`], a demonstration of how to build a custom widget that +/// draws a circle. +/// - [`geometry`], a custom widget showcasing how to draw geometry with the +/// `Mesh2D` primitive in [`iced_wgpu`]. +/// - [`pane_grid`], a grid of panes that can be split, resized, and +/// reorganized. +/// - [`progress_bar`], a simple progress bar that can be filled by using a +/// slider. +/// - [`styling`], an example showcasing custom styling with a light and dark +/// theme. +/// - [`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, Theme>; + + /// Returns the current [`Theme`] of the [`Sandbox`]. + /// + /// If you want to use your own custom theme type, you will have to use an + /// [`Application`]. + /// + /// By default, it returns [`Theme::default`]. + fn theme(&self) -> Theme { + Theme::default() + } + + /// Returns the current style variant of [`theme::Application`]. + /// + /// By default, it returns [`theme::Application::default`]. + fn style(&self) -> theme::Application { + theme::Application::default() + } + + /// Returns the scale factor of the [`Sandbox`]. + /// + /// It can be used to dynamically control the size of the UI at runtime + /// (i.e. zooming). + /// + /// For instance, a scale factor of `2.0` will make widgets twice as big, + /// while a scale factor of `0.5` will shrink them to half their size. + /// + /// By default, it returns `1.0`. + fn scale_factor(&self) -> f64 { + 1.0 + } + + /// Runs the [`Sandbox`]. + /// + /// On native platforms, this method will take control of the current thread + /// and __will NOT return__. + /// + /// It should probably be that last thing you call in your `main` function. + fn run(settings: Settings<()>) -> Result<(), Error> + where + Self: 'static + Sized, + { + ::run(settings) + } +} + +impl Application for T +where + T: Sandbox, +{ + type Executor = iced_futures::backend::null::Executor; + type Flags = (); + type Message = T::Message; + type Theme = Theme; + + fn new(_flags: ()) -> (Self, Command) { + (T::new(), Command::none()) + } + + fn title(&self, _id: Id) -> String { + T::title(self) + } + + fn update(&mut self, message: T::Message) -> Command { + T::update(self, message); + + Command::none() + } + + fn theme(&self, _id: Id) -> Self::Theme { + T::theme(self) + } + + fn style(&self) -> theme::Application { + T::style(self) + } + + fn subscription(&self) -> Subscription { + Subscription::none() + } + + fn scale_factor(&self, _id: Id) -> f64 { + T::scale_factor(self) + } + + /// Returns the widgets to display in the [`Sandbox`] window. + /// + /// These widgets can produce __messages__ based on user interaction. + fn view(&self, id: Id) -> Element<'_, Self::Message> { + T::view(self, id) + } +} diff --git a/src/window.rs b/src/window.rs index 9f96da5245..1ceb3f9094 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,8 +1,13 @@ //! Configure the window of your application in native platforms. +#[cfg(feature = "winit")] pub mod icon; +#[cfg(feature = "winit")] pub use icon::Icon; +#[cfg(feature = "winit")] +pub use settings::{PlatformSpecific, Settings}; + pub use crate::core::window::*; pub use crate::runtime::window::*; diff --git a/src/window/icon.rs b/src/window/icon.rs index ef71c22834..6c0d1ca154 100644 --- a/src/window/icon.rs +++ b/src/window/icon.rs @@ -13,7 +13,10 @@ use std::path::Path; /// This will return an error in case the file is missing at run-time. You may prefer [`from_file_data`] instead. #[cfg(feature = "image")] pub fn from_file>(icon_path: P) -> Result { - let icon = image::io::Reader::open(icon_path)?.decode()?.to_rgba8(); + let icon = ::image::io::Reader::open(icon_path)? + .with_guessed_format()? + .decode()? + .to_rgba8(); Ok(icon::from_rgba(icon.to_vec(), icon.width(), icon.height())?) } diff --git a/style/src/application.rs b/style/src/application.rs index e9a1f4ff45..673178b801 100644 --- a/style/src/application.rs +++ b/style/src/application.rs @@ -18,6 +18,9 @@ pub struct Appearance { /// The background [`Color`] of the application. pub background_color: Color, + /// The default icon [`Color`] of the application. + pub icon_color: Color, + /// The default text [`Color`] of the application. pub text_color: Color, } diff --git a/style/src/button.rs b/style/src/button.rs index 0d7a668aca..48df624570 100644 --- a/style/src/button.rs +++ b/style/src/button.rs @@ -1,5 +1,5 @@ //! Change the apperance of a button. -use iced_core::{Background, Border, Color, Shadow, Vector}; +use iced_core::{border::Radius, Background, Border, Color, Shadow, Vector}; /// The appearance of a button. #[derive(Debug, Clone, Copy)] @@ -8,6 +8,14 @@ pub struct Appearance { pub shadow_offset: Vector, /// The [`Background`] of the button. pub background: Option, + /// The border radius of the button. + pub border_radius: Radius, + /// The border width of the button. + pub border_width: f32, + /// The border [`Color`] of the button. + pub border_color: Color, + /// The icon [`Color`] of the button. + pub icon_color: Option, /// The text [`Color`] of the button. pub text_color: Color, /// The [`Border`] of the buton. @@ -21,6 +29,10 @@ impl std::default::Default for Appearance { Self { shadow_offset: Vector::default(), background: None, + border_radius: 0.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + icon_color: None, text_color: Color::BLACK, border: Border::default(), shadow: Shadow::default(), diff --git a/style/src/container.rs b/style/src/container.rs index 00649c25eb..95100cb450 100644 --- a/style/src/container.rs +++ b/style/src/container.rs @@ -4,6 +4,8 @@ use crate::core::{Background, Border, Color, Pixels, Shadow}; /// The appearance of a container. #[derive(Debug, Clone, Copy, Default)] pub struct Appearance { + /// The icon [`Color`] of the container. + pub icon_color: Option, /// The text [`Color`] of the container. pub text_color: Option, /// The [`Background`] of the container. @@ -36,6 +38,8 @@ impl Appearance { pub fn with_background(self, background: impl Into) -> Self { Self { background: Some(background.into()), + icon_color: None, + text_color: None, ..self } } diff --git a/style/src/slider.rs b/style/src/slider.rs index bf1c732961..07dc6be4cf 100644 --- a/style/src/slider.rs +++ b/style/src/slider.rs @@ -1,6 +1,5 @@ //! Change the apperance of a slider. -use crate::core::border; -use crate::core::Color; +use iced_core::{border, gradient::Linear, Color}; /// The appearance of a slider. #[derive(Debug, Clone, Copy)] @@ -9,19 +8,43 @@ pub struct Appearance { pub rail: Rail, /// The appearance of the [`Handle`] of the slider. pub handle: Handle, + /// The appearance of breakpoints. + pub breakpoint: Breakpoint, +} + +/// The appearance of slider breakpoints. +#[derive(Debug, Clone, Copy)] +pub struct Breakpoint { + /// The color of the slider breakpoint. + pub color: Color, } /// The appearance of a slider rail #[derive(Debug, Clone, Copy)] pub struct Rail { /// The colors of the rail of the slider. - pub colors: (Color, Color), + pub colors: RailBackground, /// The width of the stroke of a slider rail. pub width: f32, /// The border radius of the corners of the rail. pub border_radius: border::Radius, } +/// The background color of the rail +#[derive(Debug, Clone, Copy)] +pub enum RailBackground { + /// Start and end colors of the rail + Pair(Color, Color), + /// Linear gradient for the background of the rail + /// includes an option for auto-selecting the angle + Gradient { + /// the linear gradient of the slider + gradient: Linear, + /// Let the widget determin the angle of the gradient + auto_angle: bool, + }, +} + /// The appearance of the handle of a slider. #[derive(Debug, Clone, Copy)] pub struct Handle { @@ -47,6 +70,8 @@ pub enum HandleShape { Rectangle { /// The width of the rectangle. width: u16, + /// The height of the rectangle. + height: u16, /// The border radius of the corners of the rectangle. border_radius: border::Radius, }, diff --git a/style/src/theme.rs b/style/src/theme.rs index 8d1ff23779..0652994eb4 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,12 +16,14 @@ use crate::radio; use crate::rule; use crate::scrollable; use crate::slider; +use crate::slider::RailBackground; use crate::svg; use crate::text_editor; use crate::text_input; use crate::toggler; -use crate::core::{Background, Border, Color, Shadow, Vector}; +use iced_core::border::Radius; +use iced_core::{Background, Border, Color, Shadow, Vector}; use std::fmt; use std::rc::Rc; @@ -140,6 +142,7 @@ impl application::StyleSheet for Theme { match style { Application::Default => application::Appearance { background_color: palette.background.base.color, + icon_color: palette.background.base.icon, text_color: palette.background.base.text, }, Application::Custom(custom) => custom.appearance(self), @@ -431,6 +434,7 @@ impl container::StyleSheet for Theme { let palette = self.extended_palette(); container::Appearance { + icon_color: None, text_color: None, background: Some(palette.background.weak.color.into()), border: Border::with_radius(2), @@ -470,6 +474,7 @@ impl slider::StyleSheet for Theme { let handle = slider::Handle { shape: slider::HandleShape::Rectangle { + height: 8, width: 8, border_radius: 4.0.into(), }, @@ -480,7 +485,7 @@ impl slider::StyleSheet for Theme { slider::Appearance { rail: slider::Rail { - colors: ( + colors: RailBackground::Pair( palette.primary.base.color, palette.secondary.base.color, ), @@ -492,6 +497,9 @@ impl slider::StyleSheet for Theme { border_color: palette.primary.base.color, ..handle }, + breakpoint: slider::Breakpoint { + color: palette.background.weak.text, + }, } } Slider::Custom(custom) => custom.active(self), @@ -728,6 +736,40 @@ impl toggler::StyleSheet for Theme { palette.background.base.color }, foreground_border: None, + border_radius: Radius::from(8.0), + handle_radius: Radius::from(8.0), + handle_margin: 2.0, + } + } + Toggler::Custom(custom) => custom.active(self, is_active), + } + } + + fn disabled( + &self, + style: &Self::Style, + is_active: bool, + ) -> toggler::Appearance { + match style { + Toggler::Default => { + let palette = self.extended_palette(); + + toggler::Appearance { + background: if is_active { + palette.primary.strong.color + } else { + palette.background.strong.color + }, + background_border: None, + foreground: if is_active { + palette.primary.strong.text + } else { + palette.background.base.color + }, + foreground_border: None, + border_radius: Radius::from(8.0), + handle_radius: Radius::from(8.0), + handle_margin: 2.0, } } Toggler::Custom(custom) => custom.active(self, is_active), diff --git a/style/src/theme/palette.rs b/style/src/theme/palette.rs index 76977a2942..5500cf7b11 100644 --- a/style/src/theme/palette.rs +++ b/style/src/theme/palette.rs @@ -120,12 +120,15 @@ impl Extended { } } -/// A pair of background and text colors. +/// Recommended background, icon, and text [`Color`]. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Pair { /// The background color. pub color: Color, + /// The icon color, which defaults to the text color. + pub icon: Color, + /// The text color. /// /// It's guaranteed to be readable on top of the background [`color`]. @@ -137,9 +140,12 @@ pub struct Pair { impl Pair { /// Creates a new [`Pair`] from a background [`Color`] and some text [`Color`]. pub fn new(color: Color, text: Color) -> Self { + let text = readable(color, text); + Self { color, - text: readable(color, text), + icon: text, + text, } } } diff --git a/style/src/toggler.rs b/style/src/toggler.rs index abc73f2a25..ff0dc0f3be 100644 --- a/style/src/toggler.rs +++ b/style/src/toggler.rs @@ -1,5 +1,5 @@ //! Change the appearance of a toggler. -use iced_core::Color; +use iced_core::{border::Radius, Color}; /// The appearance of a toggler. #[derive(Debug, Clone, Copy)] @@ -12,6 +12,12 @@ pub struct Appearance { pub foreground: Color, /// The [`Color`] of the foreground border of the toggler. pub foreground_border: Option, + /// The border radius of the toggler. + pub border_radius: Radius, + /// the radius of the handle of the toggler + pub handle_radius: Radius, + /// the space between the handle and the border of the toggler + pub handle_margin: f32, } /// A set of rules that dictate the style of a toggler. @@ -28,4 +34,9 @@ pub trait StyleSheet { /// /// [`Style`]: Self::Style fn hovered(&self, style: &Self::Style, is_active: bool) -> Appearance; + + /// Returns the disabled [`Appearance`] of the toggler for the provided [`Style`]. + /// + /// [`Style`]: Self::Style + fn disabled(&self, style: &Self::Style, is_active: bool) -> Appearance; } 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 ea4a3ec6fe..de4b340773 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -161,8 +161,9 @@ impl Backend { return; } - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) - .then_some(clip_mask as &_); + let clip_mask = (!physical_bounds + .is_within_strict(&clip_bounds)) + .then_some(clip_mask as &_); let transform = tiny_skia::Transform::from_translate( translation.x, @@ -177,14 +178,24 @@ impl Backend { .min(bounds.height / 2.0); let mut fill_border_radius = <[f32; 4]>::from(border.radius); + // Offset the fill by the border width + let path_bounds = Rectangle { + x: bounds.x + border_width, + y: bounds.y + border_width, + width: bounds.width - 2.0 * border_width, + height: bounds.height - 2.0 * border_width, + }; + // fill border radius is the border radius minus the border width for radius in &mut fill_border_radius { - *radius = (*radius) - .min(bounds.width / 2.0) - .min(bounds.height / 2.0); + *radius = (*radius - border_width) + .min(path_bounds.width / 2.0) + .min(path_bounds.height / 2.0); } - let path = rounded_rectangle(*bounds, fill_border_radius); + let path = rounded_rectangle(path_bounds, fill_border_radius); - if shadow.color.a > 0.0 { + // TODO: Disabled due to graphical glitches + // if shadow.color.a > 0.0 { + if false { let shadow_bounds = (Rectangle { x: bounds.x + shadow.offset.x - shadow.blur_radius, y: bounds.y + shadow.offset.y - shadow.blur_radius, @@ -368,26 +379,32 @@ impl Backend { clip_mask, ); } else { + let transform = tiny_skia::Transform::from_translate( + translation.x, + translation.y, + ); + // Draw corners that have too small border radii as having no border radius, // but mask them with the rounded rectangle with the correct border radius. let mut temp_pixmap = tiny_skia::Pixmap::new( - bounds.width as u32, - bounds.height as u32, + physical_bounds.width as u32, + physical_bounds.height as u32, ) .unwrap(); let mut quad_mask = tiny_skia::Mask::new( - bounds.width as u32, - bounds.height as u32, + physical_bounds.width as u32, + physical_bounds.height as u32, ) .unwrap(); let zero_bounds = Rectangle { x: 0.0, y: 0.0, - width: bounds.width, - height: bounds.height, + width: physical_bounds.width, + height: physical_bounds.height, }; + let path = rounded_rectangle(zero_bounds, fill_border_radius); @@ -398,12 +415,17 @@ impl Backend { transform, ); let path_bounds = Rectangle { - x: border_width / 2.0, - y: border_width / 2.0, - width: bounds.width - border_width, - height: bounds.height - border_width, + x: (border_width / 2.0) * scale_factor, + y: (border_width / 2.0) * scale_factor, + width: physical_bounds.width + - border_width * scale_factor, + height: physical_bounds.height + - border_width * scale_factor, }; + for r in &mut border_radius { + *r /= scale_factor; + } let border_radius_path = rounded_rectangle(path_bounds, border_radius); @@ -417,7 +439,7 @@ impl Backend { ..tiny_skia::Paint::default() }, &tiny_skia::Stroke { - width: border_width, + width: border_width * scale_factor, ..tiny_skia::Stroke::default() }, transform, @@ -425,8 +447,8 @@ impl Backend { ); pixels.draw_pixmap( - bounds.x as i32, - bounds.y as i32, + (bounds.x / scale_factor) as i32, + (bounds.y / scale_factor) as i32, temp_pixmap.as_ref(), &tiny_skia::PixmapPaint::default(), transform, @@ -448,8 +470,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, @@ -473,8 +496,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, @@ -504,8 +528,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, @@ -556,6 +581,7 @@ impl Backend { handle, filter_method, bounds, + border_radius, } => { let physical_bounds = (*bounds + translation) * scale_factor; @@ -563,8 +589,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, @@ -579,6 +606,7 @@ impl Backend { pixels, transform, clip_mask, + *border_radius, ); } #[cfg(not(feature = "image"))] @@ -599,8 +627,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, @@ -635,8 +664,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, @@ -669,8 +699,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 9413e31170..1e1801186f 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -82,18 +82,20 @@ impl Pipeline { let mut font_system = font_system().write().expect("Write font system"); - draw( - font_system.raw(), - &mut self.glyph_cache, - editor.buffer(), - Rectangle::new(position, editor.bounds()), - color, - alignment::Horizontal::Left, - alignment::Vertical::Top, - scale_factor, - pixels, - clip_mask, - ); + editor.with_buffer(|buffer| { + draw( + font_system.raw(), + &mut self.glyph_cache, + buffer, + Rectangle::new(position, editor.bounds()), + color, + alignment::Horizontal::Left, + alignment::Vertical::Top, + scale_factor, + pixels, + clip_mask, + ); + }); } pub fn draw_cached( @@ -159,13 +161,16 @@ impl Pipeline { ) { let mut font_system = font_system().write().expect("Write font system"); - let (width, height) = buffer.size(); + let (width_opt, height_opt) = buffer.size(); draw( font_system.raw(), &mut self.glyph_cache, buffer, - Rectangle::new(position, Size::new(width, height)), + Rectangle::new( + position, + Size::new(width_opt.unwrap_or(0.0), height_opt.unwrap_or(0.0)), + ), color, alignment::Horizontal::Left, alignment::Vertical::Top, diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 21ccf6200e..37b03cb054 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -183,31 +183,30 @@ pub fn present>( }) .unwrap_or_else(|| vec![Rectangle::with_size(viewport.logical_size())]); - if damage.is_empty() { - return Ok(()); - } - surface.primitive_stack.push_front(primitives.to_vec()); - surface.background_color = background_color; - let damage = damage::group(damage, scale_factor, physical_size); + if !damage.is_empty() { + surface.background_color = background_color; - let mut pixels = tiny_skia::PixmapMut::from_bytes( - bytemuck::cast_slice_mut(&mut buffer), - physical_size.width, - physical_size.height, - ) - .expect("Create pixel map"); + let damage = damage::group(damage, scale_factor, physical_size); - backend.draw( - &mut pixels, - &mut surface.clip_mask, - primitives, - viewport, - &damage, - background_color, - overlay, - ); + let mut pixels = tiny_skia::PixmapMut::from_bytes( + bytemuck::cast_slice_mut(&mut buffer), + physical_size.width, + physical_size.height, + ) + .expect("Create pixel map"); + + backend.draw( + &mut pixels, + &mut surface.clip_mask, + primitives, + viewport, + &damage, + background_color, + overlay, + ); + } buffer.present().map_err(|_| compositor::SurfaceError::Lost) } diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 1d3b57a710..15499d38f9 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -42,3 +42,15 @@ resvg.optional = true tracing.workspace = true tracing.optional = true + +[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] +rustix = { version = "0.38" } +raw-window-handle.workspace = true +sctk.workspace = true +wayland-protocols.workspace = true +wayland-backend = { version = "0.3.3", features = ["client_system"] } +wayland-client = { version = "0.31.2" } +wayland-sys = { version = "0.31.1", features = ["dlopen"] } +as-raw-xcb-connection = "1.0.1" +tiny-xlib = "0.2.3" +x11rb = { version = "0.13.1", features = ["allow-unsafe-code", "dl-libxcb", "dri3", "randr"] } \ No newline at end of file diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs index 1e5d3ee0d9..c7e91bb1b2 100644 --- a/wgpu/src/image.rs +++ b/wgpu/src/image.rs @@ -400,6 +400,7 @@ impl Pipeline { handle, filter_method, bounds, + .. } => { if let Some(atlas_entry) = raster_cache.upload( device, @@ -607,12 +608,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 e213c95f18..f5e7eac44a 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())); @@ -220,6 +220,7 @@ impl<'a> Layer<'a> { handle, filter_method, bounds, + border_radius, } => { let layer = &mut layers[current_layer]; @@ -227,6 +228,7 @@ impl<'a> Layer<'a> { handle: handle.clone(), filter_method: *filter_method, bounds: *bounds + translation, + border_radius: *border_radius, }); } Primitive::Svg { diff --git a/wgpu/src/layer/image.rs b/wgpu/src/layer/image.rs index facbe1922c..0ea17bbb34 100644 --- a/wgpu/src/layer/image.rs +++ b/wgpu/src/layer/image.rs @@ -15,6 +15,9 @@ pub enum Image { /// The bounds of the image. bounds: Rectangle, + + /// Border radius to apply + border_radius: [f32; 4], }, /// A vector image. Vector { 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 dca09cb8a8..c02b6ea70f 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -193,13 +193,16 @@ impl Pipeline { return None; }; - let (width, height) = buffer.size(); + let (width_opt, height_opt) = buffer.size(); ( buffer.as_ref(), Rectangle::new( text.position, - Size::new(width, height), + Size::new( + width_opt.unwrap_or(0.0), + height_opt.unwrap_or(0.0), + ), ), alignment::Horizontal::Left, alignment::Vertical::Top, diff --git a/wgpu/src/window.rs b/wgpu/src/window.rs index 9545a14e5b..92f1687372 100644 --- a/wgpu/src/window.rs +++ b/wgpu/src/window.rs @@ -1,5 +1,41 @@ //! Display rendering results on windows. pub mod compositor; +#[cfg(all(unix, not(target_os = "macos")))] +mod wayland; +#[cfg(all(unix, not(target_os = "macos")))] +mod x11; pub use compositor::Compositor; pub use wgpu::Surface; + +#[cfg(all(unix, not(target_os = "macos")))] +use rustix::fs::{major, minor}; +#[cfg(all(unix, not(target_os = "macos")))] +use std::{fs::File, io::Read, path::PathBuf}; + +#[cfg(all(unix, not(target_os = "macos")))] +fn ids_from_dev(dev: u64) -> Option<(u16, u16)> { + let path = PathBuf::from(format!( + "/sys/dev/char/{}:{}/device", + major(dev), + minor(dev) + )); + let vendor = { + let path = path.join("vendor"); + let mut file = File::open(&path).ok()?; + let mut contents = String::new(); + let _ = file.read_to_string(&mut contents).ok()?; + u16::from_str_radix(contents.trim().trim_start_matches("0x"), 16) + .ok()? + }; + let device = { + let path = path.join("device"); + let mut file = File::open(&path).ok()?; + let mut contents = String::new(); + let _ = file.read_to_string(&mut contents).ok()?; + u16::from_str_radix(contents.trim().trim_start_matches("0x"), 16) + .ok()? + }; + + Some((vendor, device)) +} diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 58f3f65410..c0839451ac 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -6,6 +6,11 @@ use crate::graphics::compositor; use crate::graphics::{Error, Viewport}; use crate::{Backend, Primitive, Renderer, Settings}; +#[cfg(all(unix, not(target_os = "macos")))] +use super::wayland::get_wayland_device_ids; +#[cfg(all(unix, not(target_os = "macos")))] +use super::x11::get_x11_device_ids; + /// A window graphics backend for iced powered by `wgpu`. #[allow(missing_debug_implementations)] pub struct Compositor { @@ -25,6 +30,26 @@ impl Compositor { settings: Settings, compatible_window: Option, ) -> Option { + #[cfg(all(unix, not(target_os = "macos")))] + let ids = compatible_window.as_ref().and_then(|window| { + get_wayland_device_ids(window) + .or_else(|| get_x11_device_ids(window)) + }); + + // HACK: + // 1. If we specifically didn't select an nvidia gpu + // 2. and nobody set an adapter name, + // 3. and the user didn't request the high power pref + // => don't load the nvidia icd, as it might power on the gpu in hybrid setups causing severe delays + #[cfg(all(unix, not(target_os = "macos")))] + if !matches!(ids, Some((0x10de, _))) + && std::env::var_os("WGPU_ADAPTER_NAME").is_none() + && std::env::var("WGPU_POWER_PREF").as_deref() != Ok("high") + { + std::env::set_var("VK_LOADER_DRIVERS_DISABLE", "nvidia*"); + } + + // only load the instance after setting environment variables, this initializes the vulkan loader let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { backends: settings.internal_backend, ..Default::default() @@ -32,32 +57,77 @@ impl Compositor { log::info!("{settings:#?}"); + let available_adapters = + instance.enumerate_adapters(settings.internal_backend); + + std::env::remove_var("VK_LOADER_DRIVERS_DISABLE"); + #[cfg(not(target_arch = "wasm32"))] if log::max_level() >= log::LevelFilter::Info { - let available_adapters: Vec<_> = instance - .enumerate_adapters(settings.internal_backend) - .iter() - .map(wgpu::Adapter::get_info) - .collect(); - log::info!("Available adapters: {available_adapters:#?}"); + log::info!( + "Available adapters: {:#?}", + available_adapters.iter().map(wgpu::Adapter::get_info) + ); } #[allow(unsafe_code)] let compatible_surface = compatible_window .and_then(|window| instance.create_surface(window).ok()); - let adapter = instance - .request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::util::power_preference_from_env() - .unwrap_or(if settings.antialiasing.is_none() { - wgpu::PowerPreference::LowPower + let mut adapter = None; + #[cfg_attr(not(unix), allow(dead_code))] + if std::env::var_os("WGPU_ADAPTER_NAME").is_none() { + #[cfg(all(unix, not(target_os = "macos")))] + if let Some((vendor_id, device_id)) = ids { + adapter = available_adapters + .into_iter() + .filter(|adapter| { + let info = adapter.get_info(); + info.device == device_id as u32 + && info.vendor == vendor_id as u32 + }) + .find(|adapter| { + if let Some(surface) = compatible_surface.as_ref() { + adapter.is_surface_supported(surface) + } else { + true + } + }); + } + } else if let Ok(name) = std::env::var("WGPU_ADAPTER_NAME") { + adapter = available_adapters + .into_iter() + .filter(|adapter| { + let info = adapter.get_info(); + info.name == name + }) + .find(|adapter| { + if let Some(surface) = compatible_surface.as_ref() { + adapter.is_surface_supported(surface) } else { - wgpu::PowerPreference::HighPerformance - }), - compatible_surface: compatible_surface.as_ref(), - force_fallback_adapter: false, - }) - .await?; + true + } + }); + } + + let adapter = + match adapter { + Some(adapter) => adapter, + None => instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: + wgpu::util::power_preference_from_env().unwrap_or( + if settings.antialiasing.is_none() { + wgpu::PowerPreference::LowPower + } else { + wgpu::PowerPreference::HighPerformance + }, + ), + compatible_surface: compatible_surface.as_ref(), + force_fallback_adapter: false, + }) + .await?, + }; log::info!("Selected: {:#?}", adapter.get_info()); @@ -241,6 +311,21 @@ impl graphics::Compositor for Compositor { width: u32, height: u32, ) { + let caps = surface.get_capabilities(&self.adapter); + let alpha_mode = if caps + .alpha_modes + .contains(&wgpu::CompositeAlphaMode::PostMultiplied) + { + wgpu::CompositeAlphaMode::PostMultiplied + } else if caps + .alpha_modes + .contains(&wgpu::CompositeAlphaMode::PreMultiplied) + { + wgpu::CompositeAlphaMode::PreMultiplied + } else { + wgpu::CompositeAlphaMode::Auto + }; + surface.configure( &self.device, &wgpu::SurfaceConfiguration { @@ -249,7 +334,7 @@ impl graphics::Compositor for Compositor { present_mode: self.settings.present_mode, width, height, - alpha_mode: wgpu::CompositeAlphaMode::Auto, + alpha_mode, view_formats: vec![], desired_maximum_frame_latency: 2, }, diff --git a/wgpu/src/window/wayland.rs b/wgpu/src/window/wayland.rs new file mode 100644 index 0000000000..b50f5d6909 --- /dev/null +++ b/wgpu/src/window/wayland.rs @@ -0,0 +1,115 @@ +use crate::graphics::compositor::Window; +use raw_window_handle::{RawDisplayHandle, WaylandDisplayHandle}; +use sctk::{ + dmabuf::{DmabufFeedback, DmabufHandler, DmabufState}, + registry::{ProvidesRegistryState, RegistryState}, + registry_handlers, +}; +use wayland_client::{ + backend::Backend, globals::registry_queue_init, protocol::wl_buffer, + Connection, QueueHandle, +}; +use wayland_protocols::wp::linux_dmabuf::zv1::client::{ + zwp_linux_buffer_params_v1, zwp_linux_dmabuf_feedback_v1, +}; + +struct AppData { + registry_state: RegistryState, + dmabuf_state: DmabufState, + feedback: Option, +} + +impl DmabufHandler for AppData { + fn dmabuf_state(&mut self) -> &mut DmabufState { + &mut self.dmabuf_state + } + + fn dmabuf_feedback( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _proxy: &zwp_linux_dmabuf_feedback_v1::ZwpLinuxDmabufFeedbackV1, + feedback: DmabufFeedback, + ) { + self.feedback = Some(feedback); + } + + fn created( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _params: &zwp_linux_buffer_params_v1::ZwpLinuxBufferParamsV1, + _buffer: wl_buffer::WlBuffer, + ) { + } + + fn failed( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _params: &zwp_linux_buffer_params_v1::ZwpLinuxBufferParamsV1, + ) { + } + + fn released( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _buffer: &wl_buffer::WlBuffer, + ) { + } +} + +impl ProvidesRegistryState for AppData { + fn registry(&mut self) -> &mut RegistryState { + &mut self.registry_state + } + registry_handlers![,]; +} + +pub fn get_wayland_device_ids(window: &W) -> Option<(u16, u16)> { + if !wayland_sys::client::is_lib_available() { + return None; + } + + let conn = match window.display_handle().map(|handle| handle.as_raw()) { + #[allow(unsafe_code)] + Ok(RawDisplayHandle::Wayland(WaylandDisplayHandle { + display, .. + })) => Connection::from_backend(unsafe { + Backend::from_foreign_display(display.as_ptr() as *mut _) + }), + _ => { + return None; + } + }; + + let (globals, mut event_queue) = registry_queue_init(&conn).unwrap(); + let qh = event_queue.handle(); + + let mut app_data = AppData { + registry_state: RegistryState::new(&globals), + dmabuf_state: DmabufState::new(&globals, &qh), + feedback: None, + }; + + match app_data.dmabuf_state.version() { + Some(4..) => { + let _ = app_data.dmabuf_state.get_default_feedback(&qh).unwrap(); + + let feedback = loop { + let _ = event_queue.blocking_dispatch(&mut app_data).ok()?; + if let Some(feedback) = app_data.feedback.as_ref() { + break feedback; + } + }; + + let dev = feedback.main_device(); + super::ids_from_dev(dev) + } + _ => None, + } +} + +sctk::delegate_dmabuf!(AppData); +sctk::delegate_registry!(AppData); diff --git a/wgpu/src/window/x11.rs b/wgpu/src/window/x11.rs new file mode 100644 index 0000000000..58da401a2e --- /dev/null +++ b/wgpu/src/window/x11.rs @@ -0,0 +1,171 @@ +use std::{ + fs, + io::{BufRead, BufReader}, + path::Path, +}; + +use crate::graphics::compositor::Window; + +use as_raw_xcb_connection::AsRawXcbConnection; +use raw_window_handle::{ + RawDisplayHandle, XcbDisplayHandle, XlibDisplayHandle, +}; +use rustix::fs::{fstat, stat}; +use tiny_xlib::Display; +use x11rb::{ + connection::{Connection, RequestConnection}, + protocol::{ + dri3::{ConnectionExt as _, X11_EXTENSION_NAME as DRI3_NAME}, + randr::{ + ConnectionExt as _, ProviderCapability, + X11_EXTENSION_NAME as RANDR_NAME, + }, + }, + xcb_ffi::XCBConnection, +}; + +pub fn get_x11_device_ids(window: &W) -> Option<(u16, u16)> { + x11rb::xcb_ffi::load_libxcb().ok()?; + + #[allow(unsafe_code)] + let (conn, screen) = match window + .display_handle() + .map(|handle| handle.as_raw()) + { + #[allow(unsafe_code)] + Ok(RawDisplayHandle::Xlib(XlibDisplayHandle { + display, + screen, + .. + })) => match display { + Some(ptr) => unsafe { + let xlib_display = Display::from_ptr(ptr.as_ptr()); + let conn = XCBConnection::from_raw_xcb_connection( + xlib_display.as_raw_xcb_connection() as *mut _, + false, + ) + .ok(); + // intentially leak the display, we don't want to close the connection + + (conn?, screen) + }, + None => (XCBConnection::connect(None).ok()?.0, screen), + }, + Ok(RawDisplayHandle::Xcb(XcbDisplayHandle { + connection, + screen, + .. + })) => match connection { + Some(ptr) => ( + unsafe { + XCBConnection::from_raw_xcb_connection(ptr.as_ptr(), false) + .ok()? + }, + screen, + ), + None => (XCBConnection::connect(None).ok()?.0, screen), + }, + _ => { + return None; + } + }; + let root = conn.setup().roots[screen as usize].root; + + // The nvidia xorg driver advertises DRI2 and DRI3, + // but doesn't really return any useful data for either of them. + // We also can't query EGL, as a display created from an X11 display + // running on the properietary driver won't return an EGLDevice. + // + // So we have to resort to hacks. + + // check for randr + let _ = conn.extension_information(RANDR_NAME).ok()??; + // check version, because we need providers to exist + let version = conn.randr_query_version(1, 4).ok()?.reply().ok()?; + if version.major_version < 1 + || (version.major_version == 1 && version.minor_version < 4) + { + return None; + } + + // get the name of the first Source Output provider, that will be our main device + let randr = conn.randr_get_providers(root).ok()?.reply().ok()?; + let mut name = None; + for provider in randr.providers { + let info = conn + .randr_get_provider_info(provider, randr.timestamp) + .ok()? + .reply() + .ok()?; + if info + .capabilities + .contains(ProviderCapability::SOURCE_OUTPUT) + || name.is_none() + { + name = std::str::from_utf8(&info.name) + .ok() + .map(ToString::to_string); + } + } + + // if that name is formatted `NVIDIA-x`, then x represents the /dev/nvidiaX number, which we can relate to /dev/dri + if let Some(number) = name.and_then(|name| { + name.trim().strip_prefix("NVIDIA-")?.parse::().ok() + }) { + // let it be known, that I hate this "interface"... + for busid in fs::read_dir("/proc/driver/nvidia/gpus") + .ok()? + .map(Result::ok) + .flatten() + { + for line in BufReader::new( + fs::File::open(busid.path().join("information")).ok()?, + ) + .lines() + { + if let Ok(line) = line { + if line.starts_with("Device Minor") { + if let Some((_, num)) = line.split_once(":") { + let minor = num.trim().parse::().ok()?; + if minor == number { + // we found the device + for device in fs::read_dir( + Path::new("/sys/module/nvidia/drivers/pci:nvidia/") + .join(busid.file_name()) + .join("drm"), + ) + .ok()? + .map(Result::ok) + .flatten() + { + let device = device.file_name(); + if device.to_string_lossy().starts_with("card") + || device.to_string_lossy().starts_with("render") + { + let stat = + stat(Path::new("/dev/dri").join(device)).ok()?; + let dev = stat.st_rdev; + return super::ids_from_dev(dev); + } + } + } + } + } + } + } + } + + None + } else { + // check via DRI3 + let _ = conn.extension_information(DRI3_NAME).ok()??; + // we have dri3, dri3_open exists on any version, so skip version checks. + + // provider being NONE tells the X server to use the RandR provider. + let dri3 = conn.dri3_open(root, x11rb::NONE).ok()?.reply().ok()?; + let device_fd = dri3.device_fd; + let stat = fstat(device_fd).ok()?; + let dev = stat.st_rdev; + super::ids_from_dev(dev) + } +} diff --git a/widget/Cargo.toml b/widget/Cargo.toml index e8e363c41d..fa26061721 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -21,15 +21,22 @@ 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 +window_clipboard.workspace = true +dnd.workspace = true ouroboros.workspace = true ouroboros.optional = true diff --git a/widget/src/button.rs b/widget/src/button.rs index 3b11e8a7aa..ddfd7ba87a 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,7 +19,8 @@ use crate::core::{ Shell, Size, Widget, }; -pub use crate::style::button::{Appearance, StyleSheet}; +use iced_renderer::core::widget::{operation, OperationOutputWrapper}; +pub use iced_style::button::{Appearance, StyleSheet}; /// A generic widget that produces a message when pressed. /// @@ -57,6 +63,13 @@ where Renderer: crate::core::Renderer, { content: Element<'a, Message, Theme, Renderer>, + id: Id, + #[cfg(feature = "a11y")] + name: Option>, + #[cfg(feature = "a11y")] + description: Option>, + #[cfg(feature = "a11y")] + label: Option>, on_press: Option, width: Length, height: Length, @@ -78,6 +91,13 @@ where Button { content, + id: Id::unique(), + #[cfg(feature = "a11y")] + name: None, + #[cfg(feature = "a11y")] + description: None, + #[cfg(feature = "a11y")] + label: None, on_press: None, width: size.width.fluid(), height: size.height.fluid(), @@ -126,6 +146,47 @@ where self.style = style.into(); self } + + /// Sets the [`Id`] of the [`Button`]. + pub fn id(mut self, id: Id) -> Self { + self.id = id; + self + } + + #[cfg(feature = "a11y")] + /// Sets the name of the [`Button`]. + pub fn name(mut self, name: impl Into>) -> Self { + self.name = Some(name.into()); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description_widget( + mut self, + description: &T, + ) -> Self { + self.description = Some(iced_accessibility::Description::Id( + description.description(), + )); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description(mut self, description: impl Into>) -> Self { + self.description = + Some(iced_accessibility::Description::Text(description.into())); + self + } + + #[cfg(feature = "a11y")] + /// Sets the label of the [`Button`]. + pub fn label(mut self, label: &dyn iced_accessibility::Labels) -> Self { + self.label = + Some(label.label().into_iter().map(|l| l.into()).collect()); + self + } } impl<'a, Message, Theme, Renderer> Widget @@ -147,8 +208,8 @@ where vec![Tree::new(&self.content)] } - fn diff(&self, tree: &mut Tree) { - tree.diff_children(std::slice::from_ref(&self.content)); + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(std::slice::from_mut(&mut self.content)) } fn size(&self) -> Size { @@ -178,7 +239,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.content.as_widget().operate( @@ -214,9 +275,15 @@ where return event::Status::Captured; } - update(event, layout, cursor, shell, &self.on_press, || { - tree.state.downcast_mut::() - }) + update( + self.id.clone(), + event, + layout, + cursor, + shell, + &self.on_press, + || tree.state.downcast_mut::(), + ) } fn draw( @@ -224,7 +291,7 @@ where tree: &Tree, renderer: &mut Renderer, theme: &Theme, - _style: &renderer::Style, + renderer_style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, _viewport: &Rectangle, @@ -247,7 +314,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, @@ -278,6 +349,90 @@ where renderer, ) } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + p: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::{ + accesskit::{ + Action, DefaultActionVerb, NodeBuilder, NodeId, Rect, Role, + }, + A11yNode, A11yTree, + }; + + let child_layout = layout.children().next().unwrap(); + let child_tree = &state.children[0]; + let child_tree = + self.content + .as_widget() + .a11y_nodes(child_layout, child_tree, p); + + let Rectangle { + x, + y, + width, + height, + } = layout.bounds(); + let bounds = Rect::new( + x as f64, + y as f64, + (x + width) as f64, + (y + height) as f64, + ); + let is_hovered = state.state.downcast_ref::().is_hovered; + + let mut node = NodeBuilder::new(Role::Button); + node.add_action(Action::Focus); + node.add_action(Action::Default); + node.set_bounds(bounds); + if let Some(name) = self.name.as_ref() { + node.set_name(name.clone()); + } + match self.description.as_ref() { + Some(iced_accessibility::Description::Id(id)) => { + node.set_described_by( + id.iter() + .cloned() + .map(|id| NodeId::from(id)) + .collect::>(), + ); + } + Some(iced_accessibility::Description::Text(text)) => { + node.set_description(text.clone()); + } + None => {} + } + + if let Some(label) = self.label.as_ref() { + node.set_labelled_by(label.clone()); + } + + if self.on_press.is_none() { + node.set_disabled() + } + if is_hovered { + node.set_hovered() + } + node.set_default_action_verb(DefaultActionVerb::Click); + + A11yTree::node_with_child_tree( + A11yNode::new(node, self.id.clone()), + child_tree, + ) + } + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn set_id(&mut self, id: Id) { + self.id = id; + } } impl<'a, Message, Theme, Renderer> From> @@ -295,7 +450,9 @@ where /// The local state of a [`Button`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct State { + is_hovered: bool, is_pressed: bool, + is_focused: bool, } impl State { @@ -303,11 +460,32 @@ impl State { pub fn new() -> State { State::default() } + + /// Returns whether the [`Button`] is currently focused or not. + pub fn is_focused(&self) -> bool { + self.is_focused + } + + /// Returns whether the [`Button`] is currently hovered or not. + pub fn is_hovered(&self) -> bool { + self.is_hovered + } + + /// Focuses the [`Button`]. + pub fn focus(&mut self) { + self.is_focused = true; + } + + /// Unfocuses the [`Button`]. + pub fn unfocus(&mut self) { + self.is_focused = false; + } } /// Processes the given [`Event`] and updates the [`State`] of a [`Button`] /// accordingly. pub fn update<'a, Message: Clone>( + _id: Id, event: Event, layout: Layout<'_>, cursor: mouse::Cursor, @@ -348,9 +526,43 @@ 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, .. }) => { + if let Some(on_press) = on_press.clone() { + let state = state(); + if state.is_focused + && matches!( + key, + keyboard::Key::Named(keyboard::key::Named::Enter) + ) + { + state.is_pressed = true; + shell.publish(on_press); + return event::Status::Captured; + } + } + } + Event::Touch(touch::Event::FingerLost { .. }) + | Event::Mouse(mouse::Event::CursorLeft) => { + let state = state(); + state.is_hovered = false; state.is_pressed = false; } _ => {} @@ -432,3 +644,22 @@ pub fn mouse_interaction( mouse::Interaction::default() } } + +/// Produces a [`Command`] that focuses the [`Button`] with the given [`Id`]. +pub fn focus(id: Id) -> Command { + Command::widget(operation::focusable::focus(id)) +} + +impl operation::Focusable for State { + fn is_focused(&self) -> bool { + State::is_focused(self) + } + + fn focus(&mut self) { + State::focus(self) + } + + fn unfocus(&mut self) { + State::unfocus(self) + } +} diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index 6f559ccc72..cb2fb8cca4 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -1,4 +1,9 @@ //! Show toggle controls using checkboxes. +use iced_renderer::core::Size; +use iced_runtime::core::widget::Id; +#[cfg(feature = "a11y")] +use std::borrow::Cow; + use crate::core::alignment; use crate::core::event::{self, Event}; use crate::core::layout; @@ -9,7 +14,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 crate::style::checkbox::{Appearance, StyleSheet}; @@ -42,6 +48,12 @@ pub struct Checkbox< Theme: StyleSheet + crate::text::StyleSheet, Renderer: text::Renderer, { + id: Id, + label_id: Id, + #[cfg(feature = "a11y")] + name: Option>, + #[cfg(feature = "a11y")] + description: Option>, is_checked: bool, on_toggle: Box Message + 'a>, label: String, @@ -51,6 +63,7 @@ pub struct Checkbox< text_size: Option, text_line_height: text::LineHeight, text_shaping: text::Shaping, + text_wrap: text::Wrap, font: Option, icon: Icon, style: ::Style, @@ -80,6 +93,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(), @@ -88,14 +107,16 @@ where spacing: Self::DEFAULT_SPACING, text_size: None, text_line_height: text::LineHeight::default(), - text_shaping: text::Shaping::Basic, + text_shaping: text::Shaping::Advanced, + text_wrap: text::Wrap::default(), 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, + wrap: text::Wrap::default(), }, style: Default::default(), } @@ -140,6 +161,12 @@ where self } + /// Sets the [`text::Wrap`] mode of the [`Checkbox`]. + pub fn text_wrap(mut self, wrap: text::Wrap) -> Self { + self.text_wrap = wrap; + self + } + /// Sets the [`Renderer::Font`] of the text of the [`Checkbox`]. /// /// [`Renderer::Font`]: crate::core::text::Renderer @@ -162,6 +189,33 @@ where self.style = style.into(); self } + + #[cfg(feature = "a11y")] + /// Sets the name of the [`Button`]. + pub fn name(mut self, name: impl Into>) -> Self { + self.name = Some(name.into()); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description_widget( + mut self, + description: &T, + ) -> Self { + self.description = Some(iced_accessibility::Description::Id( + description.description(), + )); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description(mut self, description: impl Into>) -> Self { + self.description = + Some(iced_accessibility::Description::Text(description.into())); + self + } } impl<'a, Message, Theme, Renderer> Widget @@ -194,7 +248,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 @@ -213,6 +267,7 @@ where alignment::Horizontal::Left, alignment::Vertical::Top, self.text_shaping, + self.text_wrap, ) }, ) @@ -300,6 +355,7 @@ where size, line_height, shaping, + wrap, } = &self.icon; let size = size.unwrap_or(Pixels(bounds.height * 0.7)); @@ -314,6 +370,7 @@ where horizontal_alignment: alignment::Horizontal::Center, vertical_alignment: alignment::Vertical::Center, shaping: *shaping, + wrap: *wrap, }, bounds.center(), custom_style.icon_color, @@ -337,6 +394,92 @@ 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, Checked, 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(if self.is_checked { + Checked::True + } else { + Checked::False + }); + if is_hovered { + node.set_hovered(); + } + node.add_action(Action::Default); + let mut label_node = NodeBuilder::new(Role::StaticText); + + label_node.set_name(self.label.clone()); + // TODO proper label bounds + label_node.set_bounds(bounds); + + A11yTree::node_with_child_tree( + A11yNode::new(node, self.id.clone()), + A11yTree::leaf(label_node, self.label_id.clone()), + ) + } + fn id(&self) -> Option { + Some(Id(Internal::Set(vec![ + self.id.0.clone(), + self.label_id.0.clone(), + ]))) + } + + fn set_id(&mut self, id: Id) { + if let Id(Internal::Set(list)) = id { + if list.len() == 2 { + self.id.0 = list[0].clone(); + self.label_id.0 = list[1].clone(); + } + } + } } impl<'a, Message, Theme, Renderer> From> @@ -366,4 +509,6 @@ pub struct Icon { pub line_height: text::LineHeight, /// The shaping strategy of the icon. pub shaping: text::Shaping, + /// The wrap mode of the icon. + pub wrap: text::Wrap, } diff --git a/widget/src/column.rs b/widget/src/column.rs index faac0e48d8..0980439648 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -1,4 +1,6 @@ //! Distribute content vertically. +use iced_renderer::core::widget::OperationOutputWrapper; + use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; @@ -126,8 +128,8 @@ where self.children.iter().map(Tree::new).collect() } - fn diff(&self, tree: &mut Tree) { - tree.diff_children(&self.children); + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(self.children.as_mut_slice()); } fn size(&self) -> Size { @@ -164,7 +166,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.children @@ -262,6 +264,48 @@ 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) + }), + ) + } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + renderer: &Renderer, + dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles, + ) { + for ((e, layout), state) in self + .children + .iter() + .zip(layout.children()) + .zip(state.children.iter()) + { + e.as_widget().drag_destinations( + state, + layout, + renderer, + dnd_rectangles, + ); + } + } } impl<'a, Message, Theme, Renderer> From> diff --git a/widget/src/container.rs b/widget/src/container.rs index 78ec19788d..abd245fcae 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. @@ -28,7 +29,6 @@ pub struct Container< Theme: StyleSheet, Renderer: crate::core::Renderer, { - id: Option, padding: Padding, width: Length, height: Length, @@ -54,7 +54,6 @@ where let size = content.as_widget().size_hint(); Container { - id: None, padding: Padding::ZERO, width: size.width.fluid(), height: size.height.fluid(), @@ -67,12 +66,6 @@ where } } - /// Sets the [`Id`] of the [`Container`]. - pub fn id(mut self, id: Id) -> Self { - self.id = Some(id); - self - } - /// Sets the [`Padding`] of the [`Container`]. pub fn padding>(mut self, padding: P) -> Self { self.padding = padding.into(); @@ -152,8 +145,8 @@ where self.content.as_widget().children() } - fn diff(&self, tree: &mut Tree) { - self.content.as_widget().diff(tree); + fn diff(&mut self, tree: &mut Tree) { + self.content.as_widget_mut().diff(tree); } fn size(&self) -> Size { @@ -187,10 +180,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.content.as_widget().id().as_ref(), layout.bounds(), &mut |operation| { self.content.as_widget().operate( @@ -263,9 +256,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, @@ -286,6 +283,49 @@ where renderer, ) } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + cursor: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + let c_layout = layout.children().next().unwrap(); + let c_state = state.children.get(0); + + self.content.as_widget().a11y_nodes( + c_layout, + c_state.unwrap_or(&Tree::empty()), + cursor, + ) + } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + renderer: &Renderer, + dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles, + ) { + if let Some(l) = layout.children().next() { + self.content.as_widget().drag_destinations( + state, + l, + renderer, + dnd_rectangles, + ); + } + } + + fn id(&self) -> Option { + self.content.as_widget().id().clone() + } + + fn set_id(&mut self, id: Id) { + self.content.as_widget_mut().set_id(id); + } } impl<'a, Message, Theme, Renderer> From> @@ -355,30 +395,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 +477,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..db33e9b055 --- /dev/null +++ b/widget/src/dnd_listener.rs @@ -0,0 +1,523 @@ +//! A container for capturing mouse events. + +use crate::core::event::wayland::DndOfferEvent; +use crate::core::event::{self, Event, PlatformSpecific}; +use crate::core::layout; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::widget::OperationOutputWrapper; +use crate::core::widget::{tree, Operation, Tree}; +use crate::core::{ + overlay, Clipboard, Element, Layout, Length, Point, Rectangle, Shell, + Widget, +}; +use sctk::reexports::client::protocol::wl_data_device_manager::DndAction; + +use std::u32; + +/// Emit messages on mouse events. +#[allow(missing_debug_implementations)] +pub struct DndListener<'a, Message, Theme, Renderer> { + content: Element<'a, Message, Theme, Renderer>, + + /// Sets the message to emit on a drag enter. + on_enter: + Option, (f32, f32)) -> Message + 'a>>, + + /// Sets the message to emit on a drag motion. + /// x and y are the coordinates of the pointer relative to the widget in the range (0.0, 1.0) + on_motion: Option Message + 'a>>, + + /// Sets the message to emit on a drag exit. + on_exit: Option, + + /// Sets the message to emit on a drag drop. + on_drop: Option, + + /// Sets the message to emit on a drag mime type event. + on_mime_type: Option Message + 'a>>, + + /// Sets the message to emit on a drag action event. + on_source_actions: Option Message + 'a>>, + + /// Sets the message to emit on a drag action event. + on_selected_action: Option Message + 'a>>, + + /// Sets the message to emit on a Data event. + on_data: Option) -> Message + 'a>>, +} + +impl<'a, Message, Theme, Renderer> DndListener<'a, Message, Theme, Renderer> { + /// The message to emit on a drag enter. + #[must_use] + pub fn on_enter( + mut self, + message: impl Fn(DndAction, Vec, (f32, f32)) -> Message + 'a, + ) -> Self { + self.on_enter = Some(Box::new(message)); + self + } + + /// The message to emit on a drag motion. + #[must_use] + pub fn on_motion( + mut self, + message: impl Fn(f32, f32) -> Message + 'a, + ) -> Self { + self.on_motion = Some(Box::new(message)); + self + } + + /// The message to emit on a selected drag action. + #[must_use] + pub fn on_selected_action( + mut self, + message: impl Fn(DndAction) -> Message + 'a, + ) -> Self { + self.on_selected_action = Some(Box::new(message)); + self + } + + /// The message to emit on a drag exit. + #[must_use] + pub fn on_exit(mut self, message: Message) -> Self { + self.on_exit = Some(message); + self + } + + /// The message to emit on a drag drop. + #[must_use] + pub fn on_drop(mut self, message: Message) -> Self { + self.on_drop = Some(message); + self + } + + /// The message to emit on a drag mime type event. + #[must_use] + pub fn on_mime_type( + mut self, + message: impl Fn(String) -> Message + 'a, + ) -> Self { + self.on_mime_type = Some(Box::new(message)); + self + } + + /// The message to emit on a drag action event. + #[must_use] + pub fn on_action( + mut self, + message: impl Fn(DndAction) -> Message + 'a, + ) -> Self { + self.on_source_actions = Some(Box::new(message)); + self + } + + /// The message to emit on a drag read data event. + #[must_use] + pub fn on_data( + mut self, + message: impl Fn(String, Vec) -> Message + 'a, + ) -> Self { + self.on_data = Some(Box::new(message)); + self + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +enum DndState { + #[default] + None, + External(DndAction, Vec), + Hovered(DndAction, Vec), + Dropped, +} + +/// Local state of the [`DndListener`]. +#[derive(Default)] +struct State { + dnd: DndState, +} + +impl<'a, Message, Theme, Renderer> DndListener<'a, Message, Theme, Renderer> { + /// Creates an empty [`DndListener`]. + pub fn new( + content: impl Into>, + ) -> Self { + DndListener { + content: content.into(), + on_enter: None, + on_motion: None, + on_exit: None, + on_drop: None, + on_mime_type: None, + on_source_actions: None, + on_selected_action: None, + on_data: None, + } + } +} + +impl<'a, Message, Theme, Renderer> Widget + for DndListener<'a, Message, Theme, Renderer> +where + Renderer: crate::core::Renderer, + Message: Clone, +{ + fn children(&self) -> Vec { + vec![Tree::new(&self.content)] + } + + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(std::slice::from_mut(&mut self.content)); + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let size = self.size(); + + layout( + renderer, + limits, + size.width, + size.height, + u32::MAX, + u32::MAX, + |renderer, limits| { + self.content.as_widget().layout( + &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: &Theme, + renderer_style: &renderer::Style, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + viewport: &Rectangle, + ) { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + renderer_style, + layout.children().next().unwrap(), + cursor_position, + viewport, + ); + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option> { + self.content.as_widget_mut().overlay( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + ) + } + + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::default()) + } + + fn size(&self) -> iced_renderer::core::Size { + self.content.as_widget().size() + } +} + +impl<'a, Message, Theme, Renderer> + From> + for Element<'a, Message, Theme, Renderer> +where + Message: 'a + Clone, + Renderer: 'a + crate::core::Renderer, + Theme: 'a, +{ + fn from( + listener: DndListener<'a, Message, Theme, Renderer>, + ) -> Element<'a, Message, Theme, Renderer> { + Element::new(listener) + } +} + +/// Processes the given [`Event`] and updates the [`State`] of an [`DndListener`] +/// accordingly. +fn update( + widget: &mut DndListener<'_, Message, Theme, Renderer>, + event: &Event, + layout: Layout<'_>, + shell: &mut Shell<'_, Message>, + state: &mut State, +) -> event::Status { + match event { + Event::PlatformSpecific(PlatformSpecific::Wayland( + event::wayland::Event::DndOffer(DndOfferEvent::Enter { + x, + y, + mime_types, + }), + )) => { + let bounds = layout.bounds(); + let p = Point { + x: *x as f32, + y: *y as f32, + }; + if layout.bounds().contains(p) { + state.dnd = + DndState::Hovered(DndAction::empty(), mime_types.clone()); + if let Some(message) = widget.on_enter.as_ref() { + let normalized_x: f32 = (p.x - bounds.x) / bounds.width; + let normalized_y: f32 = (p.y - bounds.y) / bounds.height; + shell.publish(message( + DndAction::empty(), + mime_types.clone(), + (normalized_x, normalized_y), + )); + return event::Status::Captured; + } + } else { + state.dnd = + DndState::External(DndAction::empty(), mime_types.clone()); + } + } + Event::PlatformSpecific(PlatformSpecific::Wayland( + event::wayland::Event::DndOffer(DndOfferEvent::Motion { x, y }), + )) => { + let bounds = layout.bounds(); + let p = Point { + x: *x as f32, + y: *y as f32, + }; + // motion can trigger an enter, motion or leave event on the widget + if let DndState::Hovered(action, mime_types) = &state.dnd { + if !bounds.contains(p) { + state.dnd = DndState::External(*action, mime_types.clone()); + if let Some(message) = widget.on_exit.clone() { + shell.publish(message); + return event::Status::Captured; + } + } else if let Some(message) = widget.on_motion.as_ref() { + let normalized_x: f32 = (p.x - bounds.x) / bounds.width; + let normalized_y: f32 = (p.y - bounds.y) / bounds.height; + shell.publish(message(normalized_x, normalized_y)); + return event::Status::Captured; + } + } else if bounds.contains(p) { + state.dnd = match &state.dnd { + DndState::External(a, m) => { + DndState::Hovered(*a, m.clone()) + } + _ => DndState::Hovered(DndAction::empty(), vec![]), + }; + let (action, mime_types) = match &state.dnd { + DndState::Hovered(action, mime_types) => { + (action, mime_types) + } + _ => return event::Status::Ignored, + }; + + if let Some(message) = widget.on_enter.as_ref() { + let normalized_x: f32 = (p.x - bounds.x) / bounds.width; + let normalized_y: f32 = (p.y - bounds.y) / bounds.height; + shell.publish(message( + *action, + mime_types.clone(), + (normalized_x, normalized_y), + )); + return event::Status::Captured; + } + } + } + Event::PlatformSpecific(PlatformSpecific::Wayland( + event::wayland::Event::DndOffer(DndOfferEvent::Leave), + )) => { + if matches!(state.dnd, DndState::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(width, height, content.size()); + + layout::Node::with_children(size, vec![content]) +} diff --git a/widget/src/dnd_source.rs b/widget/src/dnd_source.rs new file mode 100644 index 0000000000..23a949bcec --- /dev/null +++ b/widget/src/dnd_source.rs @@ -0,0 +1,436 @@ +//! 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, Vector, Widget, +}; + +use crate::core::widget::{ + operation::OperationOutputWrapper, tree, Operation, Tree, +}; + +/// A widget that can be dragged and dropped. +#[allow(missing_debug_implementations)] +pub struct DndSource<'a, Message, Theme, Renderer> { + content: Element<'a, Message, Theme, Renderer>, + + on_drag: Option Message + 'a>>, + + on_cancelled: Option, + + on_finished: Option, + + on_dropped: Option, + + on_selection_action: Option Message + 'a>>, + + drag_threshold: f32, + + /// Whether or not captured events should be handled by the widget. + handle_captured_events: bool, +} + +impl<'a, Message, Widget, Renderer> DndSource<'a, Message, Widget, Renderer> { + /// The message to produce when the drag starts. + /// + /// Receives the size of the source widget, so the caller is able to size the + /// drag surface to match. + #[must_use] + pub fn on_drag(mut self, f: F) -> Self + where + F: Fn(Size, Vector) -> Message + 'a, + { + self.on_drag = Some(Box::new(f)); + self + } + + /// The message to produce when the drag is cancelled. + #[must_use] + pub fn on_cancelled(mut self, message: Message) -> Self { + self.on_cancelled = Some(message); + self + } + + /// The message to produce when the drag is finished. + #[must_use] + pub fn on_finished(mut self, message: Message) -> Self { + self.on_finished = Some(message); + self + } + + /// The message to produce when the drag is dropped. + #[must_use] + pub fn on_dropped(mut self, message: Message) -> Self { + self.on_dropped = Some(message); + self + } + + /// The message to produce when the selection action is triggered. + #[must_use] + pub fn on_selection_action(mut self, f: F) -> Self + where + F: Fn(DndAction) -> Message + 'a, + { + self.on_selection_action = Some(Box::new(f)); + self + } + + /// The drag radius threshold. + /// if the mouse is moved more than this radius while pressed, the drag event is triggered + #[must_use] + pub fn drag_threshold(mut self, radius: f32) -> Self { + self.drag_threshold = radius.powi(2); + self + } + + /// Whether or not captured events should be handled by the widget. + #[must_use] + pub fn handle_captured_events( + mut self, + handle_captured_events: bool, + ) -> Self { + self.handle_captured_events = handle_captured_events; + self + } +} + +/// Local state of the [`MouseListener`]. +#[derive(Default)] +struct State { + hovered: bool, + left_pressed_position: Option, + is_dragging: bool, +} + +impl<'a, Message, Widget, Renderer> DndSource<'a, Message, Widget, Renderer> { + /// Creates a new [`DndSource`]. + #[must_use] + pub fn new( + content: impl Into>, + ) -> Self { + Self { + content: content.into(), + on_drag: None, + on_cancelled: None, + on_finished: None, + on_dropped: None, + on_selection_action: None, + drag_threshold: 25.0, + handle_captured_events: true, + } + } +} + +impl<'a, Message, Theme, Renderer> From> + for Element<'a, Message, Theme, Renderer> +where + Renderer: crate::core::Renderer + 'a, + Message: Clone + 'a, + Theme: 'a, +{ + fn from( + dnd_source: DndSource<'a, Message, Theme, Renderer>, + ) -> Element<'a, Message, Theme, Renderer> { + Element::new(dnd_source) + } +} + +impl<'a, Message, Theme, Renderer> Widget + for DndSource<'a, Message, Theme, Renderer> +where + Renderer: crate::core::Renderer, + Message: Clone, +{ + fn children(&self) -> Vec { + vec![Tree::new(&self.content)] + } + + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(std::slice::from_mut(&mut self.content)); + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let size = self.size(); + layout( + renderer, + limits, + size.width, + size.height, + u32::MAX, + u32::MAX, + |renderer, limits| { + self.content.as_widget().layout( + &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()) + { + let mut offset = cursor_position; + let offset = Vector::new( + cursor_position.x - layout.bounds().x, + cursor_position.y - layout.bounds().y, + ); + shell.publish(on_drag(layout.bounds().size(), offset)); + state.is_dragging = true; + return event::Status::Captured; + }; + 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; + let offset = Vector::new( + cursor_position.x - layout.bounds().x, + cursor_position.y - layout.bounds().y, + ); + shell.publish(on_drag(layout.bounds().size(), offset)); + return event::Status::Captured; + } + } + + if self.on_drag.is_some() { + if let Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerPressed { .. }) = event + { + state.left_pressed_position = Some(cursor_position); + return event::Status::Captured; + } + } + + captured + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: layout::Layout<'_>, + cursor_position: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout.children().next().unwrap(), + cursor_position, + viewport, + renderer, + ) + } + + fn size(&self) -> Size { + self.content.as_widget().size() + } +} + +/// Computes the layout of a [`DndSource`]. +pub fn layout( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + height: Length, + max_height: u32, + max_width: u32, + layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, +) -> layout::Node { + let limits = limits + .loose() + .max_height(max_height as f32) + .max_width(max_width as f32) + .width(width) + .height(height); + + let content = layout_content(renderer, &limits); + let size = limits.resolve(width, height, content.size()); + + layout::Node::with_children(size, vec![content]) +} diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 444eb4c2ea..75b0d03a05 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -22,6 +22,11 @@ use crate::toggler::{self, Toggler}; use crate::tooltip::{self, Tooltip}; use crate::{Column, MouseArea, Row, Space, Themer, VerticalSlider}; +#[cfg(feature = "wayland")] +use crate::dnd_listener::DndListener; +#[cfg(feature = "wayland")] +use crate::dnd_source::DndSource; + use std::borrow::Cow; use std::ops::RangeInclusive; @@ -197,7 +202,7 @@ where Renderer: core::text::Renderer, Theme: toggler::StyleSheet, { - Toggler::new(label, is_checked, f) + Toggler::new(label, is_checked, Some(f)) } /// Creates a new [`TextInput`]. @@ -355,7 +360,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()) } @@ -364,7 +372,9 @@ pub fn image(handle: impl Into) -> crate::Image { /// [`Svg`]: crate::Svg /// [`Handle`]: crate::svg::Handle #[cfg(feature = "svg")] -pub fn svg(handle: impl Into) -> crate::Svg +pub fn svg<'a, Theme>( + handle: impl Into, +) -> crate::Svg<'a, Theme> where Theme: crate::svg::StyleSheet, { @@ -432,3 +442,25 @@ where { Themer::new(theme, content) } + +#[cfg(feature = "wayland")] +/// A container for a dnd source +pub fn dnd_source<'a, Message, Theme, Renderer>( + widget: impl Into>, +) -> DndSource<'a, Message, Theme, Renderer> +where + Renderer: core::Renderer, +{ + DndSource::new(widget) +} + +#[cfg(feature = "wayland")] +/// A container for a dnd target +pub fn dnd_listener<'a, Message, Theme, Renderer>( + widget: impl Into>, +) -> DndListener<'a, Message, Theme, Renderer> +where + Renderer: core::Renderer, +{ + DndListener::new(widget) +} diff --git a/widget/src/image.rs b/widget/src/image.rs index ccf1f1754a..673169f0a1 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,8 +229,8 @@ pub fn draw( } } -impl Widget - for Image +impl<'a, Message, Theme, Renderer, Handle> Widget + for Image<'a, Handle> where Renderer: image::Renderer, Handle: Clone + Hash, @@ -185,6 +255,7 @@ where self.width, self.height, self.content_fit, + self.border_radius, ) } @@ -204,17 +275,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, Theme, Renderer, Handle> From> +impl<'a, Message, Theme, Renderer, Handle> From> for Element<'a, Message, Theme, Renderer> where Renderer: image::Renderer, Handle: Clone + Hash + 'a, { - fn from(image: Image) -> Element<'a, Message, Theme, Renderer> { + fn from(image: Image<'a, Handle>) -> Element<'a, Message, Theme, Renderer> { Element::new(image) } } diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 9666ff9f0e..fb4dc73f28 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -338,6 +338,7 @@ where y: bounds.y, ..Rectangle::with_size(image_size) }, + [0.0; 4], ); }); }); diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs index 6203d2c587..2ac60ab84e 100644 --- a/widget/src/keyed/column.rs +++ b/widget/src/keyed/column.rs @@ -1,4 +1,6 @@ //! Distribute content vertically. +use iced_renderer::core::widget::OperationOutputWrapper; + use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; @@ -163,7 +165,7 @@ where self.children.iter().map(Tree::new).collect() } - fn diff(&self, tree: &mut Tree) { + fn diff(&mut self, tree: &mut Tree) { let Tree { state, children, .. } = tree; @@ -172,8 +174,8 @@ where tree::diff_children_custom_with_search( children, - &self.children, - |tree, child| child.as_widget().diff(tree), + &mut self.children, + |tree, child| child.as_widget_mut().diff(tree), |index| { self.keys.get(index).or_else(|| self.keys.last()).copied() != Some(state.keys[index]) @@ -182,7 +184,7 @@ where ); if state.keys != self.keys { - state.keys = self.keys.clone(); + state.keys.clone_from(&self.keys); } } @@ -223,7 +225,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.children diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index dda6162b27..a9b3316185 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -5,6 +5,7 @@ pub mod component; pub mod responsive; pub use component::Component; +use iced_renderer::core::widget::{Operation, OperationOutputWrapper}; pub use responsive::Responsive; mod cache; @@ -15,7 +16,7 @@ use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; -use crate::core::widget::{self, Widget}; +use crate::core::widget::Widget; use crate::core::Element; use crate::core::{ self, Clipboard, Hasher, Length, Point, Rectangle, Shell, Size, Vector, @@ -122,7 +123,7 @@ where self.with_element(|element| vec![Tree::new(element.as_widget())]) } - fn diff(&self, tree: &mut Tree) { + fn diff(&mut self, tree: &mut Tree) { let current = tree .state .downcast_mut::>(); @@ -138,8 +139,10 @@ where current.element = Rc::new(RefCell::new(Some(element))); (*self.element.borrow_mut()) = Some(current.element.clone()); - self.with_element(|element| { - tree.diff_children(std::slice::from_ref(&element.as_widget())); + self.with_element_mut(|element| { + tree.diff_children(std::slice::from_mut( + &mut element.as_widget_mut(), + )) }); } else { (*self.element.borrow_mut()) = Some(current.element.clone()); @@ -175,7 +178,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { self.with_element(|element| { element.as_widget().operate( @@ -288,6 +291,40 @@ 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 + } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + renderer: &Renderer, + dnd_rectangles: &mut core::clipboard::DndDestinationRectangles, + ) { + self.with_element(|element| { + element.as_widget().drag_destinations( + &state.children[0], + layout, + renderer, + dnd_rectangles, + ); + }); + } } #[self_referencing] diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index 30b1efedd3..75cf7b717c 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -12,6 +12,7 @@ use crate::core::{ }; use crate::runtime::overlay::Nested; +use iced_renderer::core::widget::{Operation, OperationOutputWrapper}; use ouroboros::self_referencing; use std::cell::RefCell; use std::marker::PhantomData; @@ -59,7 +60,7 @@ pub trait Component { fn operate( &self, _state: &mut Self::State, - _operation: &mut dyn widget::Operation, + _operation: &mut dyn Operation>, ) { } } @@ -117,13 +118,13 @@ where Renderer: renderer::Renderer, { fn diff_self(&self) { - self.with_element(|element| { + self.with_element_mut(|element| { self.tree .borrow_mut() .borrow_mut() .as_mut() .unwrap() - .diff_children(std::slice::from_ref(&element)); + .diff_children(std::slice::from_mut(element)); }); } @@ -161,7 +162,7 @@ where fn rebuild_element_with_operation( &self, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { let heads = self.state.borrow_mut().take().unwrap().into_heads(); @@ -232,6 +233,7 @@ where fn state(&self) -> tree::State { let state = Rc::new(RefCell::new(Some(Tree { + id: None, tag: tree::Tag::of::>(), state: tree::State::new(S::default()), children: vec![Tree::empty()], @@ -244,7 +246,7 @@ where vec![] } - fn diff(&self, tree: &mut Tree) { + fn diff(&mut self, tree: &mut Tree) { let tree = tree.state.downcast_ref::>>>(); *self.tree.borrow_mut() = tree.clone(); self.rebuild_element_if_necessary(); @@ -345,7 +347,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { self.rebuild_element_with_operation(operation); @@ -510,6 +512,51 @@ 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() + } + }) + } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + renderer: &Renderer, + dnd_rectangles: &mut core::clipboard::DndDestinationRectangles, + ) { + self.with_element(|element| { + element.as_widget().drag_destinations( + &state.children[0], + layout, + renderer, + dnd_rectangles, + ) + }); + } } struct Overlay<'a, 'b, Message, Theme, Renderer, Event, S>( diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index 9875b24ef0..b13a53f85b 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -3,7 +3,6 @@ use crate::core::layout::{self, Layout}; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; -use crate::core::widget; use crate::core::widget::tree::{self, Tree}; use crate::core::{ self, Clipboard, Element, Length, Point, Rectangle, Shell, Size, Vector, @@ -12,6 +11,7 @@ use crate::core::{ use crate::horizontal_space; use crate::runtime::overlay::Nested; +use iced_renderer::core::widget::{Operation, OperationOutputWrapper}; use ouroboros::self_referencing; use std::cell::{RefCell, RefMut}; use std::marker::PhantomData; @@ -90,7 +90,7 @@ where self.size = new_size; self.layout = None; - tree.diff(&self.element); + tree.diff(&mut self.element); } fn resolve( @@ -161,7 +161,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { let state = tree.state.downcast_mut::(); let mut content = self.content.borrow_mut(); @@ -321,6 +321,63 @@ 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); + } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + renderer: &Renderer, + dnd_rectangles: &mut core::clipboard::DndDestinationRectangles, + ) { + let ret = self.content.borrow_mut().resolve( + &mut state.state.downcast_ref::().tree.borrow_mut(), + renderer, + layout, + &self.view, + |tree, r, layout, element| { + element.as_widget().drag_destinations( + tree, + layout, + r, + dnd_rectangles, + ); + }, + ); + ret + } } impl<'a, Message, Theme, Renderer> diff --git a/widget/src/lib.rs b/widget/src/lib.rs index cefafdbebe..4901b103d6 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -137,3 +137,7 @@ pub use qr_code::QRCode; pub use renderer::Renderer; pub use style::theme::{self, Theme}; +#[cfg(feature = "wayland")] +pub mod dnd_listener; +#[cfg(feature = "wayland")] +pub mod dnd_source; diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index 62bb45d827..8ba079a53e 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -1,5 +1,9 @@ //! A container for capturing mouse events. +use iced_renderer::core::mouse::Click; +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; @@ -20,21 +24,38 @@ pub struct MouseArea< Renderer = crate::Renderer, > { content: Element<'a, Message, Theme, Renderer>, + on_drag: Option, on_press: Option, + on_double_press: Option, on_release: Option, on_right_press: Option, on_right_release: Option, on_middle_press: Option, on_middle_release: Option, + on_mouse_enter: Option, + on_mouse_exit: Option, } impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { + /// 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 { self.on_press = Some(message); self } + /// The message to emit on a left double button press. + #[must_use] + pub fn on_double_press(mut self, message: Message) -> Self { + self.on_double_press = Some(message); + self + } /// The message to emit on a left button release. #[must_use] @@ -70,12 +91,35 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { self.on_middle_release = Some(message); self } + #[must_use] + /// The message to emit on mouse enter. + pub fn on_mouse_enter(mut self, message: Message) -> Self { + self.on_mouse_enter = Some(message); + self + } + #[must_use] + /// The message to emit on mouse exit. + pub fn on_mouse_exit(mut self, message: Message) -> Self { + self.on_mouse_exit = Some(message); + self + } } /// Local state of the [`MouseArea`]. -#[derive(Default)] struct State { // TODO: Support on_mouse_enter and on_mouse_exit + drag_initiated: Option, + is_out_of_bounds: bool, + last_click: Option, +} +impl Default for State { + fn default() -> Self { + Self { + drag_initiated: Default::default(), + is_out_of_bounds: true, + last_click: Default::default(), + } + } } impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { @@ -85,12 +129,16 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { ) -> Self { MouseArea { content: content.into(), + on_drag: None, on_press: None, + on_double_press: None, on_release: None, on_right_press: None, on_right_release: None, on_middle_press: None, on_middle_release: None, + on_mouse_enter: None, + on_mouse_exit: None, } } } @@ -113,8 +161,8 @@ where vec![Tree::new(&self.content)] } - fn diff(&self, tree: &mut Tree) { - tree.diff_children(std::slice::from_ref(&self.content)); + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(std::slice::from_mut(&mut self.content)); } fn size(&self) -> Size { @@ -137,7 +185,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], @@ -171,7 +219,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( @@ -211,7 +266,6 @@ where viewport, ); } - fn overlay<'b>( &'b mut self, tree: &'b mut Tree, @@ -224,6 +278,22 @@ where renderer, ) } + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + renderer: &Renderer, + dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles, + ) { + if let Some(state) = state.children.iter().next() { + self.content.as_widget().drag_destinations( + state, + layout, + renderer, + dnd_rectangles, + ); + } + } } impl<'a, Message, Theme, Renderer> From> @@ -248,15 +318,50 @@ fn update( layout: Layout<'_>, cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, + state: &mut State, ) -> event::Status { if !cursor.is_over(layout.bounds()) { + if !state.is_out_of_bounds { + if widget + .on_mouse_enter + .as_ref() + .or(widget.on_mouse_exit.as_ref()) + .is_some() + { + if let Event::Mouse(mouse::Event::CursorMoved { .. }) = event { + state.is_out_of_bounds = true; + if let Some(message) = widget.on_mouse_exit.as_ref() { + shell.publish(message.clone()); + } + return event::Status::Captured; + } + } + } + return event::Status::Ignored; } + if let Some(message) = widget.on_double_press.as_ref() { + if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) = + event + { + if let Some(cursor_position) = cursor.position() { + let click = + mouse::Click::new(cursor_position, state.last_click); + state.last_click = Some(click); + if let mouse::click::Kind::Double = click.kind() { + shell.publish(message.clone()); + return event::Status::Captured; + } + } + } + } + if let Some(message) = widget.on_press.as_ref() { if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) = event { + state.drag_initiated = cursor.position(); shell.publish(message.clone()); return event::Status::Captured; @@ -267,6 +372,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; @@ -316,5 +422,40 @@ fn update( } } + if let Some(message) = widget + .on_mouse_enter + .as_ref() + .or(widget.on_mouse_exit.as_ref()) + { + if let Event::Mouse(mouse::Event::CursorMoved { .. }) = event { + if state.is_out_of_bounds { + state.is_out_of_bounds = false; + if widget.on_mouse_enter.is_some() { + shell.publish(message.clone()); + } + return event::Status::Captured; + } + } + } + + if state.drag_initiated.is_none() && widget.on_drag.is_some() { + if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | 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 a7b7ec9406..9b4ff35955 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -39,6 +39,7 @@ pub struct Menu< text_size: Option, text_line_height: text::LineHeight, text_shaping: text::Shaping, + text_wrap: text::Wrap, font: Option, style: Theme::Style, } @@ -69,7 +70,8 @@ where padding: Padding::ZERO, text_size: None, text_line_height: text::LineHeight::default(), - text_shaping: text::Shaping::Basic, + text_shaping: text::Shaping::Advanced, + text_wrap: text::Wrap::default(), font: None, style: Default::default(), } @@ -108,6 +110,12 @@ where self } + /// Sets the [`text::Wrap`] mode of the [`Menu`]. + pub fn text_wrap(mut self, wrap: text::Wrap) -> Self { + self.text_wrap = wrap; + self + } + /// Sets the font of the [`Menu`]. pub fn font(mut self, font: impl Into) -> Self { self.font = Some(font.into()); @@ -199,10 +207,11 @@ where text_size, text_line_height, text_shaping, + text_wrap, style, } = menu; - let container = Container::new(Scrollable::new(List { + let mut container = Container::new(Scrollable::new(List { options, hovered_option, on_selected, @@ -211,11 +220,12 @@ where text_size, text_line_height, text_shaping, + text_wrap, padding, style: style.clone(), })); - state.tree.diff(&container as &dyn Widget<_, _, _>); + state.tree.diff(&mut container as &mut dyn Widget<_, _, _>); Self { state: &mut state.tree, @@ -333,6 +343,7 @@ where text_size: Option, text_line_height: text::LineHeight, text_shaping: text::Shaping, + text_wrap: text::Wrap, font: Option, style: Theme::Style, } @@ -535,6 +546,7 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: self.text_shaping, + wrap: self.text_wrap, }, Point::new(bounds.x + self.padding.left, bounds.center_y()), if is_selected { diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index c20d959a98..226e76322b 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -24,6 +24,7 @@ pub use configuration::Configuration; pub use content::Content; pub use direction::Direction; pub use draggable::Draggable; +use iced_renderer::core::widget::{Operation, OperationOutputWrapper}; pub use node::Node; pub use pane::Pane; pub use split::Split; @@ -39,7 +40,6 @@ use crate::core::mouse; use crate::core::overlay::{self, Group}; use crate::core::renderer; use crate::core::touch; -use crate::core::widget; use crate::core::widget::tree::{self, Tree}; use crate::core::{ Clipboard, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size, @@ -111,6 +111,7 @@ pub struct PaneGrid< spacing: f32, on_click: Option Message + 'a>>, on_drag: Option Message + 'a>>, + #[allow(clippy::type_complexity)] on_resize: Option<(f32, Box Message + 'a>)>, style: ::Style, } @@ -254,15 +255,20 @@ where .collect() } - fn diff(&self, tree: &mut Tree) { - match &self.contents { - Contents::All(contents, _) => tree.diff_children_custom( - contents, - |state, (_, content)| content.diff(state), - |(_, content)| content.state(), - ), + fn diff(&mut self, tree: &mut Tree) { + match &mut self.contents { + Contents::All(contents, _) => { + let ids = contents.iter().map(|_| None).collect(); // TODO + tree.diff_children_custom( + contents, + ids, + |state, (_, content)| content.diff(state), + |(_, content)| content.state(), + ) + } Contents::Maximized(_, content, _) => tree.diff_children_custom( - &[content], + &mut [content], + vec![None], // TODO |state, content| content.diff(state), |content| content.state(), ), @@ -302,7 +308,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 @@ -460,6 +466,28 @@ where (!children.is_empty()).then(|| Group::with_children(children).overlay()) } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + renderer: &Renderer, + dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles, + ) { + for (((_pane, content), child_layout), child_tree) in self + .contents + .iter() + .zip(layout.children()) + .zip(state.children.iter()) + { + content.drag_destinations( + child_tree, + child_layout, + renderer, + dnd_rectangles, + ); + } + } } impl<'a, Message, Theme, Renderer> From> @@ -529,7 +557,10 @@ pub fn update<'a, Message, T: Draggable>( contents: impl Iterator, on_click: &Option Message + 'a>>, on_drag: &Option Message + 'a>>, - on_resize: &Option<(f32, Box Message + 'a>)>, + #[allow(clippy::type_complexity)] on_resize: &Option<( + f32, + Box Message + 'a>, + )>, ) -> event::Status { const DRAG_DEADBAND_DISTANCE: f32 = 10.0; diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs index 415dcc3ef0..34cfb5abf2 100644 --- a/widget/src/pane_grid/content.rs +++ b/widget/src/pane_grid/content.rs @@ -1,10 +1,12 @@ +use iced_renderer::core::widget::{Operation, OperationOutputWrapper}; + use crate::container; use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; -use crate::core::widget::{self, Tree}; +use crate::core::widget::Tree; use crate::core::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size}; use crate::pane_grid::{Draggable, TitleBar}; @@ -74,13 +76,13 @@ where } } - pub(super) fn diff(&self, tree: &mut Tree) { + pub(super) fn diff(&mut self, tree: &mut Tree) { if tree.children.len() == 2 { - if let Some(title_bar) = self.title_bar.as_ref() { + if let Some(title_bar) = self.title_bar.as_mut() { title_bar.diff(&mut tree.children[1]); } - tree.children[0].diff(&self.body); + tree.children[0].diff(&mut self.body); } else { *tree = self.state(); } @@ -197,7 +199,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { let body_layout = if let Some(title_bar) = &self.title_bar { let mut children = layout.children(); @@ -356,6 +358,25 @@ where ) } } + + pub(crate) fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + renderer: &Renderer, + dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles, + ) { + if let Some((child_tree, child_layout)) = + state.children.iter().zip(layout.children()).next() + { + self.body.as_widget().drag_destinations( + child_tree, + child_layout, + renderer, + dnd_rectangles, + ); + } + } } impl<'a, Message, Theme, Renderer> Draggable diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs index 3cca6b33da..7bfb000600 100644 --- a/widget/src/pane_grid/title_bar.rs +++ b/widget/src/pane_grid/title_bar.rs @@ -1,10 +1,12 @@ +use iced_renderer::core::widget::{Operation, OperationOutputWrapper}; + use crate::container; use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; -use crate::core::widget::{self, Tree}; +use crate::core::widget::Tree; use crate::core::{ Clipboard, Element, Layout, Padding, Point, Rectangle, Shell, Size, }; @@ -101,13 +103,13 @@ where } } - pub(super) fn diff(&self, tree: &mut Tree) { + pub(super) fn diff(&mut self, tree: &mut Tree) { if tree.children.len() == 2 { - if let Some(controls) = self.controls.as_ref() { + if let Some(controls) = self.controls.as_mut() { tree.children[1].diff(controls); } - tree.children[0].diff(&self.content); + tree.children[0].diff(&mut self.content); } else { *tree = self.state(); } @@ -130,7 +132,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); @@ -262,7 +266,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation>, ) { let mut children = layout.children(); let padded = children.next().unwrap(); diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index 70b1d4c07a..9f119959ad 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}; @@ -43,6 +45,7 @@ pub struct PickList< text_size: Option, text_line_height: text::LineHeight, text_shaping: text::Shaping, + text_wrap: text::Wrap, font: Option, handle: Handle, style: Theme::Style, @@ -79,7 +82,8 @@ where padding: Self::DEFAULT_PADDING, text_size: None, text_line_height: text::LineHeight::default(), - text_shaping: text::Shaping::Basic, + text_shaping: text::Shaping::Advanced, + text_wrap: text::Wrap::default(), font: None, handle: Handle::default(), style: Default::default(), @@ -125,6 +129,12 @@ where self } + /// Sets the [`text::Wrap`] mode of the [`PickList`]. + pub fn text_wrap(mut self, wrap: text::Wrap) -> Self { + self.text_wrap = wrap; + self + } + /// Sets the font of the [`PickList`]. pub fn font(mut self, font: impl Into) -> Self { self.font = Some(font.into()); @@ -190,6 +200,7 @@ where self.text_size, self.text_line_height, self.text_shaping, + self.text_wrap, self.font, self.placeholder.as_deref(), &self.options, @@ -250,6 +261,7 @@ where self.text_size, self.text_line_height, self.text_shaping, + self.text_wrap, font, self.placeholder.as_deref(), self.selected.as_ref(), @@ -274,6 +286,7 @@ where self.padding, self.text_size, self.text_shaping, + self.text_wrap, self.font.unwrap_or_else(|| renderer.default_font()), &self.options, &self.on_selected, @@ -375,6 +388,8 @@ pub struct Icon { pub line_height: text::LineHeight, /// The shaping strategy of the icon. pub shaping: text::Shaping, + /// The wrap mode of the icon. + pub wrap: text::Wrap, } /// Computes the layout of a [`PickList`]. @@ -387,6 +402,7 @@ pub fn layout( text_size: Option, text_line_height: text::LineHeight, text_shaping: text::Shaping, + text_wrap: text::Wrap, font: Option, placeholder: Option<&str>, options: &[T], @@ -414,6 +430,7 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: text_shaping, + wrap: text_wrap, }; for (option, paragraph) in options.iter().zip(state.options.iter_mut()) { @@ -577,6 +594,7 @@ pub fn overlay<'a, T, Message, Theme, Renderer>( padding: Padding, text_size: Option, text_shaping: text::Shaping, + text_wrap: text::Wrap, font: Renderer::Font, options: &'a [T], on_selected: &'a dyn Fn(T) -> Message, @@ -611,6 +629,7 @@ where .padding(padding) .font(font) .text_shaping(text_shaping) + .text_wrap(text_wrap) .style(style); if let Some(text_size) = text_size { @@ -633,6 +652,7 @@ pub fn draw<'a, T, Theme, Renderer>( text_size: Option, text_line_height: text::LineHeight, text_shaping: text::Shaping, + text_wrap: text::Wrap, font: Renderer::Font, placeholder: Option<&str>, selected: Option<&T>, @@ -669,8 +689,9 @@ pub fn draw<'a, T, Theme, Renderer>( Renderer::ICON_FONT, Renderer::ARROW_DOWN_ICON, *size, - text::LineHeight::default(), - text::Shaping::Basic, + LineHeight::default(), + text::Shaping::Advanced, + text::Wrap::default(), )), Handle::Static(Icon { font, @@ -678,7 +699,8 @@ pub fn draw<'a, T, Theme, Renderer>( size, line_height, shaping, - }) => Some((*font, *code_point, *size, *line_height, *shaping)), + wrap, + }) => Some((*font, *code_point, *size, *line_height, *shaping, *wrap)), Handle::Dynamic { open, closed } => { if state().is_open { Some(( @@ -687,6 +709,7 @@ pub fn draw<'a, T, Theme, Renderer>( open.size, open.line_height, open.shaping, + open.wrap, )) } else { Some(( @@ -695,13 +718,14 @@ pub fn draw<'a, T, Theme, Renderer>( closed.size, closed.line_height, closed.shaping, + closed.wrap, )) } } Handle::None => None, }; - if let Some((font, code_point, size, line_height, shaping)) = handle { + if let Some((font, code_point, size, line_height, shaping, wrap)) = handle { let size = size.unwrap_or_else(|| renderer.default_size()); renderer.fill_text( @@ -717,6 +741,7 @@ pub fn draw<'a, T, Theme, Renderer>( horizontal_alignment: alignment::Horizontal::Right, vertical_alignment: alignment::Vertical::Center, shaping, + wrap, }, Point::new( bounds.x + bounds.width - padding.horizontal(), @@ -745,6 +770,7 @@ pub fn draw<'a, T, Theme, Renderer>( horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: text_shaping, + wrap: text_wrap, }, Point::new(bounds.x + padding.left, bounds.center_y()), if is_selected { diff --git a/widget/src/radio.rs b/widget/src/radio.rs index 68e9bc7eff..862a03cd14 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -83,6 +83,7 @@ where text_size: Option, text_line_height: text::LineHeight, text_shaping: text::Shaping, + text_wrap: text::Wrap, font: Option, style: Theme::Style, } @@ -126,7 +127,8 @@ 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, + text_wrap: text::Wrap::default(), font: None, style: Default::default(), } @@ -171,6 +173,12 @@ where self } + /// Sets the [`text::Wrap`] mode of the [`Radio`] button. + pub fn text_wrap(mut self, wrap: text::Wrap) -> Self { + self.text_wrap = wrap; + self + } + /// Sets the text font of the [`Radio`] button. pub fn font(mut self, font: impl Into) -> Self { self.font = Some(font.into()); @@ -234,6 +242,7 @@ where alignment::Horizontal::Left, alignment::Vertical::Top, self.text_shaping, + self.text_wrap, ) }, ) diff --git a/widget/src/row.rs b/widget/src/row.rs index 89f610c990..e6884d8e2e 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -1,4 +1,6 @@ //! Distribute content horizontally. +use iced_renderer::core::widget::OperationOutputWrapper; + use crate::core::event::{self, Event}; use crate::core::layout::{self, Layout}; use crate::core::mouse; @@ -117,8 +119,8 @@ where self.children.iter().map(Tree::new).collect() } - fn diff(&self, tree: &mut Tree) { - tree.diff_children(&self.children); + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(&mut self.children) } fn size(&self) -> Size { @@ -153,7 +155,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.children @@ -251,6 +253,48 @@ 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) + }), + ) + } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + renderer: &Renderer, + dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles, + ) { + for ((e, layout), state) in self + .children + .iter() + .zip(layout.children()) + .zip(state.children.iter()) + { + e.as_widget().drag_destinations( + state, + layout, + renderer, + dnd_rectangles, + ); + } + } } impl<'a, Message, Theme, Renderer> From> diff --git a/widget/src/rule.rs b/widget/src/rule.rs index bca345413c..347ccc19bc 100644 --- a/widget/src/rule.rs +++ b/widget/src/rule.rs @@ -45,6 +45,24 @@ where } } + /// Set the width of the rule + /// Will not be applied if it is vertical + pub fn width(mut self, width: impl Into) -> Self { + if self.is_horizontal { + self.width = width.into(); + } + self + } + + /// Set the height of the rule + /// Will not be applied if it is horizontal + pub fn height(mut self, height: impl Into) -> Self { + if !self.is_horizontal { + self.height = height.into(); + } + self + } + /// Sets the style of the [`Rule`]. pub fn style(mut self, style: impl Into) -> Self { self.style = style.into(); diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 509a6b342e..72a25a635d 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -1,4 +1,10 @@ //! Navigate an endless amount of content with a scrollbar. +use dnd::DndEvent; +use iced_runtime::core::widget::Id; +use iced_style::core::clipboard::DndDestinationRectangles; +#[cfg(feature = "a11y")] +use std::borrow::Cow; + use crate::core::event::{self, Event}; use crate::core::keyboard; use crate::core::layout; @@ -6,16 +12,16 @@ use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; use crate::core::touch; -use crate::core::widget; use crate::core::widget::operation::{self, Operation}; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Background, Clipboard, Color, Element, Layout, Length, Pixels, Point, - Rectangle, Shell, Size, Vector, Widget, + id::Internal, Background, Clipboard, Color, Element, Layout, Length, + Pixels, Point, Rectangle, Shell, Size, Vector, Widget, }; use crate::runtime::Command; pub use crate::style::scrollable::{Scrollbar, Scroller, StyleSheet}; +use iced_renderer::core::widget::OperationOutputWrapper; pub use operation::scrollable::{AbsoluteOffset, RelativeOffset}; /// A widget that can vertically display an infinite amount of content with a @@ -30,7 +36,14 @@ pub struct Scrollable< Theme: StyleSheet, Renderer: crate::core::Renderer, { - id: Option, + id: Id, + scrollbar_id: Id, + #[cfg(feature = "a11y")] + name: Option>, + #[cfg(feature = "a11y")] + description: Option>, + #[cfg(feature = "a11y")] + label: Option>, width: Length, height: Length, direction: Direction, @@ -49,7 +62,14 @@ where content: impl Into>, ) -> Self { Scrollable { - id: None, + id: Id::unique(), + scrollbar_id: Id::unique(), + #[cfg(feature = "a11y")] + name: None, + #[cfg(feature = "a11y")] + description: None, + #[cfg(feature = "a11y")] + label: None, width: Length::Shrink, height: Length::Shrink, direction: Direction::default(), @@ -61,7 +81,7 @@ where /// Sets the [`Id`] of the [`Scrollable`]. pub fn id(mut self, id: Id) -> Self { - self.id = Some(id); + self.id = id; self } @@ -82,7 +102,6 @@ where self.direction = direction; self } - /// Sets a function to call when the [`Scrollable`] is scrolled. /// /// The function takes the [`Viewport`] of the [`Scrollable`] @@ -96,6 +115,41 @@ where self.style = style.into(); self } + + #[cfg(feature = "a11y")] + /// Sets the name of the [`Button`]. + pub fn name(mut self, name: impl Into>) -> Self { + self.name = Some(name.into()); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description_widget( + mut self, + description: &impl iced_accessibility::Describes, + ) -> Self { + self.description = Some(iced_accessibility::Description::Id( + description.description(), + )); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description(mut self, description: impl Into>) -> Self { + self.description = + Some(iced_accessibility::Description::Text(description.into())); + self + } + + #[cfg(feature = "a11y")] + /// Sets the label of the [`Button`]. + pub fn label(mut self, label: &dyn iced_accessibility::Labels) -> Self { + self.label = + Some(label.label().into_iter().map(|l| l.into()).collect()); + self + } } /// The direction of [`Scrollable`]. @@ -219,8 +273,8 @@ where vec![Tree::new(&self.content)] } - fn diff(&self, tree: &mut Tree) { - tree.diff_children(std::slice::from_ref(&self.content)); + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(std::slice::from_mut(&mut self.content)) } fn size(&self) -> Size { @@ -257,7 +311,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation>, ) { let state = tree.state.downcast_mut::(); @@ -267,25 +321,16 @@ where let translation = state.translation(self.direction, bounds, content_bounds); - operation.scrollable( - state, - self.id.as_ref().map(|id| &id.0), - bounds, - translation, - ); + operation.scrollable(state, Some(&self.id), bounds, translation); - operation.container( - self.id.as_ref().map(|id| &id.0), - bounds, - &mut |operation| { - self.content.as_widget().operate( - &mut tree.children[0], - layout.children().next().unwrap(), - renderer, - operation, - ); - }, - ); + operation.container(Some(&self.id), bounds, &mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + operation, + ); + }); } fn on_event( @@ -405,6 +450,181 @@ 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(); + } + } + } + + fn drag_destinations( + &self, + tree: &Tree, + layout: Layout<'_>, + renderer: &Renderer, + dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles, + ) { + let my_state = tree.state.downcast_ref::(); + if let Some((c_layout, c_state)) = + layout.children().zip(tree.children.iter()).next() + { + let mut my_dnd_rectangles = DndDestinationRectangles::new(); + self.content.as_widget().drag_destinations( + c_state, + c_layout, + renderer, + &mut my_dnd_rectangles, + ); + let mut my_dnd_rectangles = my_dnd_rectangles.into_rectangles(); + + let bounds = layout.bounds(); + let content_bounds = c_layout.bounds(); + for r in &mut my_dnd_rectangles { + let translation = my_state.translation( + self.direction, + bounds, + content_bounds, + ); + r.rectangle.x -= translation.x as f64; + r.rectangle.y -= translation.y as f64; + } + dnd_rectangles.append(&mut my_dnd_rectangles); + } + } } impl<'a, Message, Theme, Renderer> @@ -422,37 +642,13 @@ where } } -/// The identifier of a [`Scrollable`]. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Id(widget::Id); - -impl Id { - /// Creates a custom [`Id`]. - pub fn new(id: impl Into>) -> Self { - Self(widget::Id::new(id)) - } - - /// Creates a unique [`Id`]. - /// - /// This function produces a different [`Id`] every time it is called. - pub fn unique() -> Self { - Self(widget::Id::unique()) - } -} - -impl From for widget::Id { - fn from(id: Id) -> Self { - id.0 - } -} - /// Produces a [`Command`] that snaps the [`Scrollable`] with the given [`Id`] /// to the provided `percentage` along the x & y axis. pub fn snap_to( id: Id, offset: RelativeOffset, ) -> Command { - Command::widget(operation::scrollable::snap_to(id.0, offset)) + Command::widget(operation::scrollable::snap_to(id, offset)) } /// Produces a [`Command`] that scrolls the [`Scrollable`] with the given [`Id`] @@ -461,7 +657,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 +694,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 +736,28 @@ pub fn update( let translation = state.translation(direction, bounds, content_bounds); + if let Event::Dnd(DndEvent::Offer(_, e)) = &mut event { + match e { + dnd::OfferEvent::Enter { x, y, .. } => { + *x += f64::from(translation.x); + *y += f64::from(translation.y); + } + dnd::OfferEvent::Motion { x, y } => { + *x += f64::from(translation.x); + *y += f64::from(translation.y); + } + _ => {} + } + } + + #[cfg(feature = "wayland")] + if let Event::PlatformSpecific( + iced_runtime::core::event::PlatformSpecific::Wayland(e), + ) = &mut event + { + e.translate(translation); + } + update_content( event.clone(), content, @@ -584,6 +802,13 @@ pub fn update( } mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y), }; + if matches!(direction, Direction::Vertical(_)) + && delta.y.abs() < 0.1 + || matches!(direction, Direction::Horizontal(_)) + && delta.x.abs() < 0.1 + { + return event::Status::Ignored; + } state.scroll(delta, direction, bounds, content_bounds); diff --git a/widget/src/slider.rs b/widget/src/slider.rs index 5c3b63841e..5951d25cf4 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -7,17 +7,22 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; +use crate::core::widget::Id; use crate::core::{ - Border, Clipboard, Element, Layout, Length, Pixels, Point, Rectangle, - Shell, Size, Widget, + Border, Clipboard, Color, Element, Layout, Length, Pixels, Point, + Rectangle, Shell, Size, Widget, }; use std::ops::RangeInclusive; +use iced_renderer::core::{border::Radius, Degrees, Radians}; pub use iced_style::slider::{ - Appearance, Handle, HandleShape, Rail, StyleSheet, + Appearance, Handle, HandleShape, Rail, RailBackground, StyleSheet, }; +#[cfg(feature = "a11y")] +use std::borrow::Cow; + /// An horizontal bar and a handle that selects a single value from a range of /// values. /// @@ -43,13 +48,22 @@ pub use iced_style::slider::{ /// /// ![Slider drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/slider.png?raw=true) #[allow(missing_debug_implementations)] +#[must_use] pub struct Slider<'a, T, Message, Theme = crate::Theme> where Theme: StyleSheet, { + id: Id, + #[cfg(feature = "a11y")] + name: Option>, + #[cfg(feature = "a11y")] + description: Option>, + #[cfg(feature = "a11y")] + label: Option>, range: RangeInclusive, step: T, value: T, + breakpoints: &'a [T], on_change: Box Message + 'a>, on_release: Option, width: Length, @@ -91,9 +105,17 @@ 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), + breakpoints: &[], on_change: Box::new(on_change), on_release: None, width: Length::Fill, @@ -102,12 +124,20 @@ where } } + /// Defines breakpoints to visibly mark on the slider. + /// + /// The slider will gravitate towards a breakpoint when near it. + pub fn breakpoints(mut self, breakpoints: &'a [T]) -> Self { + self.breakpoints = breakpoints; + self + } + /// Sets the release message of the [`Slider`]. /// This is called when the mouse is released from the slider. /// /// Typically, the user's interaction with the slider is finished when this message is produced. /// This is useful if you need to spawn a long-running task from the slider's result, where - /// the default on_change message could create too many events. + /// the default `on_change` message could create too many events. pub fn on_release(mut self, on_release: Message) -> Self { self.on_release = Some(on_release); self @@ -136,6 +166,41 @@ where self.step = step.into(); self } + + #[cfg(feature = "a11y")] + /// Sets the name of the [`Button`]. + pub fn name(mut self, name: impl Into>) -> Self { + self.name = Some(name.into()); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description_widget( + mut self, + description: &impl iced_accessibility::Describes, + ) -> Self { + self.description = Some(iced_accessibility::Description::Id( + description.description(), + )); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description(mut self, description: impl Into>) -> Self { + self.description = + Some(iced_accessibility::Description::Text(description.into())); + self + } + + #[cfg(feature = "a11y")] + /// Sets the label of the [`Button`]. + pub fn label(mut self, label: &dyn iced_accessibility::Labels) -> Self { + self.label = + Some(label.label().into_iter().map(|l| l.into()).collect()); + self + } } impl<'a, T, Message, Theme, Renderer> Widget @@ -212,6 +277,7 @@ where tree.state.downcast_ref::(), self.value, &self.range, + self.breakpoints, theme, &self.style, ); @@ -227,6 +293,87 @@ where ) -> mouse::Interaction { mouse_interaction(layout, cursor, tree.state.downcast_ref::()) } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + layout: Layout<'_>, + _state: &Tree, + cursor: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::{ + accesskit::{NodeBuilder, NodeId, Rect, Role}, + A11yTree, + }; + + let bounds = layout.bounds(); + let is_hovered = cursor.is_over(bounds); + let Rectangle { + x, + y, + width, + height, + } = bounds; + let bounds = Rect::new( + x as f64, + y as f64, + (x + width) as f64, + (y + height) as f64, + ); + let mut node = NodeBuilder::new(Role::Slider); + node.set_bounds(bounds); + if let Some(name) = self.name.as_ref() { + node.set_name(name.clone()); + } + match self.description.as_ref() { + Some(iced_accessibility::Description::Id(id)) => { + node.set_described_by( + id.iter() + .cloned() + .map(|id| NodeId::from(id)) + .collect::>(), + ); + } + Some(iced_accessibility::Description::Text(text)) => { + node.set_description(text.clone()); + } + None => {} + } + + if is_hovered { + node.set_hovered(); + } + + if let Some(label) = self.label.as_ref() { + node.set_labelled_by(label.clone()); + } + + if let Ok(min) = self.range.start().clone().try_into() { + node.set_min_numeric_value(min); + } + if let Ok(max) = self.range.end().clone().try_into() { + node.set_max_numeric_value(max); + } + if let Ok(value) = self.value.clone().try_into() { + node.set_numeric_value(value); + } + if let Ok(step) = self.step.clone().try_into() { + node.set_numeric_value_step(step); + } + + // TODO: This could be a setting on the slider + node.set_live(iced_accessibility::accesskit::Live::Polite); + + A11yTree::leaf(node, self.id.clone()) + } + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn set_id(&mut self, id: Id) { + self.id = id; + } } impl<'a, T, Message, Theme, Renderer> From> @@ -340,6 +487,7 @@ pub fn draw( state: &State, value: T, range: &RangeInclusive, + breakpoints: &[T], theme: &Theme, style: &Theme::Style, ) where @@ -358,15 +506,42 @@ pub fn draw( theme.active(style) }; + let border_width = style + .handle + .border_width + .min(bounds.height / 2.0) + .min(bounds.width / 2.0); + let (handle_width, handle_height, handle_border_radius) = match style.handle.shape { HandleShape::Circle { radius } => { - (radius * 2.0, radius * 2.0, radius.into()) + let radius = (radius) + .max(2.0 * border_width) + .min(bounds.height / 2.0) + .min(bounds.width / 2.0); + (radius * 2.0, radius * 2.0, Radius::from(radius)) } HandleShape::Rectangle { + height, 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 = (f32::from(height)) + .max(2.0 * border_width) + .min(bounds.height); + let mut border_radius: [f32; 4] = border_radius.into(); + for r in &mut border_radius { + *r = (*r) + .min(height / 2.0) + .min(width / 2.0) + .max(*r * (width + border_width * 2.0) / width); + } + + (width, height, border_radius.into()) + } }; let value = value.into() as f32; @@ -385,39 +560,97 @@ 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, + // Draw the breakpoint indicators beneath the slider. + const BREAKPOINT_WIDTH: f32 = 2.0; + for &value in breakpoints { + let value: f64 = value.into(); + let offset = if range_start >= range_end { + 0.0 + } else { + (bounds.width - BREAKPOINT_WIDTH) * (value as f32 - range_start) + / (range_end - range_start) + }; + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + offset, + y: rail_y + 6.0, + width: BREAKPOINT_WIDTH, + height: 8.0, + }, + border: Border { + radius: 0.0.into(), + width: 0.0, + color: Color::TRANSPARENT, + }, + ..renderer::Quad::default() }, - border: Border::with_radius(style.rail.border_radius), - ..renderer::Quad::default() - }, - style.rail.colors.0, - ); + crate::core::Background::Color(style.breakpoint.color), + ); + } - 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, + match style.rail.colors { + RailBackground::Pair(l, r) => { + // rail + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: rail_y - style.rail.width / 2.0, + width: offset + handle_width / 2.0, + height: style.rail.width, + }, + border: Border::with_radius(style.rail.border_radius), + ..renderer::Quad::default() + }, + l, + ); + + // right rail + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + offset + handle_width / 2.0, + y: rail_y - style.rail.width / 2.0, + width: bounds.width - offset - handle_width / 2.0, + height: style.rail.width, + }, + border: Border::with_radius(style.rail.border_radius), + ..renderer::Quad::default() + }, + r, + ); + } + RailBackground::Gradient { + mut gradient, + auto_angle, + } => renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: rail_y - style.rail.width / 2.0, + width: bounds.width, + height: style.rail.width, + }, + border: Border::with_radius(style.rail.border_radius), + ..renderer::Quad::default() }, - border: Border::with_radius(style.rail.border_radius), - ..renderer::Quad::default() - }, - style.rail.colors.1, - ); + if auto_angle { + gradient.angle = Radians::from(Degrees(90.0)); + gradient + } else { + gradient + }, + ), + } + // handle renderer.fill_quad( renderer::Quad { bounds: Rectangle { x: bounds.x + offset, - y: rail_y - handle_height / 2.0, + y: rail_y - (handle_height / 2.0), width: handle_width, height: handle_height, }, diff --git a/widget/src/svg.rs b/widget/src/svg.rs index 12ef3d925c..ba51ee5551 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,9 @@ use crate::core::{ ContentFit, Element, Layout, Length, Rectangle, Size, Vector, Widget, }; +#[cfg(feature = "a11y")] +use std::borrow::Cow; +use std::marker::PhantomData; use std::path::PathBuf; pub use crate::style::svg::{Appearance, StyleSheet}; @@ -20,29 +25,47 @@ pub use svg::Handle; /// [`Svg`] images can have a considerable rendering cost when resized, /// specially when they are complex. #[allow(missing_debug_implementations)] -pub struct Svg +pub struct Svg<'a, Theme = crate::Theme> where Theme: StyleSheet, { + 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, style: ::Style, + symbolic: bool, + _phantom_data: PhantomData<&'a ()>, } -impl Svg +impl<'a, Theme> Svg<'a, Theme> where Theme: StyleSheet, { /// 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: PhantomData::default(), } } @@ -78,15 +101,58 @@ where } } + /// Symbolic icons inherit their color from the renderer if a color is not defined. + #[must_use] + pub fn symbolic(mut self, symbolic: bool) -> Self { + self.symbolic = symbolic; + self + } + /// Sets the style variant of this [`Svg`]. #[must_use] pub fn style(mut self, style: impl Into) -> Self { self.style = style.into(); self } + + #[cfg(feature = "a11y")] + /// Sets the name of the [`Button`]. + pub fn name(mut self, name: impl Into>) -> Self { + self.name = Some(name.into()); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description_widget( + mut self, + description: &T, + ) -> Self { + self.description = Some(iced_accessibility::Description::Id( + description.description(), + )); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description(mut self, description: impl Into>) -> Self { + self.description = + Some(iced_accessibility::Description::Text(description.into())); + self + } + + #[cfg(feature = "a11y")] + /// Sets the label of the [`Button`]. + pub fn label(mut self, label: &dyn iced_accessibility::Labels) -> Self { + self.label = + Some(label.label().into_iter().map(|l| l.into()).collect()); + self + } } -impl Widget for Svg +impl<'a, Message, Theme, Renderer> Widget + for Svg<'a, Theme> where Theme: iced_style::svg::StyleSheet, Renderer: svg::Renderer, @@ -134,7 +200,7 @@ where _state: &Tree, renderer: &mut Renderer, theme: &Theme, - _style: &renderer::Style, + style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, _viewport: &Rectangle, @@ -158,11 +224,14 @@ where ..bounds }; - let appearance = if is_mouse_over { + let mut appearance = if is_mouse_over { theme.hovered(&self.style) } else { theme.appearance(&self.style) }; + if self.symbolic && appearance.color.is_none() { + appearance.color = Some(style.icon_color); + } renderer.draw( self.handle.clone(), @@ -179,15 +248,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, Theme, Renderer> From> +impl<'a, Message, Theme, Renderer> From> for Element<'a, Message, Theme, Renderer> where Theme: iced_style::svg::StyleSheet + 'a, Renderer: svg::Renderer + 'a, { - fn from(icon: Svg) -> Element<'a, Message, Theme, Renderer> { + fn from(icon: Svg<'a, Theme>) -> Element<'a, Message, Theme, Renderer> { Element::new(icon) } } diff --git a/widget/src/text_input/mod.rs b/widget/src/text_input/mod.rs new file mode 100644 index 0000000000..8289bc2ba9 --- /dev/null +++ b/widget/src/text_input/mod.rs @@ -0,0 +1,10 @@ +//! Display fields that can be filled with text. +//! +//! A [`TextInput`] has some local [`State`]. +pub(crate) mod editor; +pub(crate) mod value; + +pub mod cursor; + +mod text_input; +pub use text_input::*; diff --git a/widget/src/text_input.rs b/widget/src/text_input/text_input.rs similarity index 97% rename from widget/src/text_input.rs rename to widget/src/text_input/text_input.rs index 0a7ed01493..fcde1a7f6d 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}; @@ -21,9 +20,9 @@ use crate::core::renderer; use crate::core::text::{self, Paragraph as _, Text}; use crate::core::time::{Duration, Instant}; use crate::core::touch; -use crate::core::widget; use crate::core::widget::operation::{self, Operation}; use crate::core::widget::tree::{self, Tree}; +use crate::core::widget::Id; use crate::core::window; use crate::core::{ Clipboard, Element, Layout, Length, Padding, Pixels, Point, Rectangle, @@ -273,7 +272,7 @@ where tree::State::new(State::::new()) } - fn diff(&self, tree: &mut Tree) { + fn diff(&mut self, tree: &mut Tree) { let state = tree.state.downcast_mut::>(); // Unfocus text input if it becomes disabled @@ -319,12 +318,12 @@ where tree: &mut Tree, _layout: Layout<'_>, _renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation>, ) { let state = tree.state.downcast_mut::>(); - operation.focusable(state, self.id.as_ref().map(|id| &id.0)); - operation.text_input(state, self.id.as_ref().map(|id| &id.0)); + operation.focusable(state, self.id.as_ref()); + operation.text_input(state, self.id.as_ref()); } fn on_event( @@ -432,45 +431,21 @@ pub enum Side { Right, } -/// The identifier of a [`TextInput`]. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Id(widget::Id); - -impl Id { - /// Creates a custom [`Id`]. - pub fn new(id: impl Into>) -> Self { - Self(widget::Id::new(id)) - } - - /// Creates a unique [`Id`]. - /// - /// This function produces a different [`Id`] every time it is called. - pub fn unique() -> Self { - Self(widget::Id::unique()) - } -} - -impl From for widget::Id { - fn from(id: Id) -> Self { - id.0 - } -} - /// Produces a [`Command`] that focuses the [`TextInput`] with the given [`Id`]. pub fn focus(id: Id) -> Command { - Command::widget(operation::focusable::focus(id.0)) + Command::widget(operation::focusable::focus(id)) } /// Produces a [`Command`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the /// end. pub fn move_cursor_to_end(id: Id) -> Command { - Command::widget(operation::text_input::move_cursor_to_end(id.0)) + Command::widget(operation::text_input::move_cursor_to_end(id)) } /// Produces a [`Command`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the /// front. pub fn move_cursor_to_front(id: Id) -> Command { - Command::widget(operation::text_input::move_cursor_to_front(id.0)) + Command::widget(operation::text_input::move_cursor_to_front(id)) } /// Produces a [`Command`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the @@ -479,12 +454,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`]. @@ -522,6 +497,7 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, + wrap: text::Wrap::default(), }; state.placeholder.update(placeholder_text); @@ -544,6 +520,7 @@ where horizontal_alignment: alignment::Horizontal::Center, vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, + wrap: text::Wrap::default(), }; state.icon.update(icon_text); @@ -1459,6 +1436,7 @@ fn replace_paragraph( horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, shaping: text::Shaping::Advanced, + wrap: text::Wrap::default(), }); } diff --git a/widget/src/themer.rs b/widget/src/themer.rs index ee96a49362..3c2e4862a5 100644 --- a/widget/src/themer.rs +++ b/widget/src/themer.rs @@ -1,3 +1,5 @@ +use iced_renderer::core::widget::OperationOutputWrapper; + use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; @@ -57,8 +59,8 @@ where self.content.as_widget().children() } - fn diff(&self, tree: &mut Tree) { - self.content.as_widget().diff(tree); + fn diff(&mut self, tree: &mut Tree) { + self.content.as_widget_mut().diff(tree); } fn size(&self) -> Size { @@ -79,7 +81,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation>, ) { self.content .as_widget() @@ -196,7 +198,7 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation>, ) { self.content.operate(layout, renderer, operation); } diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index 1d313df3ba..becc9080d7 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::{ - Border, Clipboard, Element, Event, Layout, Length, Pixels, Rectangle, + id, Border, Clipboard, Element, Event, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, }; @@ -41,8 +44,16 @@ pub struct Toggler< Theme: StyleSheet, Renderer: text::Renderer, { + id: Id, + label_id: Option, + #[cfg(feature = "a11y")] + name: Option>, + #[cfg(feature = "a11y")] + description: Option>, + #[cfg(feature = "a11y")] + labeled_by_widget: Option>, is_toggled: bool, - on_toggle: Box Message + 'a>, + on_toggle: Option Message + 'a>>, label: Option, width: Length, size: f32, @@ -50,6 +61,7 @@ pub struct Toggler< text_line_height: text::LineHeight, text_alignment: alignment::Horizontal, text_shaping: text::Shaping, + text_wrap: text::Wrap, spacing: f32, font: Option, style: Theme::Style, @@ -74,22 +86,37 @@ where pub fn new( label: impl Into>, is_toggled: bool, - f: F, + f: Option, ) -> Self 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(), + on_toggle: if f.is_some() { + Some(Box::new(f.unwrap())) + } else { + None + }, + 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, + text_wrap: text::Wrap::default(), + spacing: 0.0, font: None, style: Default::default(), } @@ -134,6 +161,12 @@ where self } + /// Sets the [`text::Wrap`] mode of the [`Toggler`]. + pub fn text_wrap(mut self, wrap: text::Wrap) -> Self { + self.text_wrap = wrap; + self + } + /// Sets the spacing between the [`Toggler`] and the text. pub fn spacing(mut self, spacing: impl Into) -> Self { self.spacing = spacing.into().0; @@ -153,6 +186,41 @@ where self.style = style.into(); self } + + #[cfg(feature = "a11y")] + /// Sets the name of the [`Button`]. + pub fn name(mut self, name: impl Into>) -> Self { + self.name = Some(name.into()); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description_widget( + mut self, + description: &T, + ) -> Self { + self.description = Some(iced_accessibility::Description::Id( + description.description(), + )); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description(mut self, description: impl Into>) -> Self { + self.description = + Some(iced_accessibility::Description::Text(description.into())); + self + } + + #[cfg(feature = "a11y")] + /// Sets the label of the [`Button`] using another widget. + pub fn label(mut self, label: &dyn iced_accessibility::Labels) -> Self { + self.labeled_by_widget = + Some(label.label().into_iter().map(|l| l.into()).collect()); + self + } } impl<'a, Message, Theme, Renderer> Widget @@ -187,7 +255,7 @@ 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(48., 24.)), |limits| { if let Some(label) = self.label.as_deref() { let state = tree @@ -207,9 +275,10 @@ where self.text_alignment, alignment::Vertical::Top, self.text_shaping, + self.text_wrap, ) } else { - layout::Node::new(Size::ZERO) + layout::Node::new(crate::core::Size::ZERO) } }, ) @@ -231,8 +300,10 @@ where | Event::Touch(touch::Event::FingerPressed { .. }) => { let mouse_over = cursor.is_over(layout.bounds()); - if mouse_over { - shell.publish((self.on_toggle)(!self.is_toggled)); + if self.on_toggle.is_some() && mouse_over { + shell.publish((self.on_toggle.as_ref().unwrap())( + !self.is_toggled, + )); event::Status::Captured } else { @@ -268,13 +339,6 @@ where cursor: mouse::Cursor, viewport: &Rectangle, ) { - /// Makes sure that the border radius of the toggler looks good at every size. - const BORDER_RADIUS_RATIO: f32 = 32.0 / 13.0; - - /// The space ratio between the background Quad and the Toggler bounds, and - /// between the background Quad and foreground Quad. - const SPACE_RATIO: f32 = 0.05; - let mut children = layout.children(); let toggler_layout = children.next().unwrap(); @@ -294,29 +358,29 @@ where let bounds = toggler_layout.bounds(); let is_mouse_over = cursor.is_over(layout.bounds()); - let style = if is_mouse_over { + let style = if self.on_toggle.is_none() { + theme.disabled(&self.style, self.is_toggled) + } else if is_mouse_over { theme.hovered(&self.style, self.is_toggled) } else { theme.active(&self.style, self.is_toggled) }; - let border_radius = bounds.height / BORDER_RADIUS_RATIO; - let space = SPACE_RATIO * bounds.height; + let space = style.handle_margin; let toggler_background_bounds = Rectangle { - x: bounds.x + space, - y: bounds.y + space, - width: bounds.width - (2.0 * space), - height: bounds.height - (2.0 * space), + x: bounds.x, + y: bounds.y, + width: bounds.width, + height: bounds.height, }; renderer.fill_quad( renderer::Quad { bounds: toggler_background_bounds, border: Border { - radius: border_radius.into(), - width: 1.0, - color: style.background_border.unwrap_or(style.background), + radius: style.border_radius, + ..Default::default() }, ..renderer::Quad::default() }, @@ -326,28 +390,127 @@ where let toggler_foreground_bounds = Rectangle { x: bounds.x + if self.is_toggled { - bounds.width - 2.0 * space - (bounds.height - (4.0 * space)) + bounds.width - space - (bounds.height - (2.0 * space)) } else { - 2.0 * space + space }, - y: bounds.y + (2.0 * space), - width: bounds.height - (4.0 * space), - height: bounds.height - (4.0 * space), + y: bounds.y + space, + width: bounds.height - (2.0 * space), + height: bounds.height - (2.0 * space), }; renderer.fill_quad( renderer::Quad { bounds: toggler_foreground_bounds, border: Border { - radius: border_radius.into(), - width: 1.0, - color: style.foreground_border.unwrap_or(style.foreground), + radius: style.handle_radius, + ..Default::default() }, ..renderer::Quad::default() }, 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, Checked, 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(if self.is_toggled { + Checked::True + } else { + Checked::False + }); + if is_hovered { + node.set_hovered(); + } + node.add_action(Action::Default); + if let Some(label) = self.label.as_ref() { + let mut label_node = NodeBuilder::new(Role::StaticText); + + label_node.set_name(label.clone()); + // TODO proper label bounds for the label + label_node.set_bounds(bounds); + + A11yTree::node_with_child_tree( + A11yNode::new(node, self.id.clone()), + A11yTree::leaf(label_node, self.label_id.clone().unwrap()), + ) + } else { + if let Some(labeled_by_widget) = self.labeled_by_widget.as_ref() { + node.set_labelled_by(labeled_by_widget.clone()); + } + A11yTree::leaf(node, self.id.clone()) + } + } + + fn id(&self) -> Option { + if self.label.is_some() { + Some(Id(iced_runtime::core::id::Internal::Set(vec![ + self.id.0.clone(), + self.label_id.clone().unwrap().0, + ]))) + } else { + Some(self.id.clone()) + } + } + + fn set_id(&mut self, id: Id) { + if let Id(id::Internal::Set(list)) = id { + if list.len() == 2 && self.label.is_some() { + self.id.0 = list[0].clone(); + self.label_id = Some(Id(list[1].clone())); + } + } else if self.label.is_none() { + self.id = id; + } + } } impl<'a, Message, Theme, Renderer> From> diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs index 44cfc4b39e..c34cf23b9b 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -123,10 +123,6 @@ where ] } - fn diff(&self, tree: &mut widget::Tree) { - tree.diff_children(&[self.content.as_widget(), &self.tooltip]); - } - fn state(&self) -> widget::tree::State { widget::tree::State::new(State::default()) } @@ -139,6 +135,13 @@ where self.content.as_widget().size() } + fn diff(&mut self, tree: &mut crate::core::widget::Tree) { + tree.diff_children(&mut [ + self.content.as_widget_mut(), + &mut self.tooltip, + ]) + } + fn layout( &self, tree: &mut widget::Tree, @@ -445,7 +448,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::<(), Theme, Renderer>::draw( diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index d3086a81f5..88a45d91b5 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; @@ -364,9 +366,10 @@ pub fn draw( (radius * 2.0, radius * 2.0, radius.into()) } HandleShape::Rectangle { + height, width, border_radius, - } => (f32::from(width), bounds.width, border_radius), + } => (f32::from(width), f32::from(height), border_radius), }; let value = value.into() as f32; @@ -385,33 +388,60 @@ pub fn draw( let rail_x = bounds.x + bounds.width / 2.0; - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: rail_x - style.rail.width / 2.0, - y: bounds.y, - width: style.rail.width, - height: offset + handle_width / 2.0, - }, - border: Border::with_radius(style.rail.border_radius), - ..renderer::Quad::default() - }, - style.rail.colors.1, - ); - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: rail_x - style.rail.width / 2.0, - y: bounds.y + offset + handle_width / 2.0, - width: style.rail.width, - height: bounds.height - offset - handle_width / 2.0, - }, - border: Border::with_radius(style.rail.border_radius), - ..renderer::Quad::default() - }, - style.rail.colors.0, - ); + match style.rail.colors { + iced_style::slider::RailBackground::Pair(start, end) => { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: rail_x - style.rail.width / 2.0, + y: bounds.y, + width: style.rail.width, + height: offset + handle_width / 2.0, + }, + border: Border::with_radius(style.rail.border_radius), + ..renderer::Quad::default() + }, + end, + ); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: rail_x - style.rail.width / 2.0, + y: bounds.y + offset + handle_width / 2.0, + width: style.rail.width, + height: bounds.height - offset - handle_width / 2.0, + }, + border: Border::with_radius(style.rail.border_radius), + ..renderer::Quad::default() + }, + start, + ); + } + iced_style::slider::RailBackground::Gradient { + mut gradient, + auto_angle, + } => { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: rail_x - style.rail.width / 2.0, + y: bounds.y, + width: style.rail.width, + height: bounds.height - handle_width / 2.0, + }, + border: Border::with_radius(style.rail.border_radius), + ..renderer::Quad::default() + }, + if auto_angle { + gradient.angle = Radians::from(Degrees(180.0)); + gradient + } else { + gradient + }, + ); + } + } renderer.fill_quad( renderer::Quad { diff --git a/winit/Cargo.toml b/winit/Cargo.toml index 87e600aeba..a32bdac83d 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -20,16 +20,20 @@ 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 window_clipboard.workspace = true +dnd.workspace = true winit.workspace = true sysinfo.workspace = true diff --git a/winit/src/application.rs b/winit/src/application.rs index 21a985e8b1..a8a898a655 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -1,7 +1,23 @@ //! Create interactive, native cross-platform applications. +mod drag_resize; mod state; +use dnd::DndAction; +use dnd::DndEvent; +use dnd::DndSurface; +use dnd::Icon; +#[cfg(feature = "a11y")] +use iced_graphics::core::widget::operation::focusable::focus; +use iced_graphics::core::widget::operation::OperationWrapper; +use iced_graphics::core::widget::Operation; +use iced_graphics::Viewport; +use iced_runtime::futures::futures::FutureExt; +use iced_style::core::clipboard::DndSource; +use iced_style::core::Clipboard as CoreClipboard; +use iced_style::core::Length; pub use state::State; +use window_clipboard::mime; +use window_clipboard::mime::ClipboardStoreData; use crate::conversion; use crate::core; @@ -20,12 +36,83 @@ 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::any::Any; use std::mem::ManuallyDrop; use std::sync::Arc; +#[cfg(feature = "trace")] +pub use profiler::Profiler; +#[cfg(feature = "trace")] +use tracing::{info_span, instrument::Instrument}; + +/// 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, + /// CLipboard Message + StartDnd { + /// internal dnd + internal: bool, + /// the surface the dnd is started from + source_surface: Option, + /// the icon if any + /// This is actually an Element + icon_surface: Option>, + /// the content of the dnd + content: Box, + /// the actions of the dnd + actions: DndAction, + }, + /// Dnd Event + Dnd(DndEvent), +} + +unsafe impl Send for UserEventWrapper {} + +impl std::fmt::Debug for UserEventWrapper { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + UserEventWrapper::Message(m) => write!(f, "Message({:?})", m), + #[cfg(feature = "a11y")] + UserEventWrapper::A11y(a) => write!(f, "A11y({:?})", a), + #[cfg(feature = "a11y")] + UserEventWrapper::A11yEnabled => write!(f, "A11yEnabled"), + UserEventWrapper::StartDnd { + internal, + source_surface: _, + icon_surface, + content: _, + actions, + } => write!( + f, + "StartDnd {{ internal: {:?}, icon_surface: {}, actions: {:?} }}", + internal, icon_surface.is_some(), actions + ), + UserEventWrapper::Dnd(_) => write!(f, "Dnd"), + } + } +} + +#[cfg(feature = "a11y")] +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 @@ -114,6 +201,11 @@ where let mut debug = Debug::new(); debug.startup_started(); + let resize_border = settings.window.resize_border; + + #[cfg(feature = "trace")] + let _ = info_span!("Application", "RUN").entered(); + let event_loop = EventLoopBuilder::with_user_event() .build() .expect("Create event loop"); @@ -209,6 +301,7 @@ where window, should_be_visible, exit_on_close_request, + resize_border, )); let mut context = task::Context::from_waker(task::noop_waker_ref()); @@ -241,24 +334,29 @@ async fn run_instance( mut application: A, mut compositor: C, mut renderer: A::Renderer, - mut runtime: Runtime, A::Message>, - mut proxy: winit::event_loop::EventLoopProxy, + mut runtime: Runtime< + E, + Proxy>, + UserEventWrapper, + >, + mut proxy: winit::event_loop::EventLoopProxy>, mut debug: Debug, mut event_receiver: mpsc::UnboundedReceiver< - winit::event::Event, + winit::event::Event>, >, mut control_sender: mpsc::UnboundedSender, init_command: Command, window: Arc, should_be_visible: bool, exit_on_close_request: bool, + resize_border: u32, ) where A: Application + 'static, E: Executor + 'static, C: Compositor + 'static, A::Theme: StyleSheet, + A::Message: Send + 'static, { - use futures::stream::StreamExt; use winit::event; use winit::event_loop::ControlFlow; @@ -266,7 +364,8 @@ async fn run_instance( let mut viewport_version = state.viewport_version(); let physical_size = state.physical_size(); - let mut clipboard = Clipboard::connect(&window); + let mut clipboard = + Clipboard::connect(&window, crate::proxy::Proxy::new(proxy.clone())); let mut cache = user_interface::Cache::default(); let mut surface = compositor.create_surface( window.clone(), @@ -294,7 +393,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, @@ -304,10 +408,53 @@ async fn run_instance( &mut debug, )); + let mut prev_dnd_rectangles_count = 0; + + // Creates closure for handling the window drag resize state with winit. + let mut drag_resize_window_func = drag_resize::event_func( + &window, + 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()); + let root = NodeId(node_id); + TreeUpdate { + nodes: vec![(root, node)], + tree: Some(Tree::new(root)), + focus: root, + } + }, + proxy.clone(), + ), + false, + ) + }; debug.startup_finished(); @@ -332,7 +479,135 @@ 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, + UserEventWrapper::StartDnd { + internal, + source_surface: _, // not needed if there is only one window + icon_surface, + content, + actions, + } => { + let mut renderer = compositor.create_renderer(); + let icon_surface = icon_surface + .map(|i| { + let i: Box = i; + i + }) + .and_then(|i| { + i.downcast::, + core::widget::tree::State, + )>>() + .ok() + }) + .map(|e| { + let e = Arc::into_inner(*e).unwrap(); + let (mut e, widget_state) = e; + let lim = core::layout::Limits::new( + Size::new(1., 1.), + Size::new( + state.viewport().physical_width() + as f32, + state.viewport().physical_height() + as f32, + ), + ); + + let mut tree = core::widget::Tree { + id: e.as_widget().id(), + tag: e.as_widget().tag(), + state: widget_state, + children: e.as_widget().children(), + }; + + let size = e + .as_widget() + .layout(&mut tree, &renderer, &lim); + e.as_widget_mut().diff(&mut tree); + + let size = lim.resolve( + Length::Shrink, + Length::Shrink, + size.size(), + ); + let mut surface = compositor.create_surface( + window.clone(), + size.width.ceil() as u32, + size.height.ceil() as u32, + ); + let viewport = Viewport::with_logical_size( + size, + state.viewport().scale_factor(), + ); + + let mut ui = UserInterface::build( + e, + size, + user_interface::Cache::default(), + &mut renderer, + ); + _ = ui.draw( + &mut renderer, + state.theme(), + &renderer::Style { + icon_color: state.icon_color(), + text_color: state.text_color(), + scale_factor: state.scale_factor(), + }, + Default::default(), + ); + let mut bytes = compositor.screenshot( + &mut renderer, + &mut surface, + &viewport, + core::Color::TRANSPARENT, + &debug.overlay(), + ); + for pix in bytes.chunks_exact_mut(4) { + // rgba -> argb little endian + pix.swap(0, 2); + } + Icon::Buffer { + data: Arc::new(bytes), + width: viewport.physical_width(), + height: viewport.physical_height(), + transparent: true, + } + }); + + clipboard.start_dnd_winit( + internal, + DndSurface(Arc::new(Box::new(window.clone()))), + icon_surface, + content, + actions, + ); + } + UserEventWrapper::Dnd(e) => events.push(Event::Dnd(e)), + }; } event::Event::WindowEvent { event: event::WindowEvent::RedrawRequested { .. }, @@ -406,11 +681,13 @@ 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(), ); - redraw_pending = false; + debug.draw_finished(); if new_mouse_interaction != mouse_interaction { @@ -421,6 +698,87 @@ async fn run_instance( mouse_interaction = new_mouse_interaction; } + redraw_pending = false; + + let physical_size = state.physical_size(); + + if physical_size.width == 0 || physical_size.height == 0 { + continue; + } + + #[cfg(feature = "a11y")] + if a11y_enabled { + use iced_accessibility::{ + accesskit::{ + NodeBuilder, NodeId, Role, Tree, TreeUpdate, + }, + A11yId, A11yNode, A11yTree, + }; + // TODO send a11y tree + let child_tree = user_interface.a11y_nodes(state.cursor()); + let mut root = NodeBuilder::new(Role::Window); + root.set_name(state.title()); + + let window_tree = A11yTree::node_with_child_tree( + A11yNode::new(root, window_a11y_id), + child_tree, + ); + let tree = Tree::new(NodeId(window_a11y_id)); + let mut current_operation = + Some(Box::new(OperationWrapper::Id(Box::new( + operation::focusable::find_focused(), + )))); + + let mut focus = None; + while let Some(mut operation) = current_operation.take() { + user_interface.operate(&renderer, operation.as_mut()); + + match operation.finish() { + operation::Outcome::None => {} + operation::Outcome::Some(message) => match message { + operation::OperationOutputWrapper::Message( + _, + ) => { + unimplemented!(); + } + operation::OperationOutputWrapper::Id(id) => { + focus = Some(A11yId::from(id)); + } + }, + operation::Outcome::Chain(next) => { + current_operation = Some(Box::new( + OperationWrapper::Wrapper(next), + )); + } + } + } + + log::debug!( + "focus: {:?}\ntree root: {:?}\n children: {:?}", + &focus, + window_tree + .root() + .iter() + .map(|n| (n.node().role(), n.id())) + .collect::>(), + window_tree + .children() + .iter() + .map(|n| (n.node().role(), n.id())) + .collect::>() + ); + // TODO maybe optimize this? + let focus = focus + .filter(|f_id| window_tree.contains(f_id)) + .map(|id| id.into()) + .unwrap_or_else(|| tree.root); + adapter.update_if_active(|| TreeUpdate { + nodes: window_tree.into(), + tree: Some(tree), + focus, + }); + } + debug.render_started(); match compositor.present( &mut renderer, @@ -453,6 +811,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 { @@ -527,6 +892,22 @@ async fn run_instance( &mut debug, )); + let dnd_rectangles = user_interface + .dnd_rectangles(prev_dnd_rectangles_count, &renderer); + let new_dnd_rectangles_count = + dnd_rectangles.as_ref().len(); + + if new_dnd_rectangles_count > 0 + || prev_dnd_rectangles_count > 0 + { + clipboard.register_dnd_destination( + DndSurface(Arc::new(Box::new(window.clone()))), + dnd_rectangles.into_rectangles(), + ); + } + + prev_dnd_rectangles_count = new_dnd_rectangles_count; + if should_exit { break; } @@ -592,19 +973,33 @@ 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( +pub fn update( application: &mut A, compositor: &mut C, surface: &mut C::Surface, cache: &mut user_interface::Cache, state: &mut State, renderer: &mut A::Renderer, - runtime: &mut Runtime, A::Message>, - clipboard: &mut Clipboard, + runtime: &mut Runtime< + E, + Proxy>, + UserEventWrapper, + >, + clipboard: &mut Clipboard, should_exit: &mut bool, - proxy: &mut winit::event_loop::EventLoopProxy, + proxy: &mut winit::event_loop::EventLoopProxy>, debug: &mut Debug, messages: &mut Vec, window: &winit::window::Window, @@ -638,8 +1033,12 @@ pub fn update( state.synchronize(application, window); - let subscription = application.subscription(); - runtime.track(subscription.into_recipes()); + runtime.track( + application + .subscription() + .map(subscription_map::) + .into_recipes(), + ); } /// Runs the actions of a [`Command`]. @@ -651,10 +1050,14 @@ pub fn run_command( state: &State, renderer: &mut A::Renderer, command: Command, - runtime: &mut Runtime, A::Message>, - clipboard: &mut Clipboard, + runtime: &mut Runtime< + E, + Proxy>, + UserEventWrapper, + >, + clipboard: &mut Clipboard, should_exit: &mut bool, - proxy: &mut winit::event_loop::EventLoopProxy, + proxy: &mut winit::event_loop::EventLoopProxy>, debug: &mut Debug, window: &winit::window::Window, ) where @@ -670,22 +1073,46 @@ 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) => { clipboard.write(contents); } + clipboard::Action::WriteData(contents) => { + clipboard.write_data(ClipboardStoreData(contents)) + } + clipboard::Action::ReadData(allowed, to_msg) => { + let contents = clipboard.read_data(allowed); + let message = to_msg(contents); + _ = proxy.send_event(UserEventWrapper::Message(message)); + } + clipboard::Action::ReadPrimary(s_to_msg) => { + let contents = clipboard.read_primary(); + let message = s_to_msg(contents); + _ = proxy.send_event(UserEventWrapper::Message(message)); + } + clipboard::Action::WritePrimary(content) => { + clipboard.write_primary(content) + } + clipboard::Action::WritePrimaryData(content) => { + clipboard.write_primary_data(ClipboardStoreData(content)) + } + clipboard::Action::ReadPrimaryData(a, to_msg) => { + let contents = clipboard.read_primary_data(a); + let message = to_msg(contents); + _ = proxy.send_event(UserEventWrapper::Message(message)); + } }, command::Action::Window(action) => match action { window::Action::Close(_id) => { @@ -712,15 +1139,16 @@ pub fn run_command( window.inner_size().to_logical(window.scale_factor()); proxy - .send_event(callback(Size::new( - size.width, - size.height, + .send_event(UserEventWrapper::Message(callback( + Size::new(size.width, size.height), ))) .expect("Send message to event loop"); } window::Action::FetchMaximized(_id, callback) => { proxy - .send_event(callback(window.is_maximized())) + .send_event(UserEventWrapper::Message(callback( + window.is_maximized(), + ))) .expect("Send message to event loop"); } window::Action::Maximize(_id, maximized) => { @@ -728,7 +1156,9 @@ pub fn run_command( } window::Action::FetchMinimized(_id, callback) => { proxy - .send_event(callback(window.is_minimized())) + .send_event(UserEventWrapper::Message(callback( + window.is_minimized(), + ))) .expect("Send message to event loop"); } window::Action::Minimize(_id, minimized) => { @@ -758,7 +1188,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) => { @@ -778,9 +1208,19 @@ pub fn run_command( window::Action::ChangeLevel(_id, level) => { window.set_window_level(conversion::window_level(level)); } + window::Action::ShowWindowMenu(_id) => { + if let mouse::Cursor::Available(point) = state.cursor() { + window.show_window_menu(winit::dpi::LogicalPosition { + x: point.x, + y: point.y, + }); + } + } window::Action::FetchId(_id, tag) => { proxy - .send_event(tag(window.id().into())) + .send_event(UserEventWrapper::Message(tag(window + .id() + .into()))) .expect("Send message to event loop"); } window::Action::Screenshot(_id, tag) => { @@ -793,9 +1233,11 @@ pub fn run_command( ); proxy - .send_event(tag(window::Screenshot::new( - bytes, - state.physical_size(), + .send_event(UserEventWrapper::Message(tag( + window::Screenshot::new( + bytes, + state.physical_size(), + ), ))) .expect("Send message to event loop."); } @@ -814,16 +1256,16 @@ pub fn run_command( let message = _tag(information); proxy - .send_event(message) - .expect("Send message to event loop"); + .send_event(UserEventWrapper::Message(message)) + .expect("Send message to event loop") }); } } }, command::Action::Widget(action) => { let mut current_cache = std::mem::take(cache); - let mut current_operation = Some(action); - + let mut current_operation = + Some(Box::new(OperationWrapper::Message(action))); let mut user_interface = build_user_interface( application, current_cache, @@ -838,12 +1280,24 @@ pub fn run_command( match operation.finish() { operation::Outcome::None => {} operation::Outcome::Some(message) => { - proxy - .send_event(message) - .expect("Send message to event loop"); + match message { + operation::OperationOutputWrapper::Message( + m, + ) => { + proxy + .send_event(UserEventWrapper::Message( + m, + )) + .expect("Send message to event loop"); + } + operation::OperationOutputWrapper::Id(_) => { + // TODO ASHLEY should not ever happen, should this panic!()? + } + } } operation::Outcome::Chain(next) => { - current_operation = Some(next); + current_operation = + Some(Box::new(OperationWrapper::Wrapper(next))); } } } @@ -858,9 +1312,44 @@ 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!(), + command::Action::Dnd(a) => match a { + iced_runtime::dnd::DndAction::RegisterDndDestination { + surface, + rectangles, + } => { + clipboard.register_dnd_destination(surface, rectangles); + } + iced_runtime::dnd::DndAction::StartDnd { + internal, + source_surface, + icon_surface, + content, + actions, + } => clipboard.start_dnd( + internal, + source_surface, + icon_surface, + content, + actions, + ), + iced_runtime::dnd::DndAction::EndDnd => { + clipboard.end_dnd(); + } + iced_runtime::dnd::DndAction::PeekDnd(m, to_msg) => { + let data = clipboard.peek_dnd(m); + let message = to_msg(data); + proxy + .send_event(UserEventWrapper::Message(message)) + .expect("Send message to event loop"); + } + iced_runtime::dnd::DndAction::SetAction(a) => { + clipboard.set_action(a); + } + }, } } } diff --git a/winit/src/application/drag_resize.rs b/winit/src/application/drag_resize.rs new file mode 100644 index 0000000000..0584fe263a --- /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< + Box bool>, +> { + if window.drag_resize_window(ResizeDirection::East).is_ok() { + // Keep track of cursor when it is within a resizeable border. + let mut cursor_prev_resize_direction = None; + + Some(Box::new( + 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 c17a3bcc13..59d631c1fe 100644 --- a/winit/src/application/state.rs +++ b/winit/src/application/state.rs @@ -110,11 +110,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/clipboard.rs b/winit/src/clipboard.rs index 8f5c5e63e7..171ad5cdd1 100644 --- a/winit/src/clipboard.rs +++ b/winit/src/clipboard.rs @@ -1,32 +1,53 @@ //! Access the clipboard. +use std::{any::Any, borrow::Cow}; + +use crate::futures::futures::Sink; +use dnd::{DndAction, DndDestinationRectangle, DndSurface, Icon}; +use iced_style::core::clipboard::DndSource; +use window_clipboard::{ + dnd::DndProvider, + mime::{self, ClipboardData, ClipboardStoreData}, +}; + +use crate::{application::UserEventWrapper, Proxy}; + /// A buffer for short-term storage and transfer within and between /// applications. #[allow(missing_debug_implementations)] -pub struct Clipboard { - state: State, +pub struct Clipboard { + state: State, } -enum State { - Connected(window_clipboard::Clipboard), +enum State { + Connected(window_clipboard::Clipboard, Proxy>), Unavailable, } -impl Clipboard { +impl Clipboard { /// Creates a new [`Clipboard`] for the given window. - pub fn connect(window: &winit::window::Window) -> Clipboard { + pub fn connect( + window: &winit::window::Window, + proxy: Proxy>, + ) -> Clipboard { #[allow(unsafe_code)] let state = unsafe { window_clipboard::Clipboard::connect(window) } .ok() - .map(State::Connected) + .map(|c| (c, proxy.clone())) + .map(|c| State::Connected(c.0, c.1)) .unwrap_or(State::Unavailable); + #[cfg(target_os = "linux")] + if let State::Connected(clipboard, _) = &state { + clipboard.init_dnd(Box::new(proxy)); + } + Clipboard { state } } /// Creates a new [`Clipboard`] that isn't associated with a window. /// This clipboard will never contain a copied value. - pub fn unconnected() -> Clipboard { + pub fn unconnected() -> Clipboard { Clipboard { state: State::Unavailable, } @@ -35,7 +56,7 @@ impl Clipboard { /// Reads the current content of the [`Clipboard`] as text. pub fn read(&self) -> Option { match &self.state { - State::Connected(clipboard) => clipboard.read().ok(), + State::Connected(clipboard, _) => clipboard.read().ok(), State::Unavailable => None, } } @@ -43,7 +64,7 @@ impl Clipboard { /// Writes the given text contents to the [`Clipboard`]. pub fn write(&mut self, contents: String) { match &mut self.state { - State::Connected(clipboard) => match clipboard.write(contents) { + State::Connected(clipboard, _) => match clipboard.write(contents) { Ok(()) => {} Err(error) => { log::warn!("error writing to clipboard: {error}"); @@ -52,14 +73,195 @@ impl Clipboard { State::Unavailable => {} } } + + /// Reads the current content of the Primary as text. + pub fn read_primary(&self) -> Option { + match &self.state { + State::Connected(clipboard, _) => { + clipboard.read_primary().and_then(|res| res.ok()) + } + State::Unavailable => None, + } + } + + /// Writes the given text contents to the Primary. + pub fn write_primary(&mut self, contents: String) { + match &mut self.state { + State::Connected(clipboard, _) => { + match clipboard.write_primary(contents) { + Some(Ok(())) => {} + Some(Err(error)) => { + log::warn!("error writing to clipboard: {error}"); + } + None => {} //Primary not available + } + } + State::Unavailable => {} + } + } + + // + pub(crate) fn start_dnd_winit( + &self, + internal: bool, + source_surface: DndSurface, + icon_surface: Option, + content: Box, + actions: DndAction, + ) { + match &self.state { + State::Connected(clipboard, _) => { + _ = clipboard.start_dnd( + internal, + source_surface, + icon_surface, + content, + actions, + ) + } + State::Unavailable => {} + } + } } -impl crate::core::Clipboard for Clipboard { +impl crate::core::Clipboard for Clipboard { fn read(&self) -> Option { - self.read() + match &self.state { + State::Connected(clipboard, _) => clipboard.read().ok(), + State::Unavailable => None, + } } fn write(&mut self, contents: String) { - self.write(contents); + match &mut self.state { + State::Connected(clipboard, _) => _ = clipboard.write(contents), + State::Unavailable => {} + } + } + + fn read_primary(&self) -> Option { + match &self.state { + State::Connected(clipboard, _) => { + clipboard.read_primary().and_then(|res| res.ok()) + } + State::Unavailable => None, + } + } + + fn write_primary(&mut self, contents: String) { + match &mut self.state { + State::Connected(clipboard, _) => { + _ = clipboard.write_primary(contents) + } + State::Unavailable => {} + } + } + + fn read_data(&self, mimes: Vec) -> Option<(Vec, String)> { + match &self.state { + State::Connected(clipboard, _) => { + clipboard.read_raw(mimes).and_then(|res| res.ok()) + } + State::Unavailable => None, + } + } + + fn write_data( + &mut self, + contents: ClipboardStoreData< + Box, + >, + ) { + match &mut self.state { + State::Connected(clipboard, _) => { + _ = clipboard.write_data(contents) + } + State::Unavailable => {} + } + } + + fn read_primary_data( + &self, + mimes: Vec, + ) -> Option<(Vec, String)> { + match &self.state { + State::Connected(clipboard, _) => { + clipboard.read_primary_raw(mimes).and_then(|res| res.ok()) + } + State::Unavailable => None, + } + } + + fn write_primary_data( + &mut self, + contents: ClipboardStoreData< + Box, + >, + ) { + match &mut self.state { + State::Connected(clipboard, _) => { + _ = clipboard.write_primary_data(contents) + } + State::Unavailable => {} + } + } + + fn start_dnd( + &self, + internal: bool, + source_surface: Option, + icon_surface: Option>, + content: Box, + actions: DndAction, + ) { + match &self.state { + State::Connected(_, tx) => { + tx.raw.send_event(UserEventWrapper::StartDnd { + internal, + source_surface, + icon_surface, + content, + actions, + }); + } + State::Unavailable => {} + } + } + + fn register_dnd_destination( + &self, + surface: DndSurface, + rectangles: Vec, + ) { + match &self.state { + State::Connected(clipboard, _) => { + _ = clipboard.register_dnd_destination(surface, rectangles) + } + State::Unavailable => {} + } + } + + fn end_dnd(&self) { + match &self.state { + State::Connected(clipboard, _) => _ = clipboard.end_dnd(), + State::Unavailable => {} + } + } + + fn peek_dnd(&self, mime: String) -> Option<(Vec, String)> { + match &self.state { + State::Connected(clipboard, _) => clipboard + .peek_offer::(Some(Cow::Owned(mime))) + .ok() + .map(|res| (res.0, res.1)), + State::Unavailable => None, + } + } + + fn set_action(&self, action: DndAction) { + match &self.state { + State::Connected(clipboard, _) => _ = clipboard.set_action(action), + State::Unavailable => {} + } } } diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index eceb160487..7aa7186701 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -2,6 +2,9 @@ //! //! [`winit`]: https://github.com/rust-windowing/winit //! [`iced_runtime`]: https://github.com/iced-rs/iced/tree/0.10/runtime +use winit::keyboard::SmolStr; +use winit::platform::modifier_supplement::KeyEventExtModifierSupplement; + use crate::core::keyboard; use crate::core::mouse; use crate::core::touch; @@ -195,51 +198,54 @@ pub fn window_event( })) } }, - WindowEvent::KeyboardInput { - event: - winit::event::KeyEvent { - logical_key, - state, - text, - location, - .. - }, - .. - } => Some(Event::Keyboard({ - let key = key(logical_key); - let modifiers = self::modifiers(modifiers); - - let location = match location { - winit::keyboard::KeyLocation::Standard => { - keyboard::Location::Standard - } - winit::keyboard::KeyLocation::Left => keyboard::Location::Left, - winit::keyboard::KeyLocation::Right => { - keyboard::Location::Right - } - winit::keyboard::KeyLocation::Numpad => { - keyboard::Location::Numpad - } - }; - - match state { - winit::event::ElementState::Pressed => { - keyboard::Event::KeyPressed { - key, - modifiers, - location, - text, + WindowEvent::KeyboardInput { event, .. } => { + let text_with_modifiers = + event.text_with_all_modifiers().map(|t| SmolStr::new(t)); + let winit::event::KeyEvent { + logical_key, + state, + text: _text, + location, + .. + } = event; + Some(Event::Keyboard({ + let key = key(logical_key); + let modifiers = self::modifiers(modifiers); + + let location = match location { + winit::keyboard::KeyLocation::Standard => { + keyboard::Location::Standard } - } - winit::event::ElementState::Released => { - keyboard::Event::KeyReleased { - key, - modifiers, - location, + winit::keyboard::KeyLocation::Left => { + keyboard::Location::Left + } + winit::keyboard::KeyLocation::Right => { + keyboard::Location::Right + } + winit::keyboard::KeyLocation::Numpad => { + keyboard::Location::Numpad + } + }; + + match state { + winit::event::ElementState::Pressed => { + keyboard::Event::KeyPressed { + key, + modifiers, + location, + text: text_with_modifiers, + } + } + winit::event::ElementState::Released => { + keyboard::Event::KeyReleased { + key, + modifiers, + location, + } } } - } - })), + })) + } WindowEvent::ModifiersChanged(new_modifiers) => { Some(Event::Keyboard(keyboard::Event::ModifiersChanged( self::modifiers(new_modifiers.state()), @@ -815,3 +821,13 @@ pub fn icon(icon: window::Icon) -> Option { winit::window::Icon::from_rgba(pixels, size.width, size.height).ok() } + +#[cfg(feature = "a11y")] +pub(crate) fn a11y( + event: iced_accessibility::accesskit::ActionRequest, +) -> Event { + // XXX + let id = + iced_runtime::core::id::Id::from(u128::from(event.target.0) as u64); + Event::A11y(id, event) +} diff --git a/winit/src/lib.rs b/winit/src/lib.rs index 948576a28a..622e9396a2 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -22,7 +22,6 @@ missing_debug_implementations, missing_docs, unused_results, - unsafe_code, rustdoc::broken_intra_doc_links )] #![cfg_attr(docsrs, feature(doc_auto_cfg))] diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 1c45ce37e0..0eaa7daa55 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -1,19 +1,24 @@ //! Create interactive, native cross-platform applications for WGPU. +#[path = "application/drag_resize.rs"] +mod drag_resize; mod state; mod window_manager; -pub use state::State; - +use crate::application::UserEventWrapper; use crate::conversion; use crate::core; +use crate::core::mouse; use crate::core::renderer; use crate::core::widget::operation; +use crate::core::widget::Operation; use crate::core::window; +use crate::core::Clipboard as CoreClipboard; 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::OperationWrapper; use crate::multi_window::window_manager::WindowManager; use crate::runtime::command::{self, Command}; use crate::runtime::multi_window::Program; @@ -21,12 +26,31 @@ use crate::runtime::user_interface::{self, UserInterface}; use crate::runtime::Debug; use crate::style::application::StyleSheet; use crate::{Clipboard, Error, Proxy, Settings}; +use dnd::DndSurface; +use dnd::Icon; +use iced_graphics::Viewport; +use iced_runtime::futures::futures::FutureExt; +use iced_style::core::Length; +pub use state::State; +use window_clipboard::mime::ClipboardStoreData; +use winit::raw_window_handle::HasWindowHandle; +use std::any::Any; use std::collections::HashMap; use std::mem::ManuallyDrop; use std::sync::Arc; 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 @@ -107,6 +131,7 @@ where A: Application + 'static, E: Executor + 'static, C: Compositor + 'static, + A::Message: Send + 'static, A::Theme: StyleSheet, { use winit::event_loop::EventLoopBuilder; @@ -135,6 +160,7 @@ where let should_main_be_visible = settings.window.visible; let exit_on_close_request = settings.window.exit_on_close_request; + let resize_border = settings.window.resize_border; let builder = conversion::window_settings( settings.window, @@ -191,6 +217,7 @@ where &application, &mut compositor, exit_on_close_request, + resize_border, ); let (mut event_sender, event_receiver) = mpsc::unbounded(); @@ -207,6 +234,7 @@ where init_command, window_manager, should_main_be_visible, + resize_border, )); let mut context = task::Context::from_waker(task::noop_waker_ref()); @@ -308,18 +336,26 @@ 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, should_main_window_be_visible: bool, + resize_border: u32, ) where A: Application + 'static, E: Executor + 'static, C: Compositor + 'static, + A::Message: Send + 'static, A::Theme: StyleSheet, { use winit::event; @@ -333,7 +369,42 @@ async fn run_instance( main_window.raw.set_visible(true); } - let mut clipboard = Clipboard::connect(&main_window.raw); + let mut clipboard = + Clipboard::connect(&main_window.raw, Proxy::new(proxy.clone())); + + #[cfg(feature = "a11y")] + let (window_a11y_id, adapter, mut a11y_enabled) = { + 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()); + let root = NodeId(node_id); + TreeUpdate { + nodes: vec![(root, node)], + tree: Some(Tree::new(root)), + focus: root, + } + }, + proxy.clone(), + ), + false, + ) + }; let mut events = { vec![( Some(window::Id::MAIN), @@ -356,6 +427,7 @@ async fn run_instance( window::Id::MAIN, user_interface::Cache::default(), )]), + &mut clipboard, )); run_command( @@ -371,12 +443,19 @@ 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(); debug.startup_finished(); + let mut cur_dnd_surface: Option = None; + 'main: while let Some(event) = event_receiver.next().await { match event { Event::WindowCreated { @@ -390,6 +469,7 @@ async fn run_instance( &application, &mut compositor, exit_on_close_request, + resize_border, ); let logical_size = window.state.logical_size(); @@ -446,9 +526,6 @@ async fn run_instance( ), )); } - event::Event::UserEvent(message) => { - messages.push(message); - } event::Event::WindowEvent { window_id: id, event: event::WindowEvent::RedrawRequested, @@ -489,7 +566,9 @@ async fn run_instance( &mut window.renderer, window.state.theme(), &renderer::Style { + icon_color: window.state.icon_color(), text_color: window.state.text_color(), + scale_factor: window.state.scale_factor(), }, cursor, ); @@ -559,7 +638,11 @@ async fn run_instance( &mut window.renderer, window.state.theme(), &renderer::Style { + icon_color: window.state.icon_color(), text_color: window.state.text_color(), + scale_factor: window + .state + .scale_factor(), }, window.state.cursor(), ); @@ -634,15 +717,32 @@ async fn run_instance( continue; }; + // Initiates a drag resize window state when found. + if let Some(func) = + window.drag_resize_window_func.as_mut() + { + if func(&window.raw, &window_event) { + continue; + } + } + if matches!( window_event, winit::event::WindowEvent::CloseRequested ) && window.exit_on_close_request { - let _ = window_manager.remove(id); + let w = window_manager.remove(id); let _ = user_interfaces.remove(&id); let _ = ui_caches.remove(&id); - + // XXX Empty rectangle list un-registers the window + if let Some(w) = w { + clipboard.register_dnd_destination( + DndSurface(Arc::new(Box::new( + w.raw.clone(), + ))), + Vec::new(), + ); + } events.push(( None, core::Event::Window(id, window::Event::Closed), @@ -768,8 +868,411 @@ async fn run_instance( &mut debug, &mut window_manager, cached_interfaces, + &mut clipboard, )); } + + debug.draw_started(); + + for (id, window) in window_manager.iter_mut() { + // TODO: Avoid redrawing all the time by forcing widgets to + // request redraws on state changes + // + // Then, we can use the `interface_state` here to decide if a redraw + // is needed right away, or simply wait until a specific time. + let redraw_event = core::Event::Window( + id, + window::Event::RedrawRequested(Instant::now()), + ); + + let cursor = window.state.cursor(); + + let ui = user_interfaces + .get_mut(&id) + .expect("Get user interface"); + + let (ui_state, _) = ui.update( + &[redraw_event.clone()], + cursor, + &mut window.renderer, + &mut clipboard, + &mut messages, + ); + + let new_mouse_interaction = { + let state = &window.state; + + ui.draw( + &mut window.renderer, + state.theme(), + &renderer::Style { + icon_color: state.icon_color(), + text_color: state.text_color(), + scale_factor: state.scale_factor(), + }, + cursor, + ) + }; + + if new_mouse_interaction != window.mouse_interaction + { + window.raw.set_cursor_icon( + conversion::mouse_interaction( + new_mouse_interaction, + ), + ); + + window.mouse_interaction = + new_mouse_interaction; + } + + // TODO once widgets can request to be redrawn, we can avoid always requesting a + // redraw + window.raw.request_redraw(); + + runtime.broadcast( + redraw_event.clone(), + core::event::Status::Ignored, + ); + + let _ = control_sender.start_send( + Control::ChangeFlow(match ui_state { + user_interface::State::Updated { + redraw_request: Some(redraw_request), + } => match redraw_request { + window::RedrawRequest::NextFrame => { + ControlFlow::Poll + } + window::RedrawRequest::At(at) => { + ControlFlow::WaitUntil(at) + } + }, + _ => ControlFlow::Wait, + }), + ); + } + + debug.draw_finished(); + } + event::Event::PlatformSpecific( + event::PlatformSpecific::MacOS( + event::MacOS::ReceivedUrl(url), + ), + ) => { + use crate::core::event; + + events.push(( + None, + event::Event::PlatformSpecific( + event::PlatformSpecific::MacOS( + event::MacOS::ReceivedUrl(url), + ), + ), + )); + } + event::Event::UserEvent(message) => { + match message { + UserEventWrapper::Message(m) => messages.push(m), + #[cfg(feature = "a11y")] + UserEventWrapper::A11y(request) => { + match request.request.action { + iced_accessibility::accesskit::Action::Focus => { + // TODO send a command for this + } + _ => {} + } + events.push(( + None, + conversion::a11y(request.request), + )); + } + #[cfg(feature = "a11y")] + UserEventWrapper::A11yEnabled => { + a11y_enabled = true + } + UserEventWrapper::StartDnd { + internal, + source_surface, + icon_surface, + content, + actions, + } => { + let Some(window_id) = + source_surface.and_then(|source| { + match source { + core::clipboard::DndSource::Surface( + s, + ) => Some(s), + core::clipboard::DndSource::Widget( + w, + ) => { + // search windows for widget with operation + user_interfaces.iter_mut().find_map( + |(ui_id, ui)| { + let mut current_operation = + Some(Box::new(OperationWrapper::Id(Box::new( + operation::search_id::search_id(w.clone()), + )))); + let Some(ui_renderer) = window_manager.get_mut(ui_id.clone()).map(|w| &w.renderer) else { + return None; + }; + while let Some(mut operation) = current_operation.take() + { + ui + .operate(&ui_renderer, operation.as_mut()); + match operation.finish() { + operation::Outcome::None => { + } + operation::Outcome::Some(message) => { + match message { + operation::OperationOutputWrapper::Message(_) => { + unimplemented!(); + } + operation::OperationOutputWrapper::Id(_) => { + return Some(ui_id.clone()); + }, + } + } + operation::Outcome::Chain(next) => { + current_operation = Some(Box::new(OperationWrapper::Wrapper(next))); + } + } + } + None + }, + ) + }, + } + }) + else { + eprintln!("No source surface"); + continue; + }; + + let Some(window) = + window_manager.get_mut(window_id) + else { + eprintln!("No window"); + continue; + }; + + let state = &window.state; + let icon_surface = icon_surface + .map(|i| { + let i: Box = i; + i + }) + .and_then(|i| { + i.downcast::, + core::widget::tree::State, + )>>( + ) + .ok() + }) + .map(|e| { + let mut renderer = + compositor.create_renderer(); + + let e = Arc::into_inner(*e).unwrap(); + let (mut e, widget_state) = e; + let lim = core::layout::Limits::new( + Size::new(1., 1.), + Size::new( + state + .viewport() + .physical_width() + as f32, + state + .viewport() + .physical_height() + as f32, + ), + ); + + let mut tree = core::widget::Tree { + id: e.as_widget().id(), + tag: e.as_widget().tag(), + state: widget_state, + children: e.as_widget().children(), + }; + + let size = e + .as_widget() + .layout(&mut tree, &renderer, &lim); + e.as_widget_mut().diff(&mut tree); + + let size = lim.resolve( + Length::Shrink, + Length::Shrink, + size.size(), + ); + let mut surface = compositor + .create_surface( + window.raw.clone(), + size.width.ceil() as u32, + size.height.ceil() as u32, + ); + let viewport = + Viewport::with_logical_size( + size, + state.viewport().scale_factor(), + ); + let mut ui = UserInterface::build( + e, + size, + user_interface::Cache::default(), + &mut renderer, + ); + _ = ui.draw( + &mut renderer, + state.theme(), + &renderer::Style { + icon_color: state.icon_color(), + text_color: state.text_color(), + scale_factor: state + .scale_factor(), + }, + Default::default(), + ); + let mut bytes = compositor.screenshot( + &mut renderer, + &mut surface, + &viewport, + core::Color::TRANSPARENT, + &debug.overlay(), + ); + for pix in bytes.chunks_exact_mut(4) { + // rgba -> argb little endian + pix.swap(0, 2); + } + Icon::Buffer { + data: Arc::new(bytes), + width: viewport.physical_width(), + height: viewport.physical_height(), + transparent: true, + } + }); + + clipboard.start_dnd_winit( + internal, + DndSurface(Arc::new(Box::new( + window.raw.clone(), + ))), + icon_surface, + content, + actions, + ); + } + UserEventWrapper::Dnd(e) => match &e { + dnd::DndEvent::Offer( + _, + dnd::OfferEvent::Leave, + ) => { + events.push(( + cur_dnd_surface, + core::Event::Dnd(e), + )); + cur_dnd_surface = None; + } + dnd::DndEvent::Offer( + _, + dnd::OfferEvent::Enter { surface, .. }, + ) => { + let window_handle = + surface.0.window_handle().ok(); + let window_id = window_manager + .iter_mut() + .find_map(|(id, window)| { + if window + .raw + .window_handle() + .ok() + .zip(window_handle) + .map(|(a, b)| a == b) + .unwrap_or_default() + { + Some(id) + } else { + None + } + }); + + cur_dnd_surface = window_id; + events.push(( + cur_dnd_surface, + core::Event::Dnd(e), + )); + } + dnd::DndEvent::Offer(..) => { + events.push(( + cur_dnd_surface, + core::Event::Dnd(e), + )); + } + dnd::DndEvent::Source(_) => { + events.push((None, core::Event::Dnd(e))) + } + }, + }; + } + event::Event::WindowEvent { + event: window_event, + window_id, + } => { + let Some((id, window)) = + window_manager.get_mut_alias(window_id) + else { + continue; + }; + + if matches!( + window_event, + winit::event::WindowEvent::CloseRequested + ) { + let w = window_manager.remove(id); + let _ = user_interfaces.remove(&id); + let _ = ui_caches.remove(&id); + if let Some(w) = w.as_ref() { + clipboard.register_dnd_destination( + DndSurface(Arc::new(Box::new( + w.raw.clone(), + ))), + Vec::new(), + ); + } + + events.push(( + None, + core::Event::Window(id, window::Event::Closed), + )); + + if window_manager.is_empty() + && w.is_some_and(|w| w.exit_on_close_request) + { + break 'main; + } + } else { + window.state.update( + &window.raw, + &window_event, + &mut debug, + ); + + if let Some(event) = conversion::window_event( + id, + window_event, + window.state.scale_factor(), + window.state.modifiers(), + ) { + events.push((Some(id), event)); + } + } } _ => {} } @@ -805,19 +1308,24 @@ where /// Updates a multi-window [`Application`] by feeding it messages, spawning any /// resulting [`Command`], and tracking its [`Subscription`]. -fn update( +fn update( application: &mut A, compositor: &mut C, - runtime: &mut Runtime, A::Message>, - clipboard: &mut Clipboard, + 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, ui_caches: &mut HashMap, ) where C: Compositor + 'static, + A::Message: Send + 'static, A::Theme: StyleSheet, { for message in messages.drain(..) { @@ -841,8 +1349,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 +1361,15 @@ fn run_command( application: &A, compositor: &mut C, command: Command, - runtime: &mut Runtime, A::Message>, - clipboard: &mut Clipboard, + 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, @@ -861,6 +1377,7 @@ fn run_command( A: Application, E: Executor, C: Compositor + 'static, + A::Message: Send + 'static, A::Theme: StyleSheet, { use crate::runtime::clipboard; @@ -870,22 +1387,46 @@ 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) => { clipboard.write(contents); } + clipboard::Action::WriteData(contents) => { + clipboard.write_data(ClipboardStoreData(contents)) + } + clipboard::Action::ReadData(allowed, to_msg) => { + let contents = clipboard.read_data(allowed); + let message = to_msg(contents); + _ = proxy.send_event(UserEventWrapper::Message(message)); + } + clipboard::Action::ReadPrimary(s_to_msg) => { + let contents = clipboard.read_primary(); + let message = s_to_msg(contents); + _ = proxy.send_event(UserEventWrapper::Message(message)); + } + clipboard::Action::WritePrimary(content) => { + clipboard.write_primary(content) + } + clipboard::Action::WritePrimaryData(content) => { + clipboard.write_primary_data(ClipboardStoreData(content)) + } + clipboard::Action::ReadPrimaryData(a, to_msg) => { + let contents = clipboard.read_primary_data(a); + let message = to_msg(contents); + _ = proxy.send_event(UserEventWrapper::Message(message)); + } }, command::Action::Window(action) => match action { window::Action::Spawn(id, settings) => { @@ -901,10 +1442,18 @@ fn run_command( .expect("Send control action"); } window::Action::Close(id) => { - let _ = window_manager.remove(id); + let w = window_manager.remove(id); let _ = ui_caches.remove(&id); + if let Some(w) = w.as_ref() { + clipboard.register_dnd_destination( + DndSurface(Arc::new(Box::new(w.raw.clone()))), + Vec::new(), + ); + } - if window_manager.is_empty() { + if window_manager.is_empty() + && w.is_some_and(|w| w.exit_on_close_request) + { control_sender .start_send(Control::Exit) .expect("Send control action"); @@ -933,9 +1482,8 @@ fn run_command( .to_logical(window.raw.scale_factor()); proxy - .send_event(callback(Size::new( - size.width, - size.height, + .send_event(UserEventWrapper::Message(callback( + Size::new(size.width, size.height), ))) .expect("Send message to event loop"); } @@ -943,7 +1491,9 @@ fn run_command( window::Action::FetchMaximized(id, callback) => { if let Some(window) = window_manager.get_mut(id) { proxy - .send_event(callback(window.raw.is_maximized())) + .send_event(UserEventWrapper::Message(callback( + window.raw.is_maximized(), + ))) .expect("Send message to event loop"); } } @@ -955,7 +1505,9 @@ fn run_command( window::Action::FetchMinimized(id, callback) => { if let Some(window) = window_manager.get_mut(id) { proxy - .send_event(callback(window.raw.is_minimized())) + .send_event(UserEventWrapper::Message(callback( + window.raw.is_minimized(), + ))) .expect("Send message to event loop"); } } @@ -997,7 +1549,7 @@ fn run_command( }; proxy - .send_event(tag(mode)) + .send_event(UserEventWrapper::Message(tag(mode))) .expect("Event loop doesn't exist."); } } @@ -1030,10 +1582,27 @@ fn run_command( .set_window_level(conversion::window_level(level)); } } + window::Action::ShowWindowMenu(id) => { + if let Some(window) = window_manager.get_mut(id) { + if let mouse::Cursor::Available(point) = + window.state.cursor() + { + window.raw.show_window_menu( + winit::dpi::LogicalPosition { + x: point.x, + y: point.y, + }, + ); + } + } + } window::Action::FetchId(id, tag) => { if let Some(window) = window_manager.get_mut(id) { proxy - .send_event(tag(window.raw.id().into())) + .send_event(UserEventWrapper::Message(tag(window + .raw + .id() + .into()))) .expect("Event loop doesn't exist."); } } @@ -1048,9 +1617,11 @@ fn run_command( ); proxy - .send_event(tag(window::Screenshot::new( - bytes, - window.state.physical_size(), + .send_event(UserEventWrapper::Message(tag( + window::Screenshot::new( + bytes, + window.state.physical_size(), + ), ))) .expect("Event loop doesn't exist."); } @@ -1070,25 +1641,24 @@ 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, window_manager, std::mem::take(ui_caches), + clipboard, ); - 'operate: while let Some(mut operation) = - current_operation.take() - { + while let Some(mut operation) = current_operation.take() { for (id, ui) in uis.iter_mut() { if let Some(window) = window_manager.get_mut(*id) { ui.operate(&window.renderer, operation.as_mut()); @@ -1096,15 +1666,25 @@ fn run_command( match operation.finish() { operation::Outcome::None => {} operation::Outcome::Some(message) => { + match message { + operation::OperationOutputWrapper::Message( + m, + ) => { proxy - .send_event(message) - .expect("Event loop doesn't exist."); - - // operation completed, don't need to try to operate on rest of UIs - break 'operate; + .send_event( + UserEventWrapper::Message(m), + ) + .expect("Send message to event loop"); + } + operation::OperationOutputWrapper::Id(_) => { + // TODO ASHLEY should not ever happen, should this panic!()? + } + } } operation::Outcome::Chain(next) => { - current_operation = Some(next); + current_operation = Some(Box::new( + OperationWrapper::Wrapper(next), + )); } } } @@ -1124,9 +1704,46 @@ 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."); + } + command::Action::Dnd(a) => match a { + iced_runtime::dnd::DndAction::RegisterDndDestination { + surface, + rectangles, + } => { + clipboard.register_dnd_destination(surface, rectangles); + } + iced_runtime::dnd::DndAction::StartDnd { + internal, + source_surface, + icon_surface, + content, + actions, + } => clipboard.start_dnd( + internal, + source_surface, + icon_surface, + content, + actions, + ), + iced_runtime::dnd::DndAction::EndDnd => { + clipboard.end_dnd(); + } + iced_runtime::dnd::DndAction::PeekDnd(m, to_msg) => { + let data = clipboard.peek_dnd(m); + let message = to_msg(data); + proxy + .send_event(UserEventWrapper::Message(message)) + .expect("Send message to event loop"); + } + iced_runtime::dnd::DndAction::SetAction(a) => { + clipboard.set_action(a); + } + }, } } } @@ -1137,6 +1754,7 @@ pub fn build_user_interfaces<'a, A: Application, C: Compositor>( debug: &mut Debug, window_manager: &mut WindowManager, mut cached_user_interfaces: HashMap, + clipboard: &mut Clipboard, ) -> HashMap> where A::Theme: StyleSheet, @@ -1146,18 +1764,33 @@ where .drain() .filter_map(|(id, cache)| { let window = window_manager.get_mut(id)?; - - Some(( + let interface = build_user_interface( + application, + cache, + &mut window.renderer, + window.state.logical_size(), + debug, id, - build_user_interface( - application, - cache, - &mut window.renderer, - window.state.logical_size(), - debug, - id, - ), - )) + ); + + let dnd_rectangles = interface.dnd_rectangles( + window.prev_dnd_destination_rectangles_count, + &window.renderer, + ); + let new_dnd_rectangles_count = dnd_rectangles.as_ref().len(); + if new_dnd_rectangles_count > 0 + || window.prev_dnd_destination_rectangles_count > 0 + { + clipboard.register_dnd_destination( + DndSurface(Arc::new(Box::new(window.raw.clone()))), + dnd_rectangles.into_rectangles(), + ); + } + + window.prev_dnd_destination_rectangles_count = + new_dnd_rectangles_count; + + Some((id, interface)) }) .collect() } diff --git a/winit/src/multi_window/state.rs b/winit/src/multi_window/state.rs index 2e97a13d96..9f65c90a1c 100644 --- a/winit/src/multi_window/state.rs +++ b/winit/src/multi_window/state.rs @@ -137,6 +137,11 @@ where self.appearance.text_color } + /// Returns the current icon [`Color`] of the [`State`]. + pub fn icon_color(&self) -> Color { + self.appearance.icon_color + } + /// Processes the provided window event and updates the [`State`] accordingly. pub fn update( &mut self, diff --git a/winit/src/multi_window/window_manager.rs b/winit/src/multi_window/window_manager.rs index 23f3c0ba0b..0a67a01580 100644 --- a/winit/src/multi_window/window_manager.rs +++ b/winit/src/multi_window/window_manager.rs @@ -39,6 +39,7 @@ where application: &A, compositor: &mut C, exit_on_close_request: bool, + resize_border: u32, ) -> &mut Window { let state = State::new(application, id, &window); let viewport_version = state.viewport_version(); @@ -52,6 +53,11 @@ where let _ = self.aliases.insert(window.id(), id); + let drag_resize_window_func = super::drag_resize::event_func( + &window, + resize_border as f64 * window.scale_factor(), + ); + let _ = self.entries.insert( id, Window { @@ -59,9 +65,11 @@ where state, viewport_version, exit_on_close_request, + drag_resize_window_func, surface, renderer, mouse_interaction: mouse::Interaction::Idle, + prev_dnd_destination_rectangles_count: 0, }, ); @@ -127,6 +135,15 @@ where pub state: State, pub viewport_version: u64, pub exit_on_close_request: bool, + pub drag_resize_window_func: Option< + Box< + dyn FnMut( + &winit::window::Window, + &winit::event::WindowEvent, + ) -> bool, + >, + >, + pub prev_dnd_destination_rectangles_count: usize, pub mouse_interaction: mouse::Interaction, pub surface: C::Surface, pub renderer: A::Renderer, diff --git a/winit/src/proxy.rs b/winit/src/proxy.rs index 1d6c48bb25..19f0502a26 100644 --- a/winit/src/proxy.rs +++ b/winit/src/proxy.rs @@ -1,14 +1,19 @@ -use crate::futures::futures::{ - channel::mpsc, - task::{Context, Poll}, - Sink, +use dnd::{DndEvent, DndSurface}; + +use crate::{ + application::UserEventWrapper, + futures::futures::{ + channel::mpsc, + task::{Context, Poll}, + Sink, + }, }; use std::pin::Pin; /// An event loop proxy that implements `Sink`. #[derive(Debug)] pub struct Proxy { - raw: winit::event_loop::EventLoopProxy, + pub(crate) raw: winit::event_loop::EventLoopProxy, } impl Clone for Proxy { @@ -59,3 +64,19 @@ impl Sink for Proxy { Poll::Ready(Ok(())) } } + +impl dnd::Sender for Proxy> { + fn send( + &self, + event: DndEvent, + ) -> Result<(), std::sync::mpsc::SendError>> { + self.raw + .send_event(UserEventWrapper::Dnd(event)) + .map_err(|_err| { + std::sync::mpsc::SendError(DndEvent::Offer( + None, + dnd::OfferEvent::Leave, + )) + }) + } +}