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

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

LogicalPosition

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

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

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

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

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

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

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

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

{ + fn from(p: mint::Point2

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

{ + fn from(p: LogicalPosition

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

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

PhysicalPosition

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

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

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

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

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

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

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

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

{ + fn from(p: mint::Point2

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

{ + fn from(p: PhysicalPosition

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

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

LogicalSize

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

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

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

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

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

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

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

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

{ + fn from(v: mint::Vector2

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

{ + fn from(s: LogicalSize

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

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

PhysicalSize

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

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

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

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

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

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

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

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

{ + fn from(v: mint::Vector2

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

{ + fn from(s: PhysicalSize

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

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

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

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

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

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

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

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

) -> Position { + Position::Logical(position.cast()) + } +} diff --git a/sctk/src/error.rs b/sctk/src/error.rs new file mode 100644 index 0000000000..807a8f84f6 --- /dev/null +++ b/sctk/src/error.rs @@ -0,0 +1,23 @@ +use iced_futures::futures; + +/// An error that occurred while running an application. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// The futures executor could not be created. + #[error("the futures executor could not be created")] + ExecutorCreationFailed(futures::io::Error), + + /// The application window could not be created. + #[error("the application window could not be created")] + WindowCreationFailed(Box), + + /// The application graphics context could not be created. + #[error("the application graphics context could not be created")] + GraphicsCreationFailed(iced_graphics::Error), +} + +impl From for Error { + fn from(error: iced_graphics::Error) -> Error { + Error::GraphicsCreationFailed(error) + } +} diff --git a/sctk/src/event_loop/adapter.rs b/sctk/src/event_loop/adapter.rs new file mode 100644 index 0000000000..a185ad9172 --- /dev/null +++ b/sctk/src/event_loop/adapter.rs @@ -0,0 +1,34 @@ +use crate::sctk_event::ActionRequestEvent; +use iced_accessibility::{accesskit, accesskit_unix}; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::Proxy; +use std::{ + num::NonZeroU128, + sync::{Arc, Mutex}, +}; + +pub enum A11yWrapper { + Enabled, + Event(ActionRequestEvent), +} + +pub struct IcedSctkAdapter { + pub(crate) id: NonZeroU128, + pub(crate) adapter: accesskit_unix::Adapter, +} + +pub struct IcedSctkActionHandler { + pub(crate) wl_surface: WlSurface, + pub(crate) event_list: Arc>>, +} +impl accesskit::ActionHandler for IcedSctkActionHandler { + fn do_action(&self, request: accesskit::ActionRequest) { + let mut event_list = self.event_list.lock().unwrap(); + event_list.push(A11yWrapper::Event( + crate::sctk_event::ActionRequestEvent { + request, + surface_id: self.wl_surface.id(), + }, + )); + } +} diff --git a/sctk/src/event_loop/control_flow.rs b/sctk/src/event_loop/control_flow.rs new file mode 100644 index 0000000000..bc920ed478 --- /dev/null +++ b/sctk/src/event_loop/control_flow.rs @@ -0,0 +1,56 @@ +/// Set by the user callback given to the [`EventLoop::run`] method. +/// +/// Indicates the desired behavior of the event loop after [`Event::RedrawEventsCleared`] is emitted. +/// +/// Defaults to [`Poll`]. +/// +/// ## Persistency +/// +/// Almost every change is persistent between multiple calls to the event loop closure within a +/// given run loop. The only exception to this is [`ExitWithCode`] which, once set, cannot be unset. +/// Changes are **not** persistent between multiple calls to `run_return` - issuing a new call will +/// reset the control flow to [`Poll`]. +/// +/// [`ExitWithCode`]: Self::ExitWithCode +/// [`Poll`]: Self::Poll +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum ControlFlow { + /// When the current loop iteration finishes, immediately begin a new iteration regardless of + /// whether or not new events are available to process. + /// + /// ## Platform-specific + /// + /// - **Web:** Events are queued and usually sent when `requestAnimationFrame` fires but sometimes + /// the events in the queue may be sent before the next `requestAnimationFrame` callback, for + /// example when the scaling of the page has changed. This should be treated as an implementation + /// detail which should not be relied on. + Poll, + /// When the current loop iteration finishes, suspend the thread until another event arrives. + Wait, + /// When the current loop iteration finishes, suspend the thread until either another event + /// arrives or the given time is reached. + /// + /// Useful for implementing efficient timers. Applications which want to render at the display's + /// native refresh rate should instead use [`Poll`] and the VSync functionality of a graphics API + /// to reduce odds of missed frames. + /// + /// [`Poll`]: Self::Poll + WaitUntil(std::time::Instant), + /// Send a [`LoopDestroyed`] event and stop the event loop. This variant is *sticky* - once set, + /// `control_flow` cannot be changed from `ExitWithCode`, and any future attempts to do so will + /// result in the `control_flow` parameter being reset to `ExitWithCode`. + /// + /// The contained number will be used as exit code. The [`Exit`] constant is a shortcut for this + /// with exit code 0. + /// + /// ## Platform-specific + /// + /// - **Android / iOS / WASM:** The supplied exit code is unused. + /// - **Unix:** On most Unix-like platforms, only the 8 least significant bits will be used, + /// which can cause surprises with negative exit values (`-42` would end up as `214`). See + /// [`std::process::exit`]. + /// + /// [`LoopDestroyed`]: Event::LoopDestroyed + /// [`Exit`]: ControlFlow::Exit + ExitWithCode(i32), +} diff --git a/sctk/src/event_loop/mod.rs b/sctk/src/event_loop/mod.rs new file mode 100644 index 0000000000..46f33cd702 --- /dev/null +++ b/sctk/src/event_loop/mod.rs @@ -0,0 +1,1373 @@ +#[cfg(feature = "a11y")] +pub mod adapter; +pub mod control_flow; +pub mod proxy; +pub mod state; + +#[cfg(feature = "a11y")] +use crate::application::SurfaceIdWrapper; +use crate::{ + application::Event, + conversion, + dpi::LogicalSize, + handlers::{ + activation::IcedRequestData, + wp_fractional_scaling::FractionalScalingManager, + wp_viewporter::ViewporterState, + }, + sctk_event::{ + DndOfferEvent, IcedSctkEvent, LayerSurfaceEventVariant, + PopupEventVariant, SctkEvent, StartCause, WindowEventVariant, + }, + settings, +}; +use iced_futures::core::window::Mode; +use iced_runtime::command::platform_specific::{ + self, + wayland::{ + data_device::DndIcon, layer_surface::SctkLayerSurfaceSettings, + window::SctkWindowSettings, + }, +}; +use sctk::{ + activation::{ActivationState, RequestData}, + compositor::CompositorState, + data_device_manager::DataDeviceManagerState, + output::OutputState, + reexports::{ + calloop::{self, EventLoop, PostAction}, + client::{ + globals::registry_queue_init, protocol::wl_surface::WlSurface, + ConnectError, Connection, DispatchError, Proxy, + }, + }, + registry::RegistryState, + seat::SeatState, + session_lock::SessionLockState, + shell::{wlr_layer::LayerShell, xdg::XdgShell, WaylandSurface}, + shm::Shm, +}; +use sctk::{ + data_device_manager::data_source::DragSource, + reexports::calloop_wayland_source::WaylandSource, +}; +#[cfg(feature = "a11y")] +use std::sync::{Arc, Mutex}; +use std::{ + collections::HashMap, + fmt::Debug, + io::{BufRead, BufReader}, + num::NonZeroU32, + time::{Duration, Instant}, +}; +use tracing::error; +use wayland_backend::client::WaylandError; + +use self::{ + control_flow::ControlFlow, + state::{Dnd, LayerSurfaceCreationError, SctkState}, +}; + +#[derive(Debug, Default, Clone, Copy)] +pub struct Features { + // TODO +} + +pub struct SctkEventLoop { + // TODO after merged + // pub data_device_manager_state: DataDeviceManagerState, + pub(crate) event_loop: EventLoop<'static, SctkState>, + pub(crate) wayland_dispatcher: + calloop::Dispatcher<'static, WaylandSource>, SctkState>, + pub(crate) _features: Features, + /// A proxy to wake up event loop. + pub event_loop_awakener: calloop::ping::Ping, + /// A sender for submitting user events in the event loop + pub user_events_sender: calloop::channel::Sender>, + pub(crate) state: SctkState, + + #[cfg(feature = "a11y")] + pub(crate) a11y_events: Arc>>, +} + +impl SctkEventLoop +where + T: 'static + Debug, +{ + pub(crate) fn new( + _settings: &settings::Settings, + ) -> Result { + let connection = Connection::connect_to_env()?; + let _display = connection.display(); + let (globals, event_queue) = registry_queue_init(&connection).unwrap(); + let event_loop = calloop::EventLoop::>::try_new().unwrap(); + let loop_handle = event_loop.handle(); + + let qh = event_queue.handle(); + let registry_state = RegistryState::new(&globals); + + let (ping, ping_source) = calloop::ping::make_ping().unwrap(); + // TODO + loop_handle + .insert_source(ping_source, |_, _, _state| { + // Drain events here as well to account for application doing batch event processing + // on RedrawEventsCleared. + // shim::handle_window_requests(state); + }) + .unwrap(); + let (user_events_sender, user_events_channel) = + calloop::channel::channel(); + + loop_handle + .insert_source(user_events_channel, |event, _, state| match event { + calloop::channel::Event::Msg(e) => { + state.pending_user_events.push(e); + } + calloop::channel::Event::Closed => {} + }) + .unwrap(); + let wayland_source = + WaylandSource::new(connection.clone(), event_queue); + + let wayland_dispatcher = calloop::Dispatcher::new( + wayland_source, + |_, queue, winit_state| queue.dispatch_pending(winit_state), + ); + + let _wayland_source_dispatcher = event_loop + .handle() + .register_dispatcher(wayland_dispatcher.clone()) + .unwrap(); + + let (viewporter_state, fractional_scaling_manager) = + match FractionalScalingManager::new(&globals, &qh) { + Ok(m) => { + let viewporter_state = + match ViewporterState::new(&globals, &qh) { + Ok(s) => Some(s), + Err(e) => { + error!( + "Failed to initialize viewporter: {}", + e + ); + None + } + }; + (viewporter_state, Some(m)) + } + Err(e) => { + error!( + "Failed to initialize fractional scaling manager: {}", + e + ); + (None, None) + } + }; + + Ok(Self { + event_loop, + wayland_dispatcher, + state: SctkState { + connection, + registry_state, + seat_state: SeatState::new(&globals, &qh), + output_state: OutputState::new(&globals, &qh), + compositor_state: CompositorState::bind(&globals, &qh) + .expect("wl_compositor is not available"), + shm_state: Shm::bind(&globals, &qh) + .expect("wl_shm is not available"), + xdg_shell_state: XdgShell::bind(&globals, &qh) + .expect("xdg shell is not available"), + layer_shell: LayerShell::bind(&globals, &qh).ok(), + data_device_manager_state: DataDeviceManagerState::bind( + &globals, &qh, + ) + .expect("data device manager is not available"), + activation_state: ActivationState::bind(&globals, &qh).ok(), + session_lock_state: SessionLockState::new(&globals, &qh), + session_lock: None, + + queue_handle: qh, + loop_handle, + + _cursor_surface: None, + _multipool: None, + outputs: Vec::new(), + seats: Vec::new(), + windows: Vec::new(), + layer_surfaces: Vec::new(), + popups: Vec::new(), + lock_surfaces: Vec::new(), + dnd_source: None, + _kbd_focus: None, + sctk_events: Vec::new(), + frame_events: Vec::new(), + pending_user_events: Vec::new(), + token_ctr: 0, + _accept_counter: 0, + dnd_offer: None, + fractional_scaling_manager, + viewporter_state, + compositor_updates: Default::default(), + }, + _features: Default::default(), + event_loop_awakener: ping, + user_events_sender, + #[cfg(feature = "a11y")] + a11y_events: Arc::new(Mutex::new(Vec::new())), + }) + } + + pub fn proxy(&self) -> proxy::Proxy> { + proxy::Proxy::new(self.user_events_sender.clone()) + } + + pub fn get_layer_surface( + &mut self, + layer_surface: SctkLayerSurfaceSettings, + ) -> Result<(iced_runtime::window::Id, WlSurface), LayerSurfaceCreationError> + { + self.state.get_layer_surface(layer_surface) + } + + pub fn get_window( + &mut self, + settings: SctkWindowSettings, + ) -> (iced_runtime::window::Id, WlSurface) { + self.state.get_window(settings) + } + + // TODO Ashley provide users a reasonable method of setting the role for the surface + #[cfg(feature = "a11y")] + pub fn init_a11y_adapter( + &mut self, + surface: &WlSurface, + app_id: Option, + surface_title: Option, + role: iced_accessibility::accesskit::Role, + ) -> adapter::IcedSctkAdapter { + use iced_accessibility::{ + accesskit::{ + Node, NodeBuilder, NodeClassSet, NodeId, Role, Tree, TreeUpdate, + }, + accesskit_unix::Adapter, + window_node_id, + }; + let node_id = window_node_id(); + let event_list = self.a11y_events.clone(); + adapter::IcedSctkAdapter { + adapter: Adapter::new( + app_id.unwrap_or_else(|| String::from("None")), + "Iced".to_string(), + env!("CARGO_PKG_VERSION").to_string(), + move || { + event_list + .lock() + .unwrap() + .push(adapter::A11yWrapper::Enabled); + let mut node = NodeBuilder::new(Role::Window); + if let Some(name) = surface_title { + node.set_name(name); + } + let node = node.build(&mut NodeClassSet::lock_global()); + TreeUpdate { + nodes: vec![(NodeId(node_id), node)], + tree: Some(Tree::new(NodeId(node_id))), + focus: None, + } + }, + Box::new(adapter::IcedSctkActionHandler { + wl_surface: surface.clone(), + event_list: self.a11y_events.clone(), + }), + ) + .unwrap(), + id: node_id, + } + } + + pub fn run_return(&mut self, mut callback: F) -> i32 + where + F: FnMut(IcedSctkEvent, &SctkState, &mut ControlFlow), + { + let mut control_flow = ControlFlow::Poll; + + callback( + IcedSctkEvent::NewEvents(StartCause::Init), + &self.state, + &mut control_flow, + ); + + let mut sctk_event_sink_back_buffer = Vec::new(); + let mut compositor_event_back_buffer = Vec::new(); + let mut frame_event_back_buffer = Vec::new(); + + // NOTE We break on errors from dispatches, since if we've got protocol error + // libwayland-client/wayland-rs will inform us anyway, but crashing downstream is not + // really an option. Instead we inform that the event loop got destroyed. We may + // communicate an error that something was terminated, but winit doesn't provide us + // with an API to do that via some event. + // Still, we set the exit code to the error's OS error code, or to 1 if not possible. + let exit_code = loop { + // Send pending events to the server. + match self.wayland_dispatcher.as_source_ref().connection().flush() { + Ok(_) => {} + Err(error) => { + break match error { + WaylandError::Io(err) => err.raw_os_error(), + WaylandError::Protocol(_) => None, + } + .unwrap_or(1) + } + } + + // During the run of the user callback, some other code monitoring and reading the + // Wayland socket may have been run (mesa for example does this with vsync), if that + // is the case, some events may have been enqueued in our event queue. + // + // If some messages are there, the event loop needs to behave as if it was instantly + // woken up by messages arriving from the Wayland socket, to avoid delaying the + // dispatch of these events until we're woken up again. + let instant_wakeup = { + let mut wayland_source = + self.wayland_dispatcher.as_source_mut(); + let queue = wayland_source.queue(); + match queue.dispatch_pending(&mut self.state) { + Ok(dispatched) => dispatched > 0, + // TODO better error handling + Err(error) => { + break match error { + DispatchError::BadMessage { .. } => None, + DispatchError::Backend(err) => match err { + WaylandError::Io(err) => err.raw_os_error(), + WaylandError::Protocol(_) => None, + }, + } + .unwrap_or(1) + } + } + }; + + match control_flow { + ControlFlow::ExitWithCode(code) => break code, + ControlFlow::Poll => { + // Non-blocking dispatch. + let timeout = Duration::from_millis(0); + if let Err(error) = + self.event_loop.dispatch(Some(timeout), &mut self.state) + { + break raw_os_err(error); + } + + callback( + IcedSctkEvent::NewEvents(StartCause::Poll), + &self.state, + &mut control_flow, + ); + } + ControlFlow::Wait => { + let timeout = if instant_wakeup { + Some(Duration::from_millis(0)) + } else { + None + }; + + if let Err(error) = + self.event_loop.dispatch(timeout, &mut self.state) + { + break raw_os_err(error); + } + + callback( + IcedSctkEvent::NewEvents(StartCause::WaitCancelled { + start: Instant::now(), + requested_resume: None, + }), + &self.state, + &mut control_flow, + ); + } + ControlFlow::WaitUntil(deadline) => { + let start = Instant::now(); + + // Compute the amount of time we'll block for. + let duration = if deadline > start && !instant_wakeup { + deadline - start + } else { + Duration::from_millis(0) + }; + + if let Err(error) = self + .event_loop + .dispatch(Some(duration), &mut self.state) + { + break raw_os_err(error); + } + + let now = Instant::now(); + + if now < deadline { + callback( + IcedSctkEvent::NewEvents( + StartCause::WaitCancelled { + start, + requested_resume: Some(deadline), + }, + ), + &self.state, + &mut control_flow, + ) + } else { + callback( + IcedSctkEvent::NewEvents( + StartCause::ResumeTimeReached { + start, + requested_resume: deadline, + }, + ), + &self.state, + &mut control_flow, + ) + } + } + } + + // handle compositor events + std::mem::swap( + &mut compositor_event_back_buffer, + &mut self.state.compositor_updates, + ); + + for event in compositor_event_back_buffer.drain(..) { + let forward_event = match &event { + SctkEvent::LayerSurfaceEvent { + variant: + LayerSurfaceEventVariant::ScaleFactorChanged(..), + .. + } + | SctkEvent::PopupEvent { + variant: PopupEventVariant::ScaleFactorChanged(..), + .. + } + | SctkEvent::WindowEvent { + variant: WindowEventVariant::ScaleFactorChanged(..), + .. + } => true, + // ignore other events that shouldn't be in this buffer + event => { + tracing::warn!( + "Unhandled compositor event: {:?}", + event + ); + false + } + }; + if forward_event { + sticky_exit_callback( + IcedSctkEvent::SctkEvent(event), + &self.state, + &mut control_flow, + &mut callback, + ); + } + } + + std::mem::swap( + &mut frame_event_back_buffer, + &mut self.state.frame_events, + ); + + for event in frame_event_back_buffer.drain(..) { + sticky_exit_callback( + IcedSctkEvent::Frame(event), + &self.state, + &mut control_flow, + &mut callback, + ); + } + + // The purpose of the back buffer and that swap is to not hold borrow_mut when + // we're doing callback to the user, since we can double borrow if the user decides + // to create a window in one of those callbacks. + std::mem::swap( + &mut sctk_event_sink_back_buffer, + &mut self.state.sctk_events, + ); + + // handle a11y events + #[cfg(feature = "a11y")] + if let Ok(mut events) = self.a11y_events.lock() { + for event in events.drain(..) { + match event { + adapter::A11yWrapper::Enabled => sticky_exit_callback( + IcedSctkEvent::A11yEnabled, + &self.state, + &mut control_flow, + &mut callback, + ), + adapter::A11yWrapper::Event(event) => { + sticky_exit_callback( + IcedSctkEvent::A11yEvent(event), + &self.state, + &mut control_flow, + &mut callback, + ) + } + } + } + } + // Handle pending sctk events. + for event in sctk_event_sink_back_buffer.drain(..) { + match event { + SctkEvent::PopupEvent { + variant: PopupEventVariant::Done, + toplevel_id, + parent_id, + id, + } => { + match self + .state + .popups + .iter() + .position(|s| s.popup.wl_surface().id() == id.id()) + { + Some(p) => { + let _p = self.state.popups.remove(p); + sticky_exit_callback( + IcedSctkEvent::SctkEvent( + SctkEvent::PopupEvent { + variant: PopupEventVariant::Done, + toplevel_id, + parent_id, + id, + }, + ), + &self.state, + &mut control_flow, + &mut callback, + ); + } + None => continue, + }; + } + SctkEvent::LayerSurfaceEvent { + variant: LayerSurfaceEventVariant::Done, + id, + } => { + if let Some(i) = + self.state.layer_surfaces.iter().position(|l| { + l.surface.wl_surface().id() == id.id() + }) + { + let _l = self.state.layer_surfaces.remove(i); + sticky_exit_callback( + IcedSctkEvent::SctkEvent( + SctkEvent::LayerSurfaceEvent { + variant: LayerSurfaceEventVariant::Done, + id, + }, + ), + &self.state, + &mut control_flow, + &mut callback, + ); + } + } + SctkEvent::WindowEvent { + variant: WindowEventVariant::Close, + id, + } => { + if let Some(i) = + self.state.windows.iter().position(|l| { + l.window.wl_surface().id() == id.id() + }) + { + let w = self.state.windows.remove(i); + w.window.xdg_toplevel().destroy(); + sticky_exit_callback( + IcedSctkEvent::SctkEvent( + SctkEvent::WindowEvent { + variant: WindowEventVariant::Close, + id, + }, + ), + &self.state, + &mut control_flow, + &mut callback, + ); + } + } + _ => sticky_exit_callback( + IcedSctkEvent::SctkEvent(event), + &self.state, + &mut control_flow, + &mut callback, + ), + } + } + + // handle events indirectly via callback to the user. + let (sctk_events, user_events): (Vec<_>, Vec<_>) = self + .state + .pending_user_events + .drain(..) + .partition(|e| matches!(e, Event::SctkEvent(_))); + let mut to_commit = HashMap::new(); + let mut pending_redraws = Vec::new(); + for event in sctk_events.into_iter().chain(user_events.into_iter()) + { + match event { + Event::Message(m) => { + sticky_exit_callback( + IcedSctkEvent::UserEvent(m), + &self.state, + &mut control_flow, + &mut callback, + ); + } + Event::SctkEvent(event) => { + match event { + IcedSctkEvent::RedrawRequested(id) => { + pending_redraws.push(id); + }, + e => sticky_exit_callback( + e, + &self.state, + &mut control_flow, + &mut callback, + ), + } + } + Event::LayerSurface(action) => match action { + platform_specific::wayland::layer_surface::Action::LayerSurface { + builder, + _phantom, + } => { + // TODO ASHLEY: error handling + if let Ok((id, wl_surface)) = self.state.get_layer_surface(builder) { + let object_id = wl_surface.id(); + // TODO Ashley: all surfaces should probably have an optional title for a11y if nothing else + sticky_exit_callback( + IcedSctkEvent::SctkEvent(SctkEvent::LayerSurfaceEvent { + variant: LayerSurfaceEventVariant::Created(object_id.clone(), id), + id: wl_surface.clone(), + }), + &self.state, + &mut control_flow, + &mut callback, + ); + #[cfg(feature = "a11y")] + { + let adapter = self.init_a11y_adapter(&wl_surface, None, None, iced_accessibility::accesskit::Role::Window); + + sticky_exit_callback( + IcedSctkEvent::A11ySurfaceCreated(SurfaceIdWrapper::LayerSurface(id), adapter), + &self.state, + &mut control_flow, + &mut callback, + ); + } + } + } + platform_specific::wayland::layer_surface::Action::Size { + id, + width, + height, + } => { + if let Some(layer_surface) = self.state.layer_surfaces.iter_mut().find(|l| l.id == id) { + layer_surface.set_size(width, height); + pending_redraws.push(layer_surface.surface.wl_surface().id()); + let wl_surface = layer_surface.surface.wl_surface(); + + if let Some(mut prev_configure) = layer_surface.last_configure.clone() { + prev_configure.new_size = (width.unwrap_or(prev_configure.new_size.0), width.unwrap_or(prev_configure.new_size.1)); + sticky_exit_callback( + IcedSctkEvent::SctkEvent(SctkEvent::LayerSurfaceEvent { variant: LayerSurfaceEventVariant::Configure(prev_configure, wl_surface.clone(), false), id: wl_surface.clone()}), + &self.state, + &mut control_flow, + &mut callback, + ); + } + } + }, + platform_specific::wayland::layer_surface::Action::Destroy(id) => { + if let Some(i) = self.state.layer_surfaces.iter().position(|l| l.id == id) { + let l = self.state.layer_surfaces.remove(i); + sticky_exit_callback( + IcedSctkEvent::SctkEvent(SctkEvent::LayerSurfaceEvent { + variant: LayerSurfaceEventVariant::Done, + id: l.surface.wl_surface().clone(), + }), + &self.state, + &mut control_flow, + &mut callback, + ); + } + }, + platform_specific::wayland::layer_surface::Action::Anchor { id, anchor } => { + if let Some(layer_surface) = self.state.layer_surfaces.iter_mut().find(|l| l.id == id) { + layer_surface.anchor = anchor; + layer_surface.surface.set_anchor(anchor); + to_commit.insert(id, layer_surface.surface.wl_surface().clone()); + + } + } + platform_specific::wayland::layer_surface::Action::ExclusiveZone { + id, + exclusive_zone, + } => { + if let Some(layer_surface) = self.state.layer_surfaces.iter_mut().find(|l| l.id == id) { + layer_surface.exclusive_zone = exclusive_zone; + layer_surface.surface.set_exclusive_zone(exclusive_zone); + to_commit.insert(id, layer_surface.surface.wl_surface().clone()); + } + }, + platform_specific::wayland::layer_surface::Action::Margin { + id, + margin, + } => { + if let Some(layer_surface) = self.state.layer_surfaces.iter_mut().find(|l| l.id == id) { + layer_surface.margin = margin; + layer_surface.surface.set_margin(margin.top, margin.right, margin.bottom, margin.left); + to_commit.insert(id, layer_surface.surface.wl_surface().clone()); + } + }, + platform_specific::wayland::layer_surface::Action::KeyboardInteractivity { id, keyboard_interactivity } => { + if let Some(layer_surface) = self.state.layer_surfaces.iter_mut().find(|l| l.id == id) { + layer_surface.keyboard_interactivity = keyboard_interactivity; + layer_surface.surface.set_keyboard_interactivity(keyboard_interactivity); + to_commit.insert(id, layer_surface.surface.wl_surface().clone()); + + } + }, + platform_specific::wayland::layer_surface::Action::Layer { id, layer } => { + if let Some(layer_surface) = self.state.layer_surfaces.iter_mut().find(|l| l.id == id) { + layer_surface.layer = layer; + layer_surface.surface.set_layer(layer); + to_commit.insert(id, layer_surface.surface.wl_surface().clone()); + + } + }, + }, + Event::SetCursor(iced_icon) => { + if let Some(ptr) = self.state.seats.get(0).and_then(|s| s.ptr.as_ref()) { + let icon = conversion::cursor_icon(iced_icon); + let _ = ptr.set_cursor(self.wayland_dispatcher.as_source_ref().connection(), icon); + } + + } + Event::Window(action) => match action { + platform_specific::wayland::window::Action::Window { builder, _phantom } => { + #[cfg(feature = "a11y")] + let app_id = builder.app_id.clone(); + #[cfg(feature = "a11y")] + let title = builder.title.clone(); + let (id, wl_surface) = self.state.get_window(builder); + let object_id = wl_surface.id(); + sticky_exit_callback( + IcedSctkEvent::SctkEvent(SctkEvent::WindowEvent { + variant: WindowEventVariant::Created(object_id.clone(), id), + id: wl_surface.clone() }), + &self.state, + &mut control_flow, + &mut callback, + ); + + #[cfg(feature = "a11y")] + { + let adapter = self.init_a11y_adapter(&wl_surface, app_id, title, iced_accessibility::accesskit::Role::Window); + + sticky_exit_callback( + IcedSctkEvent::A11ySurfaceCreated(SurfaceIdWrapper::Window(id), adapter), + &self.state, + &mut control_flow, + &mut callback, + ); + } + }, + platform_specific::wayland::window::Action::Size { id, width, height } => { + if let Some(window) = self.state.windows.iter_mut().find(|w| w.id == id) { + window.set_size(LogicalSize::new(NonZeroU32::new(width).unwrap_or(NonZeroU32::new(1).unwrap()), NonZeroU32::new(1).unwrap())); + // TODO Ashley maybe don't force window size? + pending_redraws.push(window.window.wl_surface().id()); + + if let Some(mut prev_configure) = window.last_configure.clone() { + let (width, height) = ( + NonZeroU32::new(width).unwrap_or(NonZeroU32::new(1).unwrap()), + NonZeroU32::new(height).unwrap_or(NonZeroU32::new(1).unwrap()), + ); + prev_configure.new_size = (Some(width), Some(height)); + sticky_exit_callback( + IcedSctkEvent::SctkEvent(SctkEvent::WindowEvent { variant: WindowEventVariant::Configure(prev_configure, window.window.wl_surface().clone(), false), id: window.window.wl_surface().clone()}), + &self.state, + &mut control_flow, + &mut callback, + ); + } + } + }, + platform_specific::wayland::window::Action::MinSize { id, size } => { + if let Some(window) = self.state.windows.iter_mut().find(|w| w.id == id) { + window.window.set_min_size(size); + to_commit.insert(id, window.window.wl_surface().clone()); + } + }, + platform_specific::wayland::window::Action::MaxSize { id, size } => { + if let Some(window) = self.state.windows.iter_mut().find(|w| w.id == id) { + window.window.set_max_size(size); + to_commit.insert(id, window.window.wl_surface().clone()); + } + }, + platform_specific::wayland::window::Action::Title { id, title } => { + if let Some(window) = self.state.windows.iter_mut().find(|w| w.id == id) { + window.window.set_title(title); + to_commit.insert(id, window.window.wl_surface().clone()); + } + }, + platform_specific::wayland::window::Action::Minimize { id } => { + if let Some(window) = self.state.windows.iter_mut().find(|w| w.id == id) { + window.window.set_minimized(); + to_commit.insert(id, window.window.wl_surface().clone()); + } + }, + platform_specific::wayland::window::Action::Maximize { id } => { + if let Some(window) = self.state.windows.iter_mut().find(|w| w.id == id) { + window.window.set_maximized(); + to_commit.insert(id, window.window.wl_surface().clone()); + } + }, + platform_specific::wayland::window::Action::UnsetMaximize { id } => { + if let Some(window) = self.state.windows.iter_mut().find(|w| w.id == id) { + window.window.unset_maximized(); + to_commit.insert(id, window.window.wl_surface().clone()); + } + }, + platform_specific::wayland::window::Action::Fullscreen { id } => { + if let Some(window) = self.state.windows.iter_mut().find(|w| w.id == id) { + // TODO ASHLEY: allow specific output to be requested for fullscreen? + window.window.set_fullscreen(None); + to_commit.insert(id, window.window.wl_surface().clone()); + } + }, + platform_specific::wayland::window::Action::UnsetFullscreen { id } => { + if let Some(window) = self.state.windows.iter_mut().find(|w| w.id == id) { + window.window.unset_fullscreen(); + to_commit.insert(id, window.window.wl_surface().clone()); + } + }, + platform_specific::wayland::window::Action::InteractiveMove { id } => { + if let (Some(window), Some((seat, last_press))) = (self.state.windows.iter_mut().find(|w| w.id == id), self.state.seats.first().and_then(|seat| seat.last_ptr_press.map(|p| (&seat.seat, p.2)))) { + window.window.xdg_toplevel()._move(seat, last_press); + to_commit.insert(id, window.window.wl_surface().clone()); + } + }, + platform_specific::wayland::window::Action::InteractiveResize { id, edge } => { + if let (Some(window), Some((seat, last_press))) = (self.state.windows.iter_mut().find(|w| w.id == id), self.state.seats.first().and_then(|seat| seat.last_ptr_press.map(|p| (&seat.seat, p.2)))) { + window.window.xdg_toplevel().resize(seat, last_press, edge); + to_commit.insert(id, window.window.wl_surface().clone()); + } + }, + platform_specific::wayland::window::Action::ToggleMaximized { id } => { + if let Some(window) = self.state.windows.iter_mut().find(|w| w.id == id) { + if let Some(c) = &window.last_configure { + if c.is_maximized() { + window.window.unset_maximized(); + } else { + window.window.set_maximized(); + } + to_commit.insert(id, window.window.wl_surface().clone()); + } + } + }, + platform_specific::wayland::window::Action::ShowWindowMenu { id: _, x: _, y: _ } => todo!(), + platform_specific::wayland::window::Action::Destroy(id) => { + if let Some(i) = self.state.windows.iter().position(|l| l.id == id) { + let window = self.state.windows.remove(i); + window.window.xdg_toplevel().destroy(); + sticky_exit_callback( + IcedSctkEvent::SctkEvent(SctkEvent::WindowEvent { + variant: WindowEventVariant::Close, + id: window.window.wl_surface().clone(), + }), + &self.state, + &mut control_flow, + &mut callback, + ); + } + }, + platform_specific::wayland::window::Action::Mode(id, mode) => { + if let Some(window) = self.state.windows.iter_mut().find(|w| w.id == id) { + match mode { + Mode::Windowed => { + window.window.unset_fullscreen(); + }, + Mode::Fullscreen => { + window.window.set_fullscreen(None); + }, + Mode::Hidden => { + window.window.set_minimized(); + }, + } + to_commit.insert(id, window.window.wl_surface().clone()); + } + }, + platform_specific::wayland::window::Action::ToggleFullscreen { id } => { + if let Some(window) = self.state.windows.iter_mut().find(|w| w.id == id) { + if let Some(c) = &window.last_configure { + if c.is_fullscreen() { + window.window.unset_fullscreen(); + } else { + window.window.set_fullscreen(None); + } + to_commit.insert(id, window.window.wl_surface().clone()); + } + } + }, + platform_specific::wayland::window::Action::AppId { id, app_id } => { + if let Some(window) = self.state.windows.iter_mut().find(|w| w.id == id) { + window.window.set_app_id(app_id); + to_commit.insert(id, window.window.wl_surface().clone()); + } + }, + }, + Event::Popup(action) => match action { + platform_specific::wayland::popup::Action::Popup { popup, .. } => { + if let Ok((id, parent_id, toplevel_id, wl_surface)) = self.state.get_popup(popup) { + let object_id = wl_surface.id(); + sticky_exit_callback( + IcedSctkEvent::SctkEvent(SctkEvent::PopupEvent { + variant: crate::sctk_event::PopupEventVariant::Created(object_id.clone(), id), + toplevel_id, parent_id, id: wl_surface.clone() }), + &self.state, + &mut control_flow, + &mut callback, + ); + + #[cfg(feature = "a11y")] + { + let adapter = self.init_a11y_adapter(&wl_surface, None, None, iced_accessibility::accesskit::Role::Window); + + sticky_exit_callback( + IcedSctkEvent::A11ySurfaceCreated(SurfaceIdWrapper::LayerSurface(id), adapter), + &self.state, + &mut control_flow, + &mut callback, + ); + } + } + }, + // XXX popup destruction must be done carefully + // first destroy the uppermost popup, then work down to the requested popup + platform_specific::wayland::popup::Action::Destroy { id } => { + let sctk_popup = match self.state + .popups + .iter() + .position(|s| s.data.id == id) + { + Some(p) => self.state.popups.remove(p), + None => continue, + }; + let mut to_destroy = vec![sctk_popup]; + while let Some(popup_to_destroy) = to_destroy.last() { + match popup_to_destroy.data.parent.clone() { + state::SctkSurface::LayerSurface(_) | state::SctkSurface::Window(_) => { + break; + } + state::SctkSurface::Popup(popup_to_destroy_first) => { + let popup_to_destroy_first = self + .state + .popups + .iter() + .position(|p| p.popup.wl_surface() == &popup_to_destroy_first) + .unwrap(); + let popup_to_destroy_first = self.state.popups.remove(popup_to_destroy_first); + to_destroy.push(popup_to_destroy_first); + } + } + } + for popup in to_destroy.into_iter().rev() { + sticky_exit_callback(IcedSctkEvent::SctkEvent(SctkEvent::PopupEvent { + variant: PopupEventVariant::Done, + toplevel_id: popup.data.toplevel.clone(), + parent_id: popup.data.parent.wl_surface().clone(), + id: popup.popup.wl_surface().clone(), + }), + &self.state, + &mut control_flow, + &mut callback, + ); + } + }, + platform_specific::wayland::popup::Action::Size { id, width, height } => { + if let Some(sctk_popup) = self.state + .popups + .iter_mut() + .find(|s| s.data.id == id) + { + // update geometry + // update positioner + self.state.token_ctr += 1; + sctk_popup.set_size(width, height, self.state.token_ctr); + + pending_redraws.push(sctk_popup.popup.wl_surface().id()); + + sticky_exit_callback(IcedSctkEvent::SctkEvent(SctkEvent::PopupEvent { + variant: PopupEventVariant::Size(width, height), + toplevel_id: sctk_popup.data.toplevel.clone(), + parent_id: sctk_popup.data.parent.wl_surface().clone(), + id: sctk_popup.popup.wl_surface().clone(), + }), + &self.state, + &mut control_flow, + &mut callback, + ); + } + }, + // TODO probably remove this? + platform_specific::wayland::popup::Action::Grab { .. } => {}, + }, + Event::DataDevice(action) => { + match action.inner { + platform_specific::wayland::data_device::ActionInner::Accept(mime_type) => { + let drag_offer = match self.state.dnd_offer.as_mut() { + Some(d) => d, + None => continue, + }; + drag_offer.offer.accept_mime_type(drag_offer.offer.serial, mime_type); + } + platform_specific::wayland::data_device::ActionInner::StartInternalDnd { origin_id, icon_id } => { + let qh = &self.state.queue_handle.clone(); + let seat = match self.state.seats.get(0) { + Some(s) => s, + None => continue, + }; + let serial = match seat.last_ptr_press { + Some(s) => s.2, + None => continue, + }; + + let origin = match self + .state + .windows + .iter() + .find(|w| w.id == origin_id) + .map(|w| Some(w.window.wl_surface())) + .unwrap_or_else(|| self.state.layer_surfaces.iter() + .find(|l| l.id == origin_id).map(|l| Some(l.surface.wl_surface())) + .unwrap_or_else(|| self.state.popups.iter().find(|p| p.data.id == origin_id).map(|p| p.popup.wl_surface()))) { + Some(s) => s.clone(), + None => continue, + }; + let device = match self.state.seats.get(0) { + Some(s) => &s.data_device, + None => continue, + }; + let icon_surface = if let Some(icon_id) = icon_id{ + let wl_surface = self.state.compositor_state.create_surface(qh); + DragSource::start_internal_drag(device, &origin, Some(&wl_surface), serial); + Some((wl_surface, icon_id)) + } else { + DragSource::start_internal_drag(device, &origin, None, serial); + None + }; + self.state.dnd_source = Some(Dnd { + origin_id, + icon_surface, + origin, + source: None, + pending_requests: Vec::new(), + pipe: None, + cur_write: None, + }); + } + platform_specific::wayland::data_device::ActionInner::StartDnd { mime_types, actions, origin_id, icon_id, data } => { + if let Some(dnd_source) = self.state.dnd_source.as_ref() { + if dnd_source.cur_write.is_some() { + continue; + } + } + let qh = &self.state.queue_handle.clone(); + let seat = match self.state.seats.get(0) { + Some(s) => s, + None => continue, + }; + let serial = match seat.last_ptr_press { + Some(s) => s.2, + None => continue, + }; + + let origin = match self + .state + .windows + .iter() + .find(|w| w.id == origin_id) + .map(|w| Some(w.window.wl_surface())) + .unwrap_or_else(|| self.state.layer_surfaces.iter() + .find(|l| l.id == origin_id).map(|l| Some(l.surface.wl_surface())) + .unwrap_or_else(|| self.state.popups.iter().find(|p| p.data.id == origin_id).map(|p| p.popup.wl_surface()))) { + Some(s) => s.clone(), + None => continue, + }; + let device = match self.state.seats.get(0) { + Some(s) => &s.data_device, + None => continue, + }; + let source = self.state.data_device_manager_state.create_drag_and_drop_source(qh, mime_types.iter().map(|s| s.as_str()).collect::>(), actions); + let icon_surface = if let Some(icon_id) = icon_id{ + let icon_native_id = match &icon_id { + DndIcon::Custom(icon_id) => *icon_id, + DndIcon::Widget(icon_id, _) => *icon_id, + }; + let wl_surface = self.state.compositor_state.create_surface(qh); + source.start_drag(device, &origin, Some(&wl_surface), serial); + sticky_exit_callback( + IcedSctkEvent::DndSurfaceCreated( + wl_surface.clone(), + icon_id, + origin_id) + , + &self.state, + &mut control_flow, + &mut callback + ); + Some((wl_surface, icon_native_id)) + } else { + source.start_drag(device, &origin, None, serial); + None + }; + self.state.dnd_source = Some(Dnd { origin_id, origin, source: Some((source, data)), icon_surface, pending_requests: Vec::new(), pipe: None, cur_write: None }); + }, + platform_specific::wayland::data_device::ActionInner::DndFinished => { + if let Some(offer) = self.state.dnd_offer.take() { + if offer.dropped { + offer.offer.finish(); + } + else { + self.state.dnd_offer = Some(offer); + } + } + }, + platform_specific::wayland::data_device::ActionInner::DndCancelled => { + if let Some(source) = self.state.dnd_source.as_mut() { + source.source = None; + } + }, + platform_specific::wayland::data_device::ActionInner::RequestDndData (mime_type) => { + if let Some(dnd_offer) = self.state.dnd_offer.as_mut() { + let read_pipe = match dnd_offer.offer.receive(mime_type.clone()) { + Ok(p) => p, + Err(_) => continue, // TODO error handling + }; + let loop_handle = self.event_loop.handle(); + match self.event_loop.handle().insert_source(read_pipe, move |_, f, state| { + let mut dnd_offer = match state.dnd_offer.take() { + Some(s) => s, + None => return PostAction::Continue, + }; + let (mime_type, data, token) = match dnd_offer.cur_read.take() { + Some(s) => s, + None => return PostAction::Continue, + }; + let mut reader = BufReader::new(f.as_ref()); + let consumed = match reader.fill_buf() { + Ok(buf) => { + if buf.is_empty() { + loop_handle.remove(token); + state.sctk_events.push(SctkEvent::DndOffer { event: DndOfferEvent::Data { data, mime_type }, surface: dnd_offer.offer.surface.clone() }); + if dnd_offer.dropped { + dnd_offer.offer.finish(); + } else { + state.dnd_offer = Some(dnd_offer); + } + } else { + let mut data = data; + data.extend_from_slice(buf); + dnd_offer.cur_read = Some((mime_type, data, token)); + state.dnd_offer = Some(dnd_offer); + } + buf.len() + }, + Err(e) if matches!(e.kind(), std::io::ErrorKind::Interrupted) => { + dnd_offer.cur_read = Some((mime_type, data, token)); + state.dnd_offer = Some(dnd_offer); + return PostAction::Continue; + }, + Err(e) => { + error!("Error reading selection data: {}", e); + if !dnd_offer.dropped { + state.dnd_offer = Some(dnd_offer); + } + return PostAction::Remove; + }, + }; + reader.consume(consumed); + PostAction::Continue + }) { + Ok(token) => { + dnd_offer.cur_read = Some((mime_type.clone(), Vec::new(), token)); + }, + Err(_) => continue, + }; + } + } + platform_specific::wayland::data_device::ActionInner::SetActions { preferred, accepted } => { + if let Some(offer) = self.state.dnd_offer.as_ref() { + offer.offer.set_actions(accepted, preferred); + } + } + } + }, + Event::Activation(activation_event) => match activation_event { + platform_specific::wayland::activation::Action::RequestToken { app_id, window, message } => { + if let Some(activation_state) = self.state.activation_state.as_ref() { + let (seat_and_serial, surface) = if let Some(id) = window { + let surface = self.state.windows.iter().find(|w| w.id == id) + .map(|w| w.window.wl_surface().clone()) + .or_else(|| self.state.layer_surfaces.iter().find(|l| l.id == id) + .map(|l| l.surface.wl_surface().clone()) + ); + let seat_and_serial = surface.as_ref().and_then(|surface| { + self.state.seats.first().and_then(|seat| if seat.kbd_focus.as_ref().map(|focus| focus == surface).unwrap_or(false) { + seat.last_kbd_press.as_ref().map(|(_, serial)| (seat.seat.clone(), *serial)) + } else if seat.ptr_focus.as_ref().map(|focus| focus == surface).unwrap_or(false) { + seat.last_ptr_press.as_ref().map(|(_, _, serial)| (seat.seat.clone(), *serial)) + } else { + None + }) + }); + + (seat_and_serial, surface) + } else { + (None, None) + }; + + activation_state.request_token_with_data(&self.state.queue_handle, IcedRequestData::new( + RequestData { + app_id, + seat_and_serial, + surface, + }, + message, + )); + } else { + // if we don't have the global, we don't want to stall the app + sticky_exit_callback( + IcedSctkEvent::UserEvent(message(None)), + &self.state, + &mut control_flow, + &mut callback, + ) + } + }, + platform_specific::wayland::activation::Action::Activate { window, token } => { + if let Some(activation_state) = self.state.activation_state.as_ref() { + if let Some(surface) = self.state.windows.iter().find(|w| w.id == window).map(|w| w.window.wl_surface()) { + activation_state.activate::>(surface, token) + } + } + }, + }, + Event::SessionLock(action) => match action { + platform_specific::wayland::session_lock::Action::Lock => { + if self.state.session_lock.is_none() { + // TODO send message on error? When protocol doesn't exist. + self.state.session_lock = self.state.session_lock_state.lock(&self.state.queue_handle).ok(); + } + } + platform_specific::wayland::session_lock::Action::Unlock => { + self.state.session_lock.take(); + // Make sure server processes unlock before client exits + let _ = self.state.connection.roundtrip(); + sticky_exit_callback( + IcedSctkEvent::SctkEvent(SctkEvent::SessionUnlocked), + &self.state, + &mut control_flow, + &mut callback, + ); + } + platform_specific::wayland::session_lock::Action::LockSurface { id, output, _phantom } => { + // TODO how to handle this when there's no lock? + if let Some(surface) = self.state.get_lock_surface(id, &output) { + sticky_exit_callback( + IcedSctkEvent::SctkEvent(SctkEvent::SessionLockSurfaceCreated {surface, native_id: id}), + &self.state, + &mut control_flow, + &mut callback, + ); + } + } + platform_specific::wayland::session_lock::Action::DestroyLockSurface { id } => { + if let Some(i) = + self.state.lock_surfaces.iter().position(|s| { + s.id == id + }) + { + self.state.lock_surfaces.remove(i); + } + } + } + } + } + + // Send events cleared. + sticky_exit_callback( + IcedSctkEvent::MainEventsCleared, + &self.state, + &mut control_flow, + &mut callback, + ); + + // redraw + pending_redraws.dedup(); + for id in pending_redraws { + sticky_exit_callback( + IcedSctkEvent::RedrawRequested(id.clone()), + &self.state, + &mut control_flow, + &mut callback, + ); + } + + // commit changes made via actions + for s in to_commit { + s.1.commit(); + } + + // Send RedrawEventCleared. + sticky_exit_callback( + IcedSctkEvent::RedrawEventsCleared, + &self.state, + &mut control_flow, + &mut callback, + ); + }; + + callback(IcedSctkEvent::LoopDestroyed, &self.state, &mut control_flow); + exit_code + } +} + +fn sticky_exit_callback( + evt: IcedSctkEvent, + target: &SctkState, + control_flow: &mut ControlFlow, + callback: &mut F, +) where + F: FnMut(IcedSctkEvent, &SctkState, &mut ControlFlow), +{ + // make ControlFlow::ExitWithCode sticky by providing a dummy + // control flow reference if it is already ExitWithCode. + if let ControlFlow::ExitWithCode(code) = *control_flow { + callback(evt, target, &mut ControlFlow::ExitWithCode(code)) + } else { + callback(evt, target, control_flow) + } +} + +fn raw_os_err(err: calloop::Error) -> i32 { + match err { + calloop::Error::IoError(err) => err.raw_os_error(), + _ => None, + } + .unwrap_or(1) +} diff --git a/sctk/src/event_loop/proxy.rs b/sctk/src/event_loop/proxy.rs new file mode 100644 index 0000000000..7140cef708 --- /dev/null +++ b/sctk/src/event_loop/proxy.rs @@ -0,0 +1,66 @@ +use iced_futures::futures::{ + channel::mpsc, + task::{Context, Poll}, + Sink, +}; +use sctk::reexports::calloop; +use std::pin::Pin; + +/// An event loop proxy that implements `Sink`. +#[derive(Debug)] +pub struct Proxy { + raw: calloop::channel::Sender, +} + +impl Clone for Proxy { + fn clone(&self) -> Self { + Self { + raw: self.raw.clone(), + } + } +} + +impl Proxy { + /// Creates a new [`Proxy`] from an `EventLoopProxy`. + pub fn new(raw: calloop::channel::Sender) -> Self { + Self { raw } + } + /// send an event + pub fn send_event(&self, message: Message) { + let _ = self.raw.send(message); + } +} + +impl Sink for Proxy { + type Error = mpsc::SendError; + + fn poll_ready( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + + fn start_send( + self: Pin<&mut Self>, + message: Message, + ) -> Result<(), Self::Error> { + let _ = self.raw.send(message); + + Ok(()) + } + + fn poll_flush( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_close( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } +} diff --git a/sctk/src/event_loop/state.rs b/sctk/src/event_loop/state.rs new file mode 100644 index 0000000000..be3fb9e7eb --- /dev/null +++ b/sctk/src/event_loop/state.rs @@ -0,0 +1,851 @@ +use std::{ + fmt::{Debug, Formatter}, + num::NonZeroU32, +}; + +use crate::{ + application::Event, + dpi::LogicalSize, + handlers::{ + wp_fractional_scaling::FractionalScalingManager, + wp_viewporter::ViewporterState, + }, + sctk_event::{ + LayerSurfaceEventVariant, PopupEventVariant, SctkEvent, + WindowEventVariant, + }, +}; + +use iced_runtime::{ + command::platform_specific::{ + self, + wayland::{ + data_device::DataFromMimeType, + layer_surface::{IcedMargin, IcedOutput, SctkLayerSurfaceSettings}, + popup::SctkPopupSettings, + window::SctkWindowSettings, + }, + }, + keyboard::Modifiers, + window, +}; +use sctk::{ + activation::ActivationState, + compositor::CompositorState, + data_device_manager::{ + data_device::DataDevice, data_offer::DragOffer, + data_source::DragSource, DataDeviceManagerState, WritePipe, + }, + error::GlobalError, + output::OutputState, + reexports::{ + calloop::{LoopHandle, RegistrationToken}, + client::{ + protocol::{ + wl_keyboard::WlKeyboard, + wl_output::WlOutput, + wl_seat::WlSeat, + wl_surface::{self, WlSurface}, + wl_touch::WlTouch, + }, + Connection, QueueHandle, + }, + }, + registry::RegistryState, + seat::{ + keyboard::KeyEvent, + pointer::{CursorIcon, ThemedPointer}, + SeatState, + }, + session_lock::{ + SessionLock, SessionLockState, SessionLockSurface, + SessionLockSurfaceConfigure, + }, + shell::{ + wlr_layer::{ + Anchor, KeyboardInteractivity, Layer, LayerShell, LayerSurface, + LayerSurfaceConfigure, + }, + xdg::{ + popup::{Popup, PopupConfigure}, + window::{Window, WindowConfigure, WindowDecorations}, + XdgPositioner, XdgShell, XdgSurface, + }, + WaylandSurface, + }, + shm::{multi::MultiPool, Shm}, +}; +use wayland_protocols::wp::{ + fractional_scale::v1::client::wp_fractional_scale_v1::WpFractionalScaleV1, + viewporter::client::wp_viewport::WpViewport, +}; + +#[derive(Debug)] +pub(crate) struct SctkSeat { + pub(crate) seat: WlSeat, + pub(crate) kbd: Option, + pub(crate) kbd_focus: Option, + pub(crate) last_kbd_press: Option<(KeyEvent, u32)>, + pub(crate) ptr: Option, + pub(crate) ptr_focus: Option, + pub(crate) last_ptr_press: Option<(u32, u32, u32)>, // (time, button, serial) + pub(crate) _touch: Option, + pub(crate) _modifiers: Modifiers, + pub(crate) data_device: DataDevice, + pub(crate) icon: Option, +} + +#[derive(Debug, Clone)] +pub struct SctkWindow { + pub(crate) id: window::Id, + pub(crate) window: Window, + pub(crate) scale_factor: Option, + pub(crate) requested_size: Option<(u32, u32)>, + pub(crate) current_size: Option<(NonZeroU32, NonZeroU32)>, + pub(crate) last_configure: Option, + pub(crate) resizable: Option, + /// Requests that SCTK window should perform. + pub(crate) _pending_requests: + Vec>, + pub(crate) wp_fractional_scale: Option, + pub(crate) wp_viewport: Option, +} + +impl SctkWindow { + pub(crate) fn set_size(&mut self, logical_size: LogicalSize) { + self.requested_size = + Some((logical_size.width.get(), logical_size.height.get())); + self.update_size(logical_size) + } + + pub(crate) fn update_size( + &mut self, + LogicalSize { width, height }: LogicalSize, + ) { + self.window.set_window_geometry( + 0, + 0, + width.get() as u32, + height.get() as u32, + ); + self.current_size = Some((width, height)); + // Update the target viewport, this is used if and only if fractional scaling is in use. + if let Some(viewport) = self.wp_viewport.as_ref() { + // Set inner size without the borders. + viewport.set_destination(width.get() as _, height.get() as _); + } + } +} + +#[derive(Debug, Clone)] +pub struct SctkLayerSurface { + pub(crate) id: window::Id, + pub(crate) surface: LayerSurface, + pub(crate) requested_size: (Option, Option), + pub(crate) current_size: Option>, + pub(crate) layer: Layer, + pub(crate) anchor: Anchor, + pub(crate) keyboard_interactivity: KeyboardInteractivity, + pub(crate) margin: IcedMargin, + pub(crate) exclusive_zone: i32, + pub(crate) last_configure: Option, + pub(crate) _pending_requests: + Vec>, + pub(crate) scale_factor: Option, + pub(crate) wp_fractional_scale: Option, + pub(crate) wp_viewport: Option, +} + +impl SctkLayerSurface { + pub(crate) fn set_size(&mut self, w: Option, h: Option) { + self.requested_size = (w, h); + + let (w, h) = (w.unwrap_or_default(), h.unwrap_or_default()); + self.surface.set_size(w, h); + } + + pub(crate) fn update_viewport(&mut self, w: u32, h: u32) { + self.current_size = Some(LogicalSize::new(w, h)); + if let Some(viewport) = self.wp_viewport.as_ref() { + // Set inner size without the borders. + viewport.set_destination(w as i32, h as i32); + } + } +} + +#[derive(Debug, Clone)] +pub enum SctkSurface { + LayerSurface(WlSurface), + Window(WlSurface), + Popup(WlSurface), +} + +impl SctkSurface { + pub fn wl_surface(&self) -> &WlSurface { + match self { + SctkSurface::LayerSurface(s) + | SctkSurface::Window(s) + | SctkSurface::Popup(s) => s, + } + } +} + +#[derive(Debug)] +pub struct SctkPopup { + pub(crate) popup: Popup, + pub(crate) last_configure: Option, + // pub(crate) positioner: XdgPositioner, + pub(crate) _pending_requests: + Vec>, + pub(crate) data: SctkPopupData, + pub(crate) scale_factor: Option, + pub(crate) wp_fractional_scale: Option, + pub(crate) wp_viewport: Option, +} + +impl SctkPopup { + pub(crate) fn set_size(&mut self, w: u32, h: u32, token: u32) { + // update geometry + self.popup + .xdg_surface() + .set_window_geometry(0, 0, w as i32, h as i32); + // update positioner + self.data.positioner.set_size(w as i32, h as i32); + self.popup.reposition(&self.data.positioner, token); + } +} + +#[derive(Debug)] +pub struct SctkLockSurface { + pub(crate) id: window::Id, + pub(crate) session_lock_surface: SessionLockSurface, + pub(crate) last_configure: Option, +} + +pub struct Dnd { + pub(crate) origin_id: window::Id, + pub(crate) origin: WlSurface, + pub(crate) source: Option<(DragSource, Box)>, + pub(crate) icon_surface: Option<(WlSurface, window::Id)>, + pub(crate) pending_requests: + Vec>, + pub(crate) pipe: Option, + pub(crate) cur_write: Option<(Vec, usize, RegistrationToken)>, +} + +impl Debug for Dnd { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("Dnd") + .field(&self.origin_id) + .field(&self.origin) + .field(&self.icon_surface) + .field(&self.pending_requests) + .field(&self.pipe) + .field(&self.cur_write) + .finish() + } +} + +#[derive(Debug)] +pub struct SctkDragOffer { + pub(crate) dropped: bool, + pub(crate) offer: DragOffer, + pub(crate) cur_read: Option<(String, Vec, RegistrationToken)>, +} + +#[derive(Debug)] +pub struct SctkPopupData { + pub(crate) id: window::Id, + pub(crate) parent: SctkSurface, + pub(crate) toplevel: WlSurface, + pub(crate) positioner: XdgPositioner, +} + +/// Wrapper to carry sctk state. +pub struct SctkState { + pub(crate) connection: Connection, + + /// the cursor wl_surface + pub(crate) _cursor_surface: Option, + /// a memory pool + pub(crate) _multipool: Option>, + + // all present outputs + pub(crate) outputs: Vec, + // though (for now) only one seat will be active in an iced application at a time, all ought to be tracked + // Active seat is the first seat in the list + pub(crate) seats: Vec, + // Windows / Surfaces + /// Window list containing all SCTK windows. Since those windows aren't allowed + /// to be sent to other threads, they live on the event loop's thread + /// and requests from winit's windows are being forwarded to them either via + /// `WindowUpdate` or buffer on the associated with it `WindowHandle`. + pub(crate) windows: Vec>, + pub(crate) layer_surfaces: Vec>, + pub(crate) popups: Vec>, + pub(crate) lock_surfaces: Vec, + pub(crate) dnd_source: Option>, + pub(crate) _kbd_focus: Option, + + /// Window updates, which are coming from SCTK or the compositor, which require + /// calling back to the sctk's downstream. They are handled right in the event loop, + /// unlike the ones coming from buffers on the `WindowHandle`'s. + pub compositor_updates: Vec, + + /// data data_device + pub(crate) dnd_offer: Option, + pub(crate) _accept_counter: u32, + /// A sink for window and device events that is being filled during dispatching + /// event loop and forwarded downstream afterwards. + pub(crate) sctk_events: Vec, + pub(crate) frame_events: Vec, + + /// pending user events + pub pending_user_events: Vec>, + + // handles + pub(crate) queue_handle: QueueHandle, + pub(crate) loop_handle: LoopHandle<'static, Self>, + + // sctk state objects + /// Viewporter state on the given window. + pub viewporter_state: Option>, + pub(crate) fractional_scaling_manager: Option>, + pub(crate) registry_state: RegistryState, + pub(crate) seat_state: SeatState, + pub(crate) output_state: OutputState, + pub(crate) compositor_state: CompositorState, + pub(crate) shm_state: Shm, + pub(crate) xdg_shell_state: XdgShell, + pub(crate) layer_shell: Option, + pub(crate) data_device_manager_state: DataDeviceManagerState, + pub(crate) activation_state: Option, + pub(crate) session_lock_state: SessionLockState, + pub(crate) session_lock: Option, + pub(crate) token_ctr: u32, +} + +/// An error that occurred while running an application. +#[derive(Debug, thiserror::Error)] +pub enum PopupCreationError { + /// Positioner creation failed + #[error("Positioner creation failed")] + PositionerCreationFailed(GlobalError), + + /// The specified parent is missing + #[error("The specified parent is missing")] + ParentMissing, + + /// The specified size is missing + #[error("The specified size is missing")] + SizeMissing, + + /// Popup creation failed + #[error("Popup creation failed")] + PopupCreationFailed(GlobalError), +} + +/// An error that occurred while running an application. +#[derive(Debug, thiserror::Error)] +pub enum LayerSurfaceCreationError { + /// Layer shell is not supported by the compositor + #[error("Layer shell is not supported by the compositor")] + LayerShellNotSupported, + + /// WlSurface creation failed + #[error("WlSurface creation failed")] + WlSurfaceCreationFailed(GlobalError), + + /// LayerSurface creation failed + #[error("Layer Surface creation failed")] + LayerSurfaceCreationFailed(GlobalError), +} + +/// An error that occurred while starting a drag and drop operation. +#[derive(Debug, thiserror::Error)] +pub enum DndStartError {} + +impl SctkState { + pub fn scale_factor_changed( + &mut self, + surface: &WlSurface, + scale_factor: f64, + legacy: bool, + ) { + if let Some(window) = self + .windows + .iter_mut() + .find(|w| w.window.wl_surface() == surface) + { + if legacy && window.wp_fractional_scale.is_some() { + return; + } + window.scale_factor = Some(scale_factor); + if legacy { + let _ = window.window.set_buffer_scale(scale_factor as u32); + } + self.compositor_updates.push(SctkEvent::WindowEvent { + variant: WindowEventVariant::ScaleFactorChanged( + scale_factor, + window.wp_viewport.clone(), + ), + id: window.window.wl_surface().clone(), + }); + } + + if let Some(popup) = self + .popups + .iter_mut() + .find(|p| p.popup.wl_surface() == surface) + { + if legacy && popup.wp_fractional_scale.is_some() { + return; + } + popup.scale_factor = Some(scale_factor); + if legacy { + let _ = popup + .popup + .wl_surface() + .set_buffer_scale(scale_factor as _); + } + self.compositor_updates.push(SctkEvent::PopupEvent { + variant: PopupEventVariant::ScaleFactorChanged( + scale_factor, + popup.wp_viewport.clone(), + ), + id: popup.popup.wl_surface().clone(), + toplevel_id: popup.data.toplevel.clone(), + parent_id: popup.data.parent.wl_surface().clone(), + }); + } + + if let Some(layer_surface) = self + .layer_surfaces + .iter_mut() + .find(|l| l.surface.wl_surface() == surface) + { + if legacy && layer_surface.wp_fractional_scale.is_some() { + return; + } + layer_surface.scale_factor = Some(scale_factor); + if legacy { + let _ = + layer_surface.surface.set_buffer_scale(scale_factor as u32); + } + self.compositor_updates.push(SctkEvent::LayerSurfaceEvent { + variant: LayerSurfaceEventVariant::ScaleFactorChanged( + scale_factor, + layer_surface.wp_viewport.clone(), + ), + id: layer_surface.surface.wl_surface().clone(), + }); + } + + // TODO winit sets cursor size after handling the change for the window, so maybe that should be done as well. + } +} + +impl SctkState +where + T: 'static + Debug, +{ + pub fn get_popup( + &mut self, + settings: SctkPopupSettings, + ) -> Result<(window::Id, WlSurface, WlSurface, WlSurface), PopupCreationError> + { + let (parent, toplevel) = if let Some(parent) = + self.layer_surfaces.iter().find(|l| l.id == settings.parent) + { + ( + SctkSurface::LayerSurface(parent.surface.wl_surface().clone()), + parent.surface.wl_surface().clone(), + ) + } else if let Some(parent) = + self.windows.iter().find(|w| w.id == settings.parent) + { + ( + SctkSurface::Window(parent.window.wl_surface().clone()), + parent.window.wl_surface().clone(), + ) + } else if let Some(i) = self + .popups + .iter() + .position(|p| p.data.id == settings.parent) + { + let parent = &self.popups[i]; + ( + SctkSurface::Popup(parent.popup.wl_surface().clone()), + parent.data.toplevel.clone(), + ) + } else { + return Err(PopupCreationError::ParentMissing); + }; + + let size = if settings.positioner.size.is_none() { + return Err(PopupCreationError::SizeMissing); + } else { + settings.positioner.size.unwrap() + }; + + let positioner = XdgPositioner::new(&self.xdg_shell_state) + .map_err(PopupCreationError::PositionerCreationFailed)?; + positioner.set_anchor(settings.positioner.anchor); + positioner.set_anchor_rect( + settings.positioner.anchor_rect.x, + settings.positioner.anchor_rect.y, + settings.positioner.anchor_rect.width, + settings.positioner.anchor_rect.height, + ); + positioner.set_constraint_adjustment( + settings.positioner.constraint_adjustment, + ); + positioner.set_gravity(settings.positioner.gravity); + positioner.set_offset( + settings.positioner.offset.0, + settings.positioner.offset.1, + ); + if settings.positioner.reactive { + positioner.set_reactive(); + } + positioner.set_size(size.0 as i32, size.1 as i32); + + let grab = settings.grab; + + let wl_surface = + self.compositor_state.create_surface(&self.queue_handle); + + let (toplevel, popup) = match &parent { + SctkSurface::LayerSurface(parent) => { + let parent_layer_surface = self + .layer_surfaces + .iter() + .find(|w| w.surface.wl_surface() == parent) + .unwrap(); + let popup = Popup::from_surface( + None, + &positioner, + &self.queue_handle, + wl_surface.clone(), + &self.xdg_shell_state, + ) + .map_err(PopupCreationError::PopupCreationFailed)?; + parent_layer_surface.surface.get_popup(popup.xdg_popup()); + (parent_layer_surface.surface.wl_surface(), popup) + } + SctkSurface::Window(parent) => { + let parent_window = self + .windows + .iter() + .find(|w| w.window.wl_surface() == parent) + .unwrap(); + ( + parent_window.window.wl_surface(), + Popup::from_surface( + Some(parent_window.window.xdg_surface()), + &positioner, + &self.queue_handle, + wl_surface.clone(), + &self.xdg_shell_state, + ) + .map_err(PopupCreationError::PopupCreationFailed)?, + ) + } + SctkSurface::Popup(parent) => { + let parent_xdg = self + .windows + .iter() + .find_map(|w| { + if w.window.wl_surface() == parent { + Some(w.window.xdg_surface()) + } else { + None + } + }) + .unwrap(); + + ( + &toplevel, + Popup::from_surface( + Some(parent_xdg), + &positioner, + &self.queue_handle, + wl_surface.clone(), + &self.xdg_shell_state, + ) + .map_err(PopupCreationError::PopupCreationFailed)?, + ) + } + }; + if grab { + if let Some(s) = self.seats.first() { + popup.xdg_popup().grab( + &s.seat, + s.last_ptr_press.map(|p| p.2).unwrap_or_else(|| { + s.last_kbd_press + .as_ref() + .map(|p| p.1) + .unwrap_or_default() + }), + ) + } + } + wl_surface.commit(); + + let wp_viewport = self.viewporter_state.as_ref().map(|state| { + let viewport = + state.get_viewport(popup.wl_surface(), &self.queue_handle); + viewport.set_destination(size.0 as i32, size.1 as i32); + viewport + }); + let wp_fractional_scale = + self.fractional_scaling_manager.as_ref().map(|fsm| { + fsm.fractional_scaling(popup.wl_surface(), &self.queue_handle) + }); + + self.popups.push(SctkPopup { + popup: popup.clone(), + data: SctkPopupData { + id: settings.id, + parent: parent.clone(), + toplevel: toplevel.clone(), + positioner, + }, + last_configure: None, + _pending_requests: Default::default(), + wp_viewport, + wp_fractional_scale, + scale_factor: None, + }); + + Ok(( + settings.id, + parent.wl_surface().clone(), + toplevel.clone(), + popup.wl_surface().clone(), + )) + } + + pub fn get_window( + &mut self, + settings: SctkWindowSettings, + ) -> (window::Id, WlSurface) { + let SctkWindowSettings { + size, + client_decorations, + + window_id, + app_id, + title, + + size_limits, + resizable, + xdg_activation_token, + .. + } = settings; + // TODO Ashley: set window as opaque if transparency is false + // TODO Ashley: set icon + // TODO Ashley: save settings for window + // TODO Ashley: decorations + let wl_surface = + self.compositor_state.create_surface(&self.queue_handle); + let decorations: WindowDecorations = if client_decorations { + WindowDecorations::RequestClient + } else { + WindowDecorations::RequestServer + }; + let window = self.xdg_shell_state.create_window( + wl_surface.clone(), + decorations, + &self.queue_handle, + ); + if let Some(app_id) = app_id { + window.set_app_id(app_id); + } + // TODO better way of handling size limits + let min_size = size_limits.min(); + let min_size = if min_size.width as i32 <= 0 + || min_size.height as i32 <= 0 + || min_size.width > u16::MAX as f32 + || min_size.height > u16::MAX as f32 + { + None + } else { + Some((min_size.width as u32, min_size.height as u32)) + }; + let max_size: iced_futures::core::Size = size_limits.max(); + let max_size = if max_size.width as i32 <= 0 + || max_size.height as i32 <= 0 + || max_size.width > u16::MAX as f32 + || max_size.height > u16::MAX as f32 + { + None + } else { + Some((max_size.width as u32, max_size.height as u32)) + }; + if min_size.is_some() { + window.set_min_size(min_size); + } + if max_size.is_some() { + window.set_max_size(max_size); + } + + if let Some(title) = title { + window.set_title(title); + } + // if let Some(parent) = parent.and_then(|p| self.windows.iter().find(|w| w.window.wl_surface().id() == p)) { + // window.set_parent(Some(&parent.window)); + // } + window.xdg_surface().set_window_geometry( + 0, + 0, + size.0 as i32, + size.1 as i32, + ); + + window.commit(); + + if let (Some(token), Some(activation_state)) = + (xdg_activation_token, self.activation_state.as_ref()) + { + activation_state.activate::<()>(window.wl_surface(), token); + } + + let wp_viewport = self.viewporter_state.as_ref().map(|state| { + state.get_viewport(window.wl_surface(), &self.queue_handle) + }); + let wp_fractional_scale = + self.fractional_scaling_manager.as_ref().map(|fsm| { + fsm.fractional_scaling(window.wl_surface(), &self.queue_handle) + }); + + self.windows.push(SctkWindow { + id: window_id, + window, + scale_factor: None, + requested_size: Some(size), + current_size: Some(( + NonZeroU32::new(1).unwrap(), + NonZeroU32::new(1).unwrap(), + )), + last_configure: None, + _pending_requests: Vec::new(), + resizable, + wp_viewport, + wp_fractional_scale, + }); + (window_id, wl_surface) + } + + pub fn get_layer_surface( + &mut self, + SctkLayerSurfaceSettings { + id, + layer, + keyboard_interactivity, + pointer_interactivity, + anchor, + output, + namespace, + margin, + size, + exclusive_zone, + .. + }: SctkLayerSurfaceSettings, + ) -> Result<(window::Id, WlSurface), LayerSurfaceCreationError> { + let wl_output = match output { + IcedOutput::All => None, // TODO + IcedOutput::Active => None, + IcedOutput::Output(output) => Some(output), + }; + + let layer_shell = self + .layer_shell + .as_ref() + .ok_or(LayerSurfaceCreationError::LayerShellNotSupported)?; + let wl_surface = + self.compositor_state.create_surface(&self.queue_handle); + let mut size = size.unwrap(); + if anchor.contains(Anchor::BOTTOM.union(Anchor::TOP)) { + size.1 = None; + } + if anchor.contains(Anchor::LEFT.union(Anchor::RIGHT)) { + size.0 = None; + } + let layer_surface = layer_shell.create_layer_surface( + &self.queue_handle, + wl_surface.clone(), + layer, + Some(namespace), + wl_output.as_ref(), + ); + layer_surface.set_anchor(anchor); + layer_surface.set_keyboard_interactivity(keyboard_interactivity); + layer_surface.set_margin( + margin.top, + margin.right, + margin.bottom, + margin.left, + ); + layer_surface + .set_size(size.0.unwrap_or_default(), size.1.unwrap_or_default()); + layer_surface.set_exclusive_zone(exclusive_zone); + if !pointer_interactivity { + layer_surface.set_input_region(None); + } + layer_surface.commit(); + + let wp_viewport = self.viewporter_state.as_ref().map(|state| { + state.get_viewport(layer_surface.wl_surface(), &self.queue_handle) + }); + let wp_fractional_scale = + self.fractional_scaling_manager.as_ref().map(|fsm| { + fsm.fractional_scaling( + layer_surface.wl_surface(), + &self.queue_handle, + ) + }); + + self.layer_surfaces.push(SctkLayerSurface { + id, + surface: layer_surface, + requested_size: size, + current_size: None, + layer, + // builder needs to be refactored such that these fields are accessible + anchor, + keyboard_interactivity, + margin, + exclusive_zone, + last_configure: None, + _pending_requests: Vec::new(), + wp_viewport, + wp_fractional_scale, + scale_factor: None, + }); + Ok((id, wl_surface)) + } + pub fn get_lock_surface( + &mut self, + id: window::Id, + output: &WlOutput, + ) -> Option { + if let Some(lock) = self.session_lock.as_ref() { + let wl_surface = + self.compositor_state.create_surface(&self.queue_handle); + let session_lock_surface = lock.create_lock_surface( + wl_surface.clone(), + output, + &self.queue_handle, + ); + self.lock_surfaces.push(SctkLockSurface { + id, + session_lock_surface, + last_configure: None, + }); + Some(wl_surface) + } else { + None + } + } +} diff --git a/sctk/src/handlers/activation.rs b/sctk/src/handlers/activation.rs new file mode 100644 index 0000000000..e310d1a979 --- /dev/null +++ b/sctk/src/handlers/activation.rs @@ -0,0 +1,60 @@ +use std::sync::Mutex; + +use sctk::{ + activation::{ActivationHandler, RequestData, RequestDataExt}, + delegate_activation, + reexports::client::protocol::{wl_seat::WlSeat, wl_surface::WlSurface}, +}; + +use crate::event_loop::state::SctkState; + +pub struct IcedRequestData { + data: RequestData, + message: Mutex< + Option) -> T + Send + Sync + 'static>>, + >, +} + +impl IcedRequestData { + pub fn new( + data: RequestData, + message: Box) -> T + Send + Sync + 'static>, + ) -> IcedRequestData { + IcedRequestData { + data, + message: Mutex::new(Some(message)), + } + } +} + +impl RequestDataExt for IcedRequestData { + fn app_id(&self) -> Option<&str> { + self.data.app_id() + } + + fn seat_and_serial(&self) -> Option<(&WlSeat, u32)> { + self.data.seat_and_serial() + } + + fn surface(&self) -> Option<&WlSurface> { + self.data.surface() + } +} + +impl ActivationHandler for SctkState { + type RequestData = IcedRequestData; + + fn new_token(&mut self, token: String, data: &Self::RequestData) { + if let Some(message) = data.message.lock().unwrap().take() { + self.pending_user_events.push( + crate::application::Event::SctkEvent( + crate::sctk_event::IcedSctkEvent::UserEvent(message(Some( + token, + ))), + ), + ); + } // else the compositor send two tokens??? + } +} + +delegate_activation!(@ SctkState, IcedRequestData); diff --git a/sctk/src/handlers/compositor.rs b/sctk/src/handlers/compositor.rs new file mode 100644 index 0000000000..3e77b21dfc --- /dev/null +++ b/sctk/src/handlers/compositor.rs @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MPL-2.0-only +use sctk::{ + compositor::CompositorHandler, + delegate_compositor, + reexports::client::{protocol::wl_surface, Connection, Proxy, QueueHandle}, + shell::WaylandSurface, +}; +use std::fmt::Debug; + +use crate::{event_loop::state::SctkState, sctk_event::SctkEvent}; + +impl CompositorHandler for SctkState { + fn scale_factor_changed( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + surface: &wl_surface::WlSurface, + new_factor: i32, + ) { + self.scale_factor_changed(surface, new_factor as f64, true); + } + + fn frame( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + surface: &wl_surface::WlSurface, + _time: u32, + ) { + self.frame_events.push(surface.clone()); + } + + fn transform_changed( + &mut self, + conn: &Connection, + qh: &QueueHandle, + surface: &wl_surface::WlSurface, + new_transform: sctk::reexports::client::protocol::wl_output::Transform, + ) { + // TODO + // this is not required + } +} + +delegate_compositor!(@ SctkState); diff --git a/sctk/src/handlers/data_device/data_device.rs b/sctk/src/handlers/data_device/data_device.rs new file mode 100644 index 0000000000..0adf03c354 --- /dev/null +++ b/sctk/src/handlers/data_device/data_device.rs @@ -0,0 +1,140 @@ +use std::fmt::Debug; + +use sctk::{ + data_device_manager::{ + data_device::{DataDevice, DataDeviceHandler}, + data_offer::DragOffer, + }, + reexports::client::{protocol::wl_data_device, Connection, QueueHandle}, +}; + +use crate::{ + event_loop::state::{SctkDragOffer, SctkState}, + sctk_event::{DndOfferEvent, SctkEvent}, +}; + +impl DataDeviceHandler for SctkState { + fn enter( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + wl_data_device: &wl_data_device::WlDataDevice, + ) { + let data_device = if let Some(seat) = self + .seats + .iter() + .find(|s| s.data_device.inner() == wl_data_device) + { + &seat.data_device + } else { + return; + }; + + let drag_offer = data_device.data().drag_offer().unwrap(); + let mime_types = drag_offer.with_mime_types(|types| types.to_vec()); + self.dnd_offer = Some(SctkDragOffer { + dropped: false, + offer: drag_offer.clone(), + cur_read: None, + }); + self.sctk_events.push(SctkEvent::DndOffer { + event: DndOfferEvent::Enter { + mime_types, + x: drag_offer.x, + y: drag_offer.y, + }, + surface: drag_offer.surface.clone(), + }); + } + + fn leave( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _wl_data_device: &wl_data_device::WlDataDevice, + ) { + // ASHLEY TODO the dnd_offer should be removed when the leave event is received + // but for now it is not if the offer was previously dropped. + // It seems that leave events are received even for offers which have + // been accepted and need to be read. + if let Some(dnd_offer) = self.dnd_offer.take() { + if dnd_offer.dropped { + self.dnd_offer = Some(dnd_offer); + return; + } + + self.sctk_events.push(SctkEvent::DndOffer { + event: DndOfferEvent::Leave, + surface: dnd_offer.offer.surface.clone(), + }); + } + } + + fn motion( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + wl_data_device: &wl_data_device::WlDataDevice, + ) { + let data_device = if let Some(seat) = self + .seats + .iter() + .find(|s| s.data_device.inner() == wl_data_device) + { + &seat.data_device + } else { + return; + }; + + let offer = data_device.data().drag_offer(); + // if the offer is not the same as the current one, ignore the leave event + if offer.as_ref() != self.dnd_offer.as_ref().map(|o| &o.offer) { + return; + } + let DragOffer { x, y, surface, .. } = + data_device.data().drag_offer().unwrap(); + self.sctk_events.push(SctkEvent::DndOffer { + event: DndOfferEvent::Motion { x, y }, + surface: surface.clone(), + }); + } + + fn selection( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _wl_data_device: &wl_data_device::WlDataDevice, + ) { + // not handled here + } + + fn drop_performed( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + wl_data_device: &wl_data_device::WlDataDevice, + ) { + let data_device = if let Some(seat) = self + .seats + .iter() + .find(|s| s.data_device.inner() == wl_data_device) + { + &seat.data_device + } else { + return; + }; + + if let Some(offer) = data_device.data().drag_offer() { + if let Some(dnd_offer) = self.dnd_offer.as_mut() { + if offer != dnd_offer.offer { + return; + } + dnd_offer.dropped = true; + } + self.sctk_events.push(SctkEvent::DndOffer { + event: DndOfferEvent::DropPerformed, + surface: offer.surface.clone(), + }); + } + } +} diff --git a/sctk/src/handlers/data_device/data_offer.rs b/sctk/src/handlers/data_device/data_offer.rs new file mode 100644 index 0000000000..b56f5810bd --- /dev/null +++ b/sctk/src/handlers/data_device/data_offer.rs @@ -0,0 +1,57 @@ +use sctk::{ + data_device_manager::data_offer::{DataOfferHandler, DragOffer}, + reexports::client::{ + protocol::wl_data_device_manager::DndAction, Connection, QueueHandle, + }, +}; +use std::fmt::Debug; + +use crate::event_loop::state::SctkState; + +impl DataOfferHandler for SctkState { + fn source_actions( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + offer: &mut DragOffer, + actions: DndAction, + ) { + if self + .dnd_offer + .as_ref() + .map(|o| o.offer.inner() == offer.inner()) + .unwrap_or(false) + { + self.sctk_events + .push(crate::sctk_event::SctkEvent::DndOffer { + event: crate::sctk_event::DndOfferEvent::SourceActions( + actions, + ), + surface: offer.surface.clone(), + }); + } + } + + fn selected_action( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + offer: &mut DragOffer, + actions: DndAction, + ) { + if self + .dnd_offer + .as_ref() + .map(|o| o.offer.inner() == offer.inner()) + .unwrap_or(false) + { + self.sctk_events + .push(crate::sctk_event::SctkEvent::DndOffer { + event: crate::sctk_event::DndOfferEvent::SelectedAction( + actions, + ), + surface: offer.surface.clone(), + }); + } + } +} diff --git a/sctk/src/handlers/data_device/data_source.rs b/sctk/src/handlers/data_device/data_source.rs new file mode 100644 index 0000000000..834cccc483 --- /dev/null +++ b/sctk/src/handlers/data_device/data_source.rs @@ -0,0 +1,200 @@ +use crate::event_loop::state::SctkState; +use crate::sctk_event::{DataSourceEvent, SctkEvent}; +use sctk::data_device_manager::WritePipe; +use sctk::{ + data_device_manager::data_source::DataSourceHandler, + reexports::{ + calloop::PostAction, + client::{ + protocol::{ + wl_data_device_manager::DndAction, wl_data_source::WlDataSource, + }, + Connection, QueueHandle, + }, + }, +}; +use std::io::{BufWriter, Write}; +use tracing::error; + +impl DataSourceHandler for SctkState { + fn accept_mime( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + source: &WlDataSource, + mime: Option, + ) { + let is_active_source = self + .dnd_source + .as_ref() + .and_then(|s| (s.source.as_ref().map(|s| s.0.inner() == source))) + .unwrap_or(false); + if is_active_source { + self.sctk_events.push(SctkEvent::DataSource( + DataSourceEvent::MimeAccepted(mime), + )); + } + } + + fn send_request( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + source: &WlDataSource, + mime: String, + pipe: WritePipe, + ) { + let is_active_source = self + .dnd_source + .as_ref() + .and_then(|s| s.source.as_ref().map(|s| s.0.inner() == source)) + .unwrap_or(false); + + if !is_active_source { + source.destroy(); + return; + } + + if let Some(source) = self.dnd_source.as_mut().filter(|s| { + s.source + .as_ref() + .map(|s| (s.0.inner() == source)) + .unwrap_or(false) + }) { + let (_my_source, data) = match source.source.as_ref() { + Some((source, data)) => (source, data), + None => return, + }; + match self.loop_handle.insert_source( + pipe, + move |_, f, state| -> PostAction { + let loop_handle = &state.loop_handle; + let dnd_source = match state.dnd_source.as_mut() { + Some(s) => s, + None => return PostAction::Continue, + }; + let (data, mut cur_index, token) = + match dnd_source.cur_write.take() { + Some(s) => s, + None => return PostAction::Continue, + }; + let mut writer = BufWriter::new(f.as_ref()); + let slice = &data.as_slice()[cur_index + ..(cur_index + writer.capacity()).min(data.len())]; + match writer.write(slice) { + Ok(num_written) => { + cur_index += num_written; + if cur_index == data.len() { + loop_handle.remove(token); + } else { + dnd_source.cur_write = + Some((data, cur_index, token)); + } + if let Err(err) = writer.flush() { + loop_handle.remove(token); + error!("Failed to flush pipe: {}", err); + } + } + Err(e) + if matches!( + e.kind(), + std::io::ErrorKind::Interrupted + ) => + { + // try again + dnd_source.cur_write = + Some((data, cur_index, token)); + } + Err(_) => { + loop_handle.remove(token); + error!("Failed to write to pipe"); + } + }; + PostAction::Continue + }, + ) { + Ok(s) => { + source.cur_write = Some(( + data.from_mime_type(&mime).unwrap_or_default(), + 0, + s, + )); + } + Err(_) => { + error!("Failed to insert source"); + } + }; + } + } + + fn cancelled( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + source: &WlDataSource, + ) { + let is_active_source = self + .dnd_source + .as_ref() + .and_then(|s| s.source.as_ref().map(|s| s.0.inner() == source)) + .unwrap_or(false); + if is_active_source { + self.sctk_events + .push(SctkEvent::DataSource(DataSourceEvent::DndCancelled)); + } + } + + fn dnd_dropped( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + source: &WlDataSource, + ) { + let is_active_source = self + .dnd_source + .as_ref() + .and_then(|s| (s.source.as_ref().map(|s| s.0.inner() == source))) + .unwrap_or(false); + if is_active_source { + self.sctk_events + .push(SctkEvent::DataSource(DataSourceEvent::DndDropPerformed)); + } + } + + fn dnd_finished( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + source: &WlDataSource, + ) { + let is_active_source = self + .dnd_source + .as_ref() + .and_then(|s| (s.source.as_ref().map(|s| s.0.inner() == source))) + .unwrap_or(false); + if is_active_source { + self.sctk_events + .push(SctkEvent::DataSource(DataSourceEvent::DndFinished)); + } + } + + fn action( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + source: &WlDataSource, + action: DndAction, + ) { + let is_active_source = self + .dnd_source + .as_ref() + .and_then(|s| (s.source.as_ref().map(|s| s.0.inner() == source))) + .unwrap_or(false); + if is_active_source { + self.sctk_events + .push(crate::sctk_event::SctkEvent::DataSource( + DataSourceEvent::DndActionAccepted(action), + )); + } + } +} diff --git a/sctk/src/handlers/data_device/mod.rs b/sctk/src/handlers/data_device/mod.rs new file mode 100644 index 0000000000..f0f2d482e9 --- /dev/null +++ b/sctk/src/handlers/data_device/mod.rs @@ -0,0 +1,9 @@ +use crate::handlers::SctkState; +use sctk::delegate_data_device; +use std::fmt::Debug; + +pub mod data_device; +pub mod data_offer; +pub mod data_source; + +delegate_data_device!(@ SctkState); diff --git a/sctk/src/handlers/mod.rs b/sctk/src/handlers/mod.rs new file mode 100644 index 0000000000..332d296682 --- /dev/null +++ b/sctk/src/handlers/mod.rs @@ -0,0 +1,41 @@ +// handlers +pub mod activation; +pub mod compositor; +pub mod data_device; +pub mod output; +pub mod seat; +pub mod session_lock; +pub mod shell; +pub mod wp_fractional_scaling; +pub mod wp_viewporter; + +use sctk::{ + delegate_registry, delegate_shm, + output::OutputState, + registry::{ProvidesRegistryState, RegistryState}, + registry_handlers, + seat::SeatState, + shm::{Shm, ShmHandler}, +}; +use std::fmt::Debug; + +use crate::event_loop::state::SctkState; + +impl ShmHandler for SctkState { + fn shm_state(&mut self) -> &mut Shm { + &mut self.shm_state + } +} + +impl ProvidesRegistryState for SctkState +where + T: 'static, +{ + fn registry(&mut self) -> &mut RegistryState { + &mut self.registry_state + } + registry_handlers![OutputState, SeatState,]; +} + +delegate_shm!(@ SctkState); +delegate_registry!(@ SctkState); diff --git a/sctk/src/handlers/output.rs b/sctk/src/handlers/output.rs new file mode 100644 index 0000000000..f0725c08cc --- /dev/null +++ b/sctk/src/handlers/output.rs @@ -0,0 +1,48 @@ +use crate::{event_loop::state::SctkState, sctk_event::SctkEvent}; +use sctk::{delegate_output, output::OutputHandler}; +use std::fmt::Debug; + +impl OutputHandler for SctkState { + fn output_state(&mut self) -> &mut sctk::output::OutputState { + &mut self.output_state + } + + fn new_output( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + output: sctk::reexports::client::protocol::wl_output::WlOutput, + ) { + self.sctk_events.push(SctkEvent::NewOutput { + id: output.clone(), + info: self.output_state.info(&output), + }); + self.outputs.push(output); + } + + fn update_output( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + output: sctk::reexports::client::protocol::wl_output::WlOutput, + ) { + if let Some(info) = self.output_state.info(&output) { + self.sctk_events.push(SctkEvent::UpdateOutput { + id: output.clone(), + info, + }); + } + } + + fn output_destroyed( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + output: sctk::reexports::client::protocol::wl_output::WlOutput, + ) { + self.sctk_events.push(SctkEvent::RemovedOutput(output)); + // TODO clean up any layer surfaces on this output? + } +} + +delegate_output!(@ SctkState); diff --git a/sctk/src/handlers/seat/keyboard.rs b/sctk/src/handlers/seat/keyboard.rs new file mode 100644 index 0000000000..c150c077bf --- /dev/null +++ b/sctk/src/handlers/seat/keyboard.rs @@ -0,0 +1,200 @@ +use crate::{ + event_loop::state::SctkState, + sctk_event::{KeyboardEventVariant, SctkEvent}, +}; + +use sctk::{ + delegate_keyboard, + seat::keyboard::{KeyboardHandler, Keysym}, +}; +use std::fmt::Debug; + +impl KeyboardHandler for SctkState { + fn enter( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + keyboard: &sctk::reexports::client::protocol::wl_keyboard::WlKeyboard, + surface: &sctk::reexports::client::protocol::wl_surface::WlSurface, + _serial: u32, + _raw: &[u32], + _keysyms: &[Keysym], + ) { + let (i, mut is_active, seat) = { + let (i, is_active, my_seat) = + match self.seats.iter_mut().enumerate().find_map(|(i, s)| { + if s.kbd.as_ref() == Some(keyboard) { + Some((i, s)) + } else { + None + } + }) { + Some((i, s)) => (i, i == 0, s), + None => return, + }; + my_seat.kbd_focus.replace(surface.clone()); + + let seat = my_seat.seat.clone(); + (i, is_active, seat) + }; + + // TODO Ashley: thoroughly test this + // swap the active seat to be the current seat if the current "active" seat is not focused on the application anyway + if !is_active && self.seats[0].kbd_focus.is_none() { + is_active = true; + self.seats.swap(0, i); + } + + if is_active { + self.sctk_events.push(SctkEvent::KeyboardEvent { + variant: KeyboardEventVariant::Enter(surface.clone()), + kbd_id: keyboard.clone(), + seat_id: seat, + }) + } + } + + fn leave( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + keyboard: &sctk::reexports::client::protocol::wl_keyboard::WlKeyboard, + surface: &sctk::reexports::client::protocol::wl_surface::WlSurface, + _serial: u32, + ) { + let (is_active, seat, kbd) = { + let (is_active, my_seat) = + match self.seats.iter_mut().enumerate().find_map(|(i, s)| { + if s.kbd.as_ref() == Some(keyboard) { + Some((i, s)) + } else { + None + } + }) { + Some((i, s)) => (i == 0, s), + None => return, + }; + let seat = my_seat.seat.clone(); + let kbd = keyboard.clone(); + my_seat.kbd_focus.take(); + (is_active, seat, kbd) + }; + + if is_active { + self.sctk_events.push(SctkEvent::KeyboardEvent { + variant: KeyboardEventVariant::Leave(surface.clone()), + kbd_id: kbd, + seat_id: seat, + }); + // if there is another seat with a keyboard focused on a surface make that the new active seat + if let Some(i) = + self.seats.iter().position(|s| s.kbd_focus.is_some()) + { + self.seats.swap(0, i); + let s = &self.seats[0]; + self.sctk_events.push(SctkEvent::KeyboardEvent { + variant: KeyboardEventVariant::Enter( + s.kbd_focus.clone().unwrap(), + ), + kbd_id: s.kbd.clone().unwrap(), + seat_id: s.seat.clone(), + }) + } + } + } + + fn press_key( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + keyboard: &sctk::reexports::client::protocol::wl_keyboard::WlKeyboard, + serial: u32, + event: sctk::seat::keyboard::KeyEvent, + ) { + let (is_active, my_seat) = + match self.seats.iter_mut().enumerate().find_map(|(i, s)| { + if s.kbd.as_ref() == Some(keyboard) { + Some((i, s)) + } else { + None + } + }) { + Some((i, s)) => (i == 0, s), + None => return, + }; + let seat_id = my_seat.seat.clone(); + let kbd_id = keyboard.clone(); + my_seat.last_kbd_press.replace((event.clone(), serial)); + if is_active { + self.sctk_events.push(SctkEvent::KeyboardEvent { + variant: KeyboardEventVariant::Press(event), + kbd_id, + seat_id, + }); + } + } + + fn release_key( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + keyboard: &sctk::reexports::client::protocol::wl_keyboard::WlKeyboard, + _serial: u32, + event: sctk::seat::keyboard::KeyEvent, + ) { + let (is_active, my_seat) = + match self.seats.iter_mut().enumerate().find_map(|(i, s)| { + if s.kbd.as_ref() == Some(keyboard) { + Some((i, s)) + } else { + None + } + }) { + Some((i, s)) => (i == 0, s), + None => return, + }; + let seat_id = my_seat.seat.clone(); + let kbd_id = keyboard.clone(); + + if is_active { + self.sctk_events.push(SctkEvent::KeyboardEvent { + variant: KeyboardEventVariant::Release(event), + kbd_id, + seat_id, + }); + } + } + + fn update_modifiers( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + keyboard: &sctk::reexports::client::protocol::wl_keyboard::WlKeyboard, + _serial: u32, + modifiers: sctk::seat::keyboard::Modifiers, + ) { + let (is_active, my_seat) = + match self.seats.iter_mut().enumerate().find_map(|(i, s)| { + if s.kbd.as_ref() == Some(keyboard) { + Some((i, s)) + } else { + None + } + }) { + Some((i, s)) => (i == 0, s), + None => return, + }; + let seat_id = my_seat.seat.clone(); + let kbd_id = keyboard.clone(); + + if is_active { + self.sctk_events.push(SctkEvent::KeyboardEvent { + variant: KeyboardEventVariant::Modifiers(modifiers), + kbd_id, + seat_id, + }) + } + } +} + +delegate_keyboard!(@ SctkState); diff --git a/sctk/src/handlers/seat/mod.rs b/sctk/src/handlers/seat/mod.rs new file mode 100644 index 0000000000..38369b437b --- /dev/null +++ b/sctk/src/handlers/seat/mod.rs @@ -0,0 +1,5 @@ +// TODO support multi-seat handling +pub mod keyboard; +pub mod pointer; +pub mod seat; +pub mod touch; diff --git a/sctk/src/handlers/seat/pointer.rs b/sctk/src/handlers/seat/pointer.rs new file mode 100644 index 0000000000..9777a320e4 --- /dev/null +++ b/sctk/src/handlers/seat/pointer.rs @@ -0,0 +1,163 @@ +use crate::{event_loop::state::SctkState, sctk_event::SctkEvent}; +use sctk::{ + delegate_pointer, + reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge, + seat::pointer::{CursorIcon, PointerEventKind, PointerHandler, BTN_LEFT}, + shell::WaylandSurface, +}; +use std::fmt::Debug; + +impl PointerHandler for SctkState { + fn pointer_frame( + &mut self, + conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + pointer: &sctk::reexports::client::protocol::wl_pointer::WlPointer, + events: &[sctk::seat::pointer::PointerEvent], + ) { + let (is_active, my_seat) = + match self.seats.iter_mut().enumerate().find_map(|(i, s)| { + if s.ptr.as_ref().map(|p| p.pointer()) == Some(pointer) { + Some((i, s)) + } else { + None + } + }) { + Some((i, s)) => (i == 0, s), + None => return, + }; + + // track events, but only forward for the active seat + for e in events { + // check if it is over a resizable window's border and handle the event yourself if it is. + if let Some((resize_edge, window)) = self + .windows + .iter() + .find(|w| w.window.wl_surface() == &e.surface) + .and_then(|w| { + w.resizable.zip(w.current_size).and_then( + |(border, (width, height))| { + let (width, height) = + (width.get() as f64, height.get() as f64); + let (x, y) = e.position; + let left_edge = x < border; + let top_edge = y < border; + let right_edge = x > width - border; + let bottom_edge = y > height - border; + + if left_edge && top_edge { + Some((ResizeEdge::TopLeft, w)) + } else if left_edge && bottom_edge { + Some((ResizeEdge::BottomLeft, w)) + } else if right_edge && top_edge { + Some((ResizeEdge::TopRight, w)) + } else if right_edge && bottom_edge { + Some((ResizeEdge::BottomRight, w)) + } else if left_edge { + Some((ResizeEdge::Left, w)) + } else if right_edge { + Some((ResizeEdge::Right, w)) + } else if top_edge { + Some((ResizeEdge::Top, w)) + } else if bottom_edge { + Some((ResizeEdge::Bottom, w)) + } else { + None + } + }, + ) + }) + { + let icon = match resize_edge { + ResizeEdge::Top => CursorIcon::NResize, + ResizeEdge::Bottom => CursorIcon::SResize, + ResizeEdge::Left => CursorIcon::WResize, + ResizeEdge::TopLeft => CursorIcon::NwResize, + ResizeEdge::BottomLeft => CursorIcon::SwResize, + ResizeEdge::Right => CursorIcon::EResize, + ResizeEdge::TopRight => CursorIcon::NeResize, + ResizeEdge::BottomRight => CursorIcon::SeResize, + _ => unimplemented!(), + }; + match e.kind { + PointerEventKind::Press { + time, + button, + serial, + } if button == BTN_LEFT => { + my_seat.last_ptr_press.replace((time, button, serial)); + window.window.resize( + &my_seat.seat, + serial, + resize_edge, + ); + return; + } + PointerEventKind::Motion { .. } => { + if my_seat.icon != Some(icon) { + let _ = my_seat + .ptr + .as_ref() + .unwrap() + .set_cursor(conn, icon); + my_seat.icon = Some(icon); + } + return; + } + PointerEventKind::Enter { .. } => { + my_seat.ptr_focus.replace(e.surface.clone()); + if my_seat.icon != Some(icon) { + let _ = my_seat + .ptr + .as_ref() + .unwrap() + .set_cursor(conn, icon); + my_seat.icon = Some(icon); + } + } + PointerEventKind::Leave { .. } => { + my_seat.ptr_focus.take(); + my_seat.icon = None; + } + _ => {} + } + let _ = my_seat.ptr.as_ref().unwrap().set_cursor(conn, icon); + } else if my_seat.icon.is_some() { + let _ = my_seat + .ptr + .as_ref() + .unwrap() + .set_cursor(conn, CursorIcon::Default); + my_seat.icon = None; + } + + if is_active { + self.sctk_events.push(SctkEvent::PointerEvent { + variant: e.clone(), + ptr_id: pointer.clone(), + seat_id: my_seat.seat.clone(), + }); + } + match e.kind { + PointerEventKind::Enter { .. } => { + my_seat.ptr_focus.replace(e.surface.clone()); + } + PointerEventKind::Leave { .. } => { + my_seat.ptr_focus.take(); + my_seat.icon = None; + } + PointerEventKind::Press { + time, + button, + serial, + } => { + my_seat.last_ptr_press.replace((time, button, serial)); + } + // TODO revisit events that ought to be handled and change internal state + _ => {} + } + } + } +} + +delegate_pointer!(@ SctkState); diff --git a/sctk/src/handlers/seat/seat.rs b/sctk/src/handlers/seat/seat.rs new file mode 100644 index 0000000000..cf28572a7d --- /dev/null +++ b/sctk/src/handlers/seat/seat.rs @@ -0,0 +1,191 @@ +use crate::{ + event_loop::{state::SctkSeat, state::SctkState}, + sctk_event::{KeyboardEventVariant, SctkEvent, SeatEventVariant}, +}; +use iced_runtime::keyboard::Modifiers; +use sctk::{ + delegate_seat, + reexports::client::{protocol::wl_keyboard::WlKeyboard, Proxy}, + seat::{pointer::ThemeSpec, SeatHandler}, +}; +use std::fmt::Debug; + +impl SeatHandler for SctkState +where + T: 'static, +{ + fn seat_state(&mut self) -> &mut sctk::seat::SeatState { + &mut self.seat_state + } + + fn new_seat( + &mut self, + _conn: &sctk::reexports::client::Connection, + qh: &sctk::reexports::client::QueueHandle, + seat: sctk::reexports::client::protocol::wl_seat::WlSeat, + ) { + self.sctk_events.push(SctkEvent::SeatEvent { + variant: SeatEventVariant::New, + id: seat.clone(), + }); + let data_device = + self.data_device_manager_state.get_data_device(qh, &seat); + self.seats.push(SctkSeat { + seat, + kbd: None, + ptr: None, + _touch: None, + data_device, + _modifiers: Modifiers::default(), + kbd_focus: None, + ptr_focus: None, + last_ptr_press: None, + last_kbd_press: None, + icon: None, + }); + } + + fn new_capability( + &mut self, + _conn: &sctk::reexports::client::Connection, + qh: &sctk::reexports::client::QueueHandle, + seat: sctk::reexports::client::protocol::wl_seat::WlSeat, + capability: sctk::seat::Capability, + ) { + let my_seat = match self.seats.iter_mut().find(|s| s.seat == seat) { + Some(s) => s, + None => { + self.seats.push(SctkSeat { + seat: seat.clone(), + kbd: None, + ptr: None, + _touch: None, + data_device: self + .data_device_manager_state + .get_data_device(qh, &seat), + _modifiers: Modifiers::default(), + kbd_focus: None, + ptr_focus: None, + last_ptr_press: None, + last_kbd_press: None, + icon: None, + }); + self.seats.last_mut().unwrap() + } + }; + // TODO data device + match capability { + sctk::seat::Capability::Keyboard => { + let seat_clone = seat.clone(); + if let Ok(kbd) = self.seat_state.get_keyboard_with_repeat( + qh, + &seat, + None, + self.loop_handle.clone(), + Box::new(move |state, kbd: &WlKeyboard, e| { + state.sctk_events.push(SctkEvent::KeyboardEvent { + variant: KeyboardEventVariant::Repeat(e), + kbd_id: kbd.clone(), + seat_id: seat_clone.clone(), + }); + }), + ) { + self.sctk_events.push(SctkEvent::SeatEvent { + variant: SeatEventVariant::NewCapability( + capability, + kbd.id(), + ), + id: seat.clone(), + }); + my_seat.kbd.replace(kbd); + } + } + sctk::seat::Capability::Pointer => { + let surface = self.compositor_state.create_surface(qh); + + if let Ok(ptr) = self.seat_state.get_pointer_with_theme( + qh, + &seat, + self.shm_state.wl_shm(), + surface, + ThemeSpec::default(), + ) { + self.sctk_events.push(SctkEvent::SeatEvent { + variant: SeatEventVariant::NewCapability( + capability, + ptr.pointer().id(), + ), + id: seat.clone(), + }); + my_seat.ptr.replace(ptr); + } + } + sctk::seat::Capability::Touch => { + // TODO touch + } + _ => unimplemented!(), + } + } + + fn remove_capability( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + seat: sctk::reexports::client::protocol::wl_seat::WlSeat, + capability: sctk::seat::Capability, + ) { + let my_seat = match self.seats.iter_mut().find(|s| s.seat == seat) { + Some(s) => s, + None => return, + }; + + // TODO data device + match capability { + // TODO use repeating kbd? + sctk::seat::Capability::Keyboard => { + if let Some(kbd) = my_seat.kbd.take() { + self.sctk_events.push(SctkEvent::SeatEvent { + variant: SeatEventVariant::RemoveCapability( + capability, + kbd.id(), + ), + id: seat.clone(), + }); + } + } + sctk::seat::Capability::Pointer => { + if let Some(ptr) = my_seat.ptr.take() { + self.sctk_events.push(SctkEvent::SeatEvent { + variant: SeatEventVariant::RemoveCapability( + capability, + ptr.pointer().id(), + ), + id: seat.clone(), + }); + } + } + sctk::seat::Capability::Touch => { + // TODO touch + // my_seat.touch = self.seat_state.get_touch(qh, &seat).ok(); + } + _ => unimplemented!(), + } + } + + fn remove_seat( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + seat: sctk::reexports::client::protocol::wl_seat::WlSeat, + ) { + self.sctk_events.push(SctkEvent::SeatEvent { + variant: SeatEventVariant::Remove, + id: seat.clone(), + }); + if let Some(i) = self.seats.iter().position(|s| s.seat == seat) { + self.seats.remove(i); + } + } +} + +delegate_seat!(@ SctkState); diff --git a/sctk/src/handlers/seat/touch.rs b/sctk/src/handlers/seat/touch.rs new file mode 100644 index 0000000000..70b786d12e --- /dev/null +++ b/sctk/src/handlers/seat/touch.rs @@ -0,0 +1 @@ +// TODO diff --git a/sctk/src/handlers/session_lock.rs b/sctk/src/handlers/session_lock.rs new file mode 100644 index 0000000000..16d6322610 --- /dev/null +++ b/sctk/src/handlers/session_lock.rs @@ -0,0 +1,57 @@ +use crate::{handlers::SctkState, sctk_event::SctkEvent}; +use sctk::{ + delegate_session_lock, + reexports::client::{Connection, QueueHandle}, + session_lock::{ + SessionLock, SessionLockHandler, SessionLockSurface, + SessionLockSurfaceConfigure, + }, +}; +use std::fmt::Debug; + +impl SessionLockHandler for SctkState { + fn locked( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _session_lock: SessionLock, + ) { + self.sctk_events.push(SctkEvent::SessionLocked); + } + + fn finished( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _session_lock: SessionLock, + ) { + self.sctk_events.push(SctkEvent::SessionLockFinished); + } + + fn configure( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + session_lock_surface: SessionLockSurface, + configure: SessionLockSurfaceConfigure, + _serial: u32, + ) { + let lock_surface = match self.lock_surfaces.iter_mut().find(|s| { + s.session_lock_surface.wl_surface() + == session_lock_surface.wl_surface() + }) { + Some(l) => l, + None => return, + }; + let first = lock_surface.last_configure.is_none(); + lock_surface.last_configure.replace(configure.clone()); + self.sctk_events + .push(SctkEvent::SessionLockSurfaceConfigure { + surface: session_lock_surface.wl_surface().clone(), + configure, + first, + }); + } +} + +delegate_session_lock!(@ SctkState); diff --git a/sctk/src/handlers/shell/layer.rs b/sctk/src/handlers/shell/layer.rs new file mode 100644 index 0000000000..e45f5e9387 --- /dev/null +++ b/sctk/src/handlers/shell/layer.rs @@ -0,0 +1,113 @@ +use crate::{ + dpi::LogicalSize, + event_loop::state::SctkState, + sctk_event::{LayerSurfaceEventVariant, SctkEvent}, +}; +use sctk::{ + delegate_layer, + reexports::client::Proxy, + shell::{ + wlr_layer::{Anchor, KeyboardInteractivity, LayerShellHandler}, + WaylandSurface, + }, +}; +use std::fmt::Debug; + +impl LayerShellHandler for SctkState { + fn closed( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + layer: &sctk::shell::wlr_layer::LayerSurface, + ) { + let layer = match self.layer_surfaces.iter().position(|s| { + s.surface.wl_surface().id() == layer.wl_surface().id() + }) { + Some(w) => self.layer_surfaces.remove(w), + None => return, + }; + + self.sctk_events.push(SctkEvent::LayerSurfaceEvent { + variant: LayerSurfaceEventVariant::Done, + id: layer.surface.wl_surface().clone(), + }) + // TODO popup cleanup + } + + fn configure( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + layer: &sctk::shell::wlr_layer::LayerSurface, + mut configure: sctk::shell::wlr_layer::LayerSurfaceConfigure, + _serial: u32, + ) { + let layer = + match self.layer_surfaces.iter_mut().find(|s| { + s.surface.wl_surface().id() == layer.wl_surface().id() + }) { + Some(l) => l, + None => return, + }; + configure.new_size.0 = if let Some(w) = layer.requested_size.0 { + w + } else { + configure.new_size.0.max(1) + }; + configure.new_size.1 = if let Some(h) = layer.requested_size.1 { + h + } else { + configure.new_size.1.max(1) + }; + + layer.update_viewport(configure.new_size.0, configure.new_size.1); + let first = layer.last_configure.is_none(); + layer.last_configure.replace(configure.clone()); + + self.sctk_events.push(SctkEvent::LayerSurfaceEvent { + variant: LayerSurfaceEventVariant::Configure( + configure, + layer.surface.wl_surface().clone(), + first, + ), + id: layer.surface.wl_surface().clone(), + }); + self.frame_events.push(layer.surface.wl_surface().clone()); + } +} + +delegate_layer!(@ SctkState); + +#[allow(dead_code)] +/// A request to SCTK window from Winit window. +#[derive(Debug, Clone)] +pub enum LayerSurfaceRequest { + /// Set fullscreen. + /// + /// Passing `None` will set it on the current monitor. + Size(LogicalSize), + + /// Unset fullscreen. + UnsetFullscreen, + + /// Show cursor for the certain window or not. + ShowCursor(bool), + + /// Set anchor + Anchor(Anchor), + + /// Set margin + ExclusiveZone(i32), + + /// Set margin + Margin(u32), + + /// Passthrough mouse input to underlying windows. + KeyboardInteractivity(KeyboardInteractivity), + + /// Redraw was requested. + Redraw, + + /// Window should be closed. + Close, +} diff --git a/sctk/src/handlers/shell/mod.rs b/sctk/src/handlers/shell/mod.rs new file mode 100644 index 0000000000..5556c08d3e --- /dev/null +++ b/sctk/src/handlers/shell/mod.rs @@ -0,0 +1,3 @@ +pub mod layer; +pub mod xdg_popup; +pub mod xdg_window; diff --git a/sctk/src/handlers/shell/xdg_popup.rs b/sctk/src/handlers/shell/xdg_popup.rs new file mode 100644 index 0000000000..8be7166323 --- /dev/null +++ b/sctk/src/handlers/shell/xdg_popup.rs @@ -0,0 +1,86 @@ +use crate::{ + event_loop::state::{self, SctkState, SctkSurface}, + sctk_event::{PopupEventVariant, SctkEvent}, +}; +use sctk::{delegate_xdg_popup, shell::xdg::popup::PopupHandler}; +use std::fmt::Debug; + +impl PopupHandler for SctkState { + fn configure( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + popup: &sctk::shell::xdg::popup::Popup, + configure: sctk::shell::xdg::popup::PopupConfigure, + ) { + let sctk_popup = match self.popups.iter_mut().find(|s| { + s.popup.wl_surface().clone() == popup.wl_surface().clone() + }) { + Some(p) => p, + None => return, + }; + let first = sctk_popup.last_configure.is_none(); + sctk_popup.last_configure.replace(configure.clone()); + + self.sctk_events.push(SctkEvent::PopupEvent { + variant: PopupEventVariant::Configure( + configure, + popup.wl_surface().clone(), + first, + ), + id: popup.wl_surface().clone(), + toplevel_id: sctk_popup.data.toplevel.clone(), + parent_id: match &sctk_popup.data.parent { + SctkSurface::LayerSurface(s) => s.clone(), + SctkSurface::Window(s) => s.clone(), + SctkSurface::Popup(s) => s.clone(), + }, + }); + self.frame_events.push(popup.wl_surface().clone()); + } + + fn done( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + popup: &sctk::shell::xdg::popup::Popup, + ) { + let sctk_popup = match self.popups.iter().position(|s| { + s.popup.wl_surface().clone() == popup.wl_surface().clone() + }) { + Some(p) => self.popups.remove(p), + None => return, + }; + let mut to_destroy = vec![sctk_popup]; + while let Some(popup_to_destroy) = to_destroy.last() { + match popup_to_destroy.data.parent.clone() { + state::SctkSurface::LayerSurface(_) + | state::SctkSurface::Window(_) => { + break; + } + state::SctkSurface::Popup(popup_to_destroy_first) => { + let popup_to_destroy_first = self + .popups + .iter() + .position(|p| { + p.popup.wl_surface() == &popup_to_destroy_first + }) + .unwrap(); + let popup_to_destroy_first = + self.popups.remove(popup_to_destroy_first); + to_destroy.push(popup_to_destroy_first); + } + } + } + for popup in to_destroy.into_iter().rev() { + self.sctk_events.push(SctkEvent::PopupEvent { + variant: PopupEventVariant::Done, + toplevel_id: popup.data.toplevel.clone(), + parent_id: popup.data.parent.wl_surface().clone(), + id: popup.popup.wl_surface().clone(), + }); + self.popups.push(popup); + } + } +} +delegate_xdg_popup!(@ SctkState); diff --git a/sctk/src/handlers/shell/xdg_window.rs b/sctk/src/handlers/shell/xdg_window.rs new file mode 100644 index 0000000000..b03b69870f --- /dev/null +++ b/sctk/src/handlers/shell/xdg_window.rs @@ -0,0 +1,116 @@ +use crate::{ + dpi::LogicalSize, + event_loop::state::SctkState, + sctk_event::{SctkEvent, WindowEventVariant}, +}; +use sctk::{ + delegate_xdg_shell, delegate_xdg_window, + shell::{xdg::window::WindowHandler, WaylandSurface}, +}; +use std::{fmt::Debug, num::NonZeroU32}; + +impl WindowHandler for SctkState { + fn request_close( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + window: &sctk::shell::xdg::window::Window, + ) { + let window = match self + .windows + .iter() + .find(|s| s.window.wl_surface() == window.wl_surface()) + { + Some(w) => w, + None => return, + }; + + self.sctk_events.push(SctkEvent::WindowEvent { + variant: WindowEventVariant::Close, + id: window.window.wl_surface().clone(), + }) + // TODO popup cleanup + } + + fn configure( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + window: &sctk::shell::xdg::window::Window, + mut configure: sctk::shell::xdg::window::WindowConfigure, + _serial: u32, + ) { + let window = match self + .windows + .iter_mut() + .find(|w| w.window.wl_surface() == window.wl_surface()) + { + Some(w) => w, + None => return, + }; + + if window.last_configure.as_ref().map(|c| c.state) + != Some(configure.state) + { + self.sctk_events.push(SctkEvent::WindowEvent { + variant: WindowEventVariant::StateChanged(configure.state), + id: window.window.wl_surface().clone(), + }); + } + if window.last_configure.as_ref().map(|c| c.capabilities) + != Some(configure.capabilities) + { + self.sctk_events.push(SctkEvent::WindowEvent { + variant: WindowEventVariant::WmCapabilities( + configure.capabilities, + ), + id: window.window.wl_surface().clone(), + }); + } + + if configure.new_size.0.is_none() { + configure.new_size.0 = Some( + window + .requested_size + .and_then(|r| NonZeroU32::new(r.0)) + .unwrap_or_else(|| NonZeroU32::new(300).unwrap()), + ); + } + if configure.new_size.1.is_none() { + configure.new_size.1 = Some( + window + .requested_size + .and_then(|r| NonZeroU32::new(r.1)) + .unwrap_or_else(|| NonZeroU32::new(500).unwrap()), + ); + } + configure + .new_size + .0 + .zip(configure.new_size.1) + .map(|new_size| { + window.update_size(LogicalSize { + width: new_size.0, + height: new_size.1, + }); + }); + + let wl_surface = window.window.wl_surface(); + let id = wl_surface.clone(); + let first = window.last_configure.is_none(); + window.last_configure.replace(configure.clone()); + + self.sctk_events.push(SctkEvent::WindowEvent { + variant: WindowEventVariant::Configure( + configure, + wl_surface.clone(), + first, + ), + id, + }); + self.frame_events.push(wl_surface.clone()); + } +} + +delegate_xdg_window!(@ SctkState); +delegate_xdg_shell!(@ SctkState); diff --git a/sctk/src/handlers/wp_fractional_scaling.rs b/sctk/src/handlers/wp_fractional_scaling.rs new file mode 100644 index 0000000000..aed95e2087 --- /dev/null +++ b/sctk/src/handlers/wp_fractional_scaling.rs @@ -0,0 +1,97 @@ +// From: https://github.com/rust-windowing/winit/blob/master/src/platform_impl/linux/wayland/types/wp_fractional_scaling.rs +//! Handling of the fractional scaling. + +use std::marker::PhantomData; + +use sctk::reexports::client::globals::{BindError, GlobalList}; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::Dispatch; +use sctk::reexports::client::{delegate_dispatch, Connection, Proxy, QueueHandle}; +use sctk::reexports::protocols::wp::fractional_scale::v1::client::wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1; +use sctk::reexports::protocols::wp::fractional_scale::v1::client::wp_fractional_scale_v1::Event as FractionalScalingEvent; +use sctk::reexports::protocols::wp::fractional_scale::v1::client::wp_fractional_scale_v1::WpFractionalScaleV1; + +use sctk::globals::GlobalData; + +use crate::event_loop::state::SctkState; + +/// The scaling factor denominator. +const SCALE_DENOMINATOR: f64 = 120.; + +/// Fractional scaling manager. +#[derive(Debug)] +pub struct FractionalScalingManager { + manager: WpFractionalScaleManagerV1, + + _phantom: PhantomData, +} + +pub struct FractionalScaling { + /// The surface used for scaling. + surface: WlSurface, +} + +impl FractionalScalingManager { + /// Create new viewporter. + pub fn new( + globals: &GlobalList, + queue_handle: &QueueHandle>, + ) -> Result { + let manager = globals.bind(queue_handle, 1..=1, GlobalData)?; + Ok(Self { + manager, + _phantom: PhantomData, + }) + } + + pub fn fractional_scaling( + &self, + surface: &WlSurface, + queue_handle: &QueueHandle>, + ) -> WpFractionalScaleV1 { + let data = FractionalScaling { + surface: surface.clone(), + }; + self.manager + .get_fractional_scale(surface, queue_handle, data) + } +} + +impl Dispatch> + for FractionalScalingManager +{ + fn event( + _: &mut SctkState, + _: &WpFractionalScaleManagerV1, + _: ::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle>, + ) { + // No events. + } +} + +impl Dispatch> + for FractionalScalingManager +{ + fn event( + state: &mut SctkState, + _: &WpFractionalScaleV1, + event: ::Event, + data: &FractionalScaling, + _: &Connection, + _: &QueueHandle>, + ) { + if let FractionalScalingEvent::PreferredScale { scale } = event { + state.scale_factor_changed( + &data.surface, + scale as f64 / SCALE_DENOMINATOR, + false, + ); + } + } +} + +delegate_dispatch!(@ SctkState: [WpFractionalScaleManagerV1: GlobalData] => FractionalScalingManager); +delegate_dispatch!(@ SctkState: [WpFractionalScaleV1: FractionalScaling] => FractionalScalingManager); diff --git a/sctk/src/handlers/wp_viewporter.rs b/sctk/src/handlers/wp_viewporter.rs new file mode 100644 index 0000000000..31ca68777b --- /dev/null +++ b/sctk/src/handlers/wp_viewporter.rs @@ -0,0 +1,80 @@ +//! Handling of the wp-viewporter. + +use std::marker::PhantomData; + +use sctk::reexports::client::globals::{BindError, GlobalList}; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::Dispatch; +use sctk::reexports::client::{ + delegate_dispatch, Connection, Proxy, QueueHandle, +}; +use sctk::reexports::protocols::wp::viewporter::client::wp_viewport::WpViewport; +use sctk::reexports::protocols::wp::viewporter::client::wp_viewporter::WpViewporter; + +use sctk::globals::GlobalData; + +use crate::event_loop::state::SctkState; + +/// Viewporter. +#[derive(Debug)] +pub struct ViewporterState { + viewporter: WpViewporter, + _phantom: PhantomData, +} + +impl ViewporterState { + /// Create new viewporter. + pub fn new( + globals: &GlobalList, + queue_handle: &QueueHandle>, + ) -> Result { + let viewporter = globals.bind(queue_handle, 1..=1, GlobalData)?; + Ok(Self { + viewporter, + _phantom: PhantomData, + }) + } + + /// Get the viewport for the given object. + pub fn get_viewport( + &self, + surface: &WlSurface, + queue_handle: &QueueHandle>, + ) -> WpViewport { + self.viewporter + .get_viewport(surface, queue_handle, GlobalData) + } +} + +impl Dispatch> + for ViewporterState +{ + fn event( + _: &mut SctkState, + _: &WpViewporter, + _: ::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle>, + ) { + // No events. + } +} + +impl Dispatch> + for ViewporterState +{ + fn event( + _: &mut SctkState, + _: &WpViewport, + _: ::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle>, + ) { + // No events. + } +} + +delegate_dispatch!(@ SctkState: [WpViewporter: GlobalData] => ViewporterState); +delegate_dispatch!(@ SctkState: [WpViewport: GlobalData] => ViewporterState); diff --git a/sctk/src/keymap.rs b/sctk/src/keymap.rs new file mode 100644 index 0000000000..fe02f37bb7 --- /dev/null +++ b/sctk/src/keymap.rs @@ -0,0 +1,475 @@ +// Borrowed from winit + +pub fn keysym_to_key(keysym: u32) -> Key { + use xkbcommon_dl::keysyms; + Key::Named(match keysym { + // TTY function keys + keysyms::BackSpace => Named::Backspace, + keysyms::Tab => Named::Tab, + // keysyms::Linefeed => Named::Linefeed, + keysyms::Clear => Named::Clear, + keysyms::Return => Named::Enter, + keysyms::Pause => Named::Pause, + keysyms::Scroll_Lock => Named::ScrollLock, + keysyms::Sys_Req => Named::PrintScreen, + keysyms::Escape => Named::Escape, + keysyms::Delete => Named::Delete, + + // IME keys + keysyms::Multi_key => Named::Compose, + keysyms::Codeinput => Named::CodeInput, + keysyms::SingleCandidate => Named::SingleCandidate, + keysyms::MultipleCandidate => Named::AllCandidates, + keysyms::PreviousCandidate => Named::PreviousCandidate, + + // Japanese key + keysyms::Kanji => Named::KanjiMode, + keysyms::Muhenkan => Named::NonConvert, + keysyms::Henkan_Mode => Named::Convert, + keysyms::Romaji => Named::Romaji, + keysyms::Hiragana => Named::Hiragana, + keysyms::Hiragana_Katakana => Named::HiraganaKatakana, + keysyms::Zenkaku => Named::Zenkaku, + keysyms::Hankaku => Named::Hankaku, + keysyms::Zenkaku_Hankaku => Named::ZenkakuHankaku, + // keysyms::Touroku => Named::Touroku, + // keysyms::Massyo => Named::Massyo, + keysyms::Kana_Lock => Named::KanaMode, + keysyms::Kana_Shift => Named::KanaMode, + keysyms::Eisu_Shift => Named::Alphanumeric, + keysyms::Eisu_toggle => Named::Alphanumeric, + // NOTE: The next three items are aliases for values we've already mapped. + // keysyms::Kanji_Bangou => Named::CodeInput, + // keysyms::Zen_Koho => Named::AllCandidates, + // keysyms::Mae_Koho => Named::PreviousCandidate, + + // Cursor control & motion + keysyms::Home => Named::Home, + keysyms::Left => Named::ArrowLeft, + keysyms::Up => Named::ArrowUp, + keysyms::Right => Named::ArrowRight, + keysyms::Down => Named::ArrowDown, + // keysyms::Prior => Named::PageUp, + keysyms::Page_Up => Named::PageUp, + // keysyms::Next => Named::PageDown, + keysyms::Page_Down => Named::PageDown, + keysyms::End => Named::End, + // keysyms::Begin => Named::Begin, + + // Misc. functions + keysyms::Select => Named::Select, + keysyms::Print => Named::PrintScreen, + keysyms::Execute => Named::Execute, + keysyms::Insert => Named::Insert, + keysyms::Undo => Named::Undo, + keysyms::Redo => Named::Redo, + keysyms::Menu => Named::ContextMenu, + keysyms::Find => Named::Find, + keysyms::Cancel => Named::Cancel, + keysyms::Help => Named::Help, + keysyms::Break => Named::Pause, + keysyms::Mode_switch => Named::ModeChange, + // keysyms::script_switch => Named::ModeChange, + keysyms::Num_Lock => Named::NumLock, + + // Keypad keys + // keysyms::KP_Space => return Key::Character(" "), + keysyms::KP_Tab => Named::Tab, + keysyms::KP_Enter => Named::Enter, + keysyms::KP_F1 => Named::F1, + keysyms::KP_F2 => Named::F2, + keysyms::KP_F3 => Named::F3, + keysyms::KP_F4 => Named::F4, + keysyms::KP_Home => Named::Home, + keysyms::KP_Left => Named::ArrowLeft, + keysyms::KP_Up => Named::ArrowUp, + keysyms::KP_Right => Named::ArrowRight, + keysyms::KP_Down => Named::ArrowDown, + // keysyms::KP_Prior => Named::PageUp, + keysyms::KP_Page_Up => Named::PageUp, + // keysyms::KP_Next => Named::PageDown, + keysyms::KP_Page_Down => Named::PageDown, + keysyms::KP_End => Named::End, + // This is the key labeled "5" on the numpad when NumLock is off. + // keysyms::KP_Begin => Named::Begin, + keysyms::KP_Insert => Named::Insert, + keysyms::KP_Delete => Named::Delete, + // keysyms::KP_Equal => Named::Equal, + // keysyms::KP_Multiply => Named::Multiply, + // keysyms::KP_Add => Named::Add, + // keysyms::KP_Separator => Named::Separator, + // keysyms::KP_Subtract => Named::Subtract, + // keysyms::KP_Decimal => Named::Decimal, + // keysyms::KP_Divide => Named::Divide, + + // keysyms::KP_0 => return Key::Character("0"), + // keysyms::KP_1 => return Key::Character("1"), + // keysyms::KP_2 => return Key::Character("2"), + // keysyms::KP_3 => return Key::Character("3"), + // keysyms::KP_4 => return Key::Character("4"), + // keysyms::KP_5 => return Key::Character("5"), + // keysyms::KP_6 => return Key::Character("6"), + // keysyms::KP_7 => return Key::Character("7"), + // keysyms::KP_8 => return Key::Character("8"), + // keysyms::KP_9 => return Key::Character("9"), + + // Function keys + keysyms::F1 => Named::F1, + keysyms::F2 => Named::F2, + keysyms::F3 => Named::F3, + keysyms::F4 => Named::F4, + keysyms::F5 => Named::F5, + keysyms::F6 => Named::F6, + keysyms::F7 => Named::F7, + keysyms::F8 => Named::F8, + keysyms::F9 => Named::F9, + keysyms::F10 => Named::F10, + keysyms::F11 => Named::F11, + keysyms::F12 => Named::F12, + keysyms::F13 => Named::F13, + keysyms::F14 => Named::F14, + keysyms::F15 => Named::F15, + keysyms::F16 => Named::F16, + keysyms::F17 => Named::F17, + keysyms::F18 => Named::F18, + keysyms::F19 => Named::F19, + keysyms::F20 => Named::F20, + keysyms::F21 => Named::F21, + keysyms::F22 => Named::F22, + keysyms::F23 => Named::F23, + keysyms::F24 => Named::F24, + keysyms::F25 => Named::F25, + keysyms::F26 => Named::F26, + keysyms::F27 => Named::F27, + keysyms::F28 => Named::F28, + keysyms::F29 => Named::F29, + keysyms::F30 => Named::F30, + keysyms::F31 => Named::F31, + keysyms::F32 => Named::F32, + keysyms::F33 => Named::F33, + keysyms::F34 => Named::F34, + keysyms::F35 => Named::F35, + + // Modifiers + keysyms::Shift_L => Named::Shift, + keysyms::Shift_R => Named::Shift, + keysyms::Control_L => Named::Control, + keysyms::Control_R => Named::Control, + keysyms::Caps_Lock => Named::CapsLock, + // keysyms::Shift_Lock => Named::ShiftLock, + + // keysyms::Meta_L => Named::Meta, + // keysyms::Meta_R => Named::Meta, + keysyms::Alt_L => Named::Alt, + keysyms::Alt_R => Named::Alt, + keysyms::Super_L => Named::Super, + keysyms::Super_R => Named::Super, + keysyms::Hyper_L => Named::Hyper, + keysyms::Hyper_R => Named::Hyper, + + // XKB function and modifier keys + // keysyms::ISO_Lock => Named::IsoLock, + // keysyms::ISO_Level2_Latch => Named::IsoLevel2Latch, + keysyms::ISO_Level3_Shift => Named::AltGraph, + keysyms::ISO_Level3_Latch => Named::AltGraph, + keysyms::ISO_Level3_Lock => Named::AltGraph, + // keysyms::ISO_Level5_Shift => Named::IsoLevel5Shift, + // keysyms::ISO_Level5_Latch => Named::IsoLevel5Latch, + // keysyms::ISO_Level5_Lock => Named::IsoLevel5Lock, + // keysyms::ISO_Group_Shift => Named::IsoGroupShift, + // keysyms::ISO_Group_Latch => Named::IsoGroupLatch, + // keysyms::ISO_Group_Lock => Named::IsoGroupLock, + keysyms::ISO_Next_Group => Named::GroupNext, + // keysyms::ISO_Next_Group_Lock => Named::GroupNextLock, + keysyms::ISO_Prev_Group => Named::GroupPrevious, + // keysyms::ISO_Prev_Group_Lock => Named::GroupPreviousLock, + keysyms::ISO_First_Group => Named::GroupFirst, + // keysyms::ISO_First_Group_Lock => Named::GroupFirstLock, + keysyms::ISO_Last_Group => Named::GroupLast, + // keysyms::ISO_Last_Group_Lock => Named::GroupLastLock, + // + keysyms::ISO_Left_Tab => Named::Tab, + // keysyms::ISO_Move_Line_Up => Named::IsoMoveLineUp, + // keysyms::ISO_Move_Line_Down => Named::IsoMoveLineDown, + // keysyms::ISO_Partial_Line_Up => Named::IsoPartialLineUp, + // keysyms::ISO_Partial_Line_Down => Named::IsoPartialLineDown, + // keysyms::ISO_Partial_Space_Left => Named::IsoPartialSpaceLeft, + // keysyms::ISO_Partial_Space_Right => Named::IsoPartialSpaceRight, + // keysyms::ISO_Set_Margin_Left => Named::IsoSetMarginLeft, + // keysyms::ISO_Set_Margin_Right => Named::IsoSetMarginRight, + // keysyms::ISO_Release_Margin_Left => Named::IsoReleaseMarginLeft, + // keysyms::ISO_Release_Margin_Right => Named::IsoReleaseMarginRight, + // keysyms::ISO_Release_Both_Margins => Named::IsoReleaseBothMargins, + // keysyms::ISO_Fast_Cursor_Left => Named::IsoFastCursorLeft, + // keysyms::ISO_Fast_Cursor_Right => Named::IsoFastCursorRight, + // keysyms::ISO_Fast_Cursor_Up => Named::IsoFastCursorUp, + // keysyms::ISO_Fast_Cursor_Down => Named::IsoFastCursorDown, + // keysyms::ISO_Continuous_Underline => Named::IsoContinuousUnderline, + // keysyms::ISO_Discontinuous_Underline => Named::IsoDiscontinuousUnderline, + // keysyms::ISO_Emphasize => Named::IsoEmphasize, + // keysyms::ISO_Center_Object => Named::IsoCenterObject, + keysyms::ISO_Enter => Named::Enter, + + // dead_grave..dead_currency + + // dead_lowline..dead_longsolidusoverlay + + // dead_a..dead_capital_schwa + + // dead_greek + + // First_Virtual_Screen..Terminate_Server + + // AccessX_Enable..AudibleBell_Enable + + // Pointer_Left..Pointer_Drag5 + + // Pointer_EnableKeys..Pointer_DfltBtnPrev + + // ch..C_H + + // 3270 terminal keys + // keysyms::3270_Duplicate => Named::Duplicate, + // keysyms::3270_FieldMark => Named::FieldMark, + // keysyms::3270_Right2 => Named::Right2, + // keysyms::3270_Left2 => Named::Left2, + // keysyms::3270_BackTab => Named::BackTab, + keysyms::_3270_EraseEOF => Named::EraseEof, + // keysyms::3270_EraseInput => Named::EraseInput, + // keysyms::3270_Reset => Named::Reset, + // keysyms::3270_Quit => Named::Quit, + // keysyms::3270_PA1 => Named::Pa1, + // keysyms::3270_PA2 => Named::Pa2, + // keysyms::3270_PA3 => Named::Pa3, + // keysyms::3270_Test => Named::Test, + keysyms::_3270_Attn => Named::Attn, + // keysyms::3270_CursorBlink => Named::CursorBlink, + // keysyms::3270_AltCursor => Named::AltCursor, + // keysyms::3270_KeyClick => Named::KeyClick, + // keysyms::3270_Jump => Named::Jump, + // keysyms::3270_Ident => Named::Ident, + // keysyms::3270_Rule => Named::Rule, + // keysyms::3270_Copy => Named::Copy, + keysyms::_3270_Play => Named::Play, + // keysyms::3270_Setup => Named::Setup, + // keysyms::3270_Record => Named::Record, + // keysyms::3270_ChangeScreen => Named::ChangeScreen, + // keysyms::3270_DeleteWord => Named::DeleteWord, + keysyms::_3270_ExSelect => Named::ExSel, + keysyms::_3270_CursorSelect => Named::CrSel, + keysyms::_3270_PrintScreen => Named::PrintScreen, + keysyms::_3270_Enter => Named::Enter, + + keysyms::space => Named::Space, + // exclam..Sinh_kunddaliya + + // XFree86 + // keysyms::XF86_ModeLock => Named::ModeLock, + + // XFree86 - Backlight controls + keysyms::XF86_MonBrightnessUp => Named::BrightnessUp, + keysyms::XF86_MonBrightnessDown => Named::BrightnessDown, + // keysyms::XF86_KbdLightOnOff => Named::LightOnOff, + // keysyms::XF86_KbdBrightnessUp => Named::KeyboardBrightnessUp, + // keysyms::XF86_KbdBrightnessDown => Named::KeyboardBrightnessDown, + + // XFree86 - "Internet" + keysyms::XF86_Standby => Named::Standby, + keysyms::XF86_AudioLowerVolume => Named::AudioVolumeDown, + keysyms::XF86_AudioRaiseVolume => Named::AudioVolumeUp, + keysyms::XF86_AudioPlay => Named::MediaPlay, + keysyms::XF86_AudioStop => Named::MediaStop, + keysyms::XF86_AudioPrev => Named::MediaTrackPrevious, + keysyms::XF86_AudioNext => Named::MediaTrackNext, + keysyms::XF86_HomePage => Named::BrowserHome, + keysyms::XF86_Mail => Named::LaunchMail, + // keysyms::XF86_Start => Named::Start, + keysyms::XF86_Search => Named::BrowserSearch, + keysyms::XF86_AudioRecord => Named::MediaRecord, + + // XFree86 - PDA + keysyms::XF86_Calculator => Named::LaunchApplication2, + // keysyms::XF86_Memo => Named::Memo, + // keysyms::XF86_ToDoList => Named::ToDoList, + keysyms::XF86_Calendar => Named::LaunchCalendar, + keysyms::XF86_PowerDown => Named::Power, + // keysyms::XF86_ContrastAdjust => Named::AdjustContrast, + // keysyms::XF86_RockerUp => Named::RockerUp, + // keysyms::XF86_RockerDown => Named::RockerDown, + // keysyms::XF86_RockerEnter => Named::RockerEnter, + + // XFree86 - More "Internet" + keysyms::XF86_Back => Named::BrowserBack, + keysyms::XF86_Forward => Named::BrowserForward, + // keysyms::XF86_Stop => Named::Stop, + keysyms::XF86_Refresh => Named::BrowserRefresh, + keysyms::XF86_PowerOff => Named::Power, + keysyms::XF86_WakeUp => Named::WakeUp, + keysyms::XF86_Eject => Named::Eject, + keysyms::XF86_ScreenSaver => Named::LaunchScreenSaver, + keysyms::XF86_WWW => Named::LaunchWebBrowser, + keysyms::XF86_Sleep => Named::Standby, + keysyms::XF86_Favorites => Named::BrowserFavorites, + keysyms::XF86_AudioPause => Named::MediaPause, + // keysyms::XF86_AudioMedia => Named::AudioMedia, + keysyms::XF86_MyComputer => Named::LaunchApplication1, + // keysyms::XF86_VendorHome => Named::VendorHome, + // keysyms::XF86_LightBulb => Named::LightBulb, + // keysyms::XF86_Shop => Named::BrowserShop, + // keysyms::XF86_History => Named::BrowserHistory, + // keysyms::XF86_OpenURL => Named::OpenUrl, + // keysyms::XF86_AddFavorite => Named::AddFavorite, + // keysyms::XF86_HotLinks => Named::HotLinks, + // keysyms::XF86_BrightnessAdjust => Named::BrightnessAdjust, + // keysyms::XF86_Finance => Named::BrowserFinance, + // keysyms::XF86_Community => Named::BrowserCommunity, + keysyms::XF86_AudioRewind => Named::MediaRewind, + // keysyms::XF86_BackForward => Key::???, + // XF86_Launch0..XF86_LaunchF + + // XF86_ApplicationLeft..XF86_CD + keysyms::XF86_Calculater => Named::LaunchApplication2, // Nice typo, libxkbcommon :) + // XF86_Clear + keysyms::XF86_Close => Named::Close, + keysyms::XF86_Copy => Named::Copy, + keysyms::XF86_Cut => Named::Cut, + // XF86_Display..XF86_Documents + keysyms::XF86_Excel => Named::LaunchSpreadsheet, + // XF86_Explorer..XF86iTouch + keysyms::XF86_LogOff => Named::LogOff, + // XF86_Market..XF86_MenuPB + keysyms::XF86_MySites => Named::BrowserFavorites, + keysyms::XF86_New => Named::New, + // XF86_News..XF86_OfficeHome + keysyms::XF86_Open => Named::Open, + // XF86_Option + keysyms::XF86_Paste => Named::Paste, + keysyms::XF86_Phone => Named::LaunchPhone, + // XF86_Q + keysyms::XF86_Reply => Named::MailReply, + keysyms::XF86_Reload => Named::BrowserRefresh, + // XF86_RotateWindows..XF86_RotationKB + keysyms::XF86_Save => Named::Save, + // XF86_ScrollUp..XF86_ScrollClick + keysyms::XF86_Send => Named::MailSend, + keysyms::XF86_Spell => Named::SpellCheck, + keysyms::XF86_SplitScreen => Named::SplitScreenToggle, + // XF86_Support..XF86_User2KB + keysyms::XF86_Video => Named::LaunchMediaPlayer, + // XF86_WheelButton + keysyms::XF86_Word => Named::LaunchWordProcessor, + // XF86_Xfer + keysyms::XF86_ZoomIn => Named::ZoomIn, + keysyms::XF86_ZoomOut => Named::ZoomOut, + + // XF86_Away..XF86_Messenger + keysyms::XF86_WebCam => Named::LaunchWebCam, + keysyms::XF86_MailForward => Named::MailForward, + // XF86_Pictures + keysyms::XF86_Music => Named::LaunchMusicPlayer, + + // XF86_Battery..XF86_UWB + // + keysyms::XF86_AudioForward => Named::MediaFastForward, + // XF86_AudioRepeat + keysyms::XF86_AudioRandomPlay => Named::RandomToggle, + keysyms::XF86_Subtitle => Named::Subtitle, + keysyms::XF86_AudioCycleTrack => Named::MediaAudioTrack, + // XF86_CycleAngle..XF86_Blue + // + keysyms::XF86_Suspend => Named::Standby, + keysyms::XF86_Hibernate => Named::Hibernate, + // XF86_TouchpadToggle..XF86_TouchpadOff + // + keysyms::XF86_AudioMute => Named::AudioVolumeMute, + + // XF86_Switch_VT_1..XF86_Switch_VT_12 + + // XF86_Ungrab..XF86_ClearGrab + keysyms::XF86_Next_VMode => Named::VideoModeNext, + // keysyms::XF86_Prev_VMode => Named::VideoModePrevious, + // XF86_LogWindowTree..XF86_LogGrabInfo + + // SunFA_Grave..SunFA_Cedilla + + // keysyms::SunF36 => Named::F36 | Named::F11, + // keysyms::SunF37 => Named::F37 | Named::F12, + + // keysyms::SunSys_Req => Named::PrintScreen, + // The next couple of xkb (until SunStop) are already handled. + // SunPrint_Screen..SunPageDown + + // SunUndo..SunFront + keysyms::SUN_Copy => Named::Copy, + keysyms::SUN_Open => Named::Open, + keysyms::SUN_Paste => Named::Paste, + keysyms::SUN_Cut => Named::Cut, + + // SunPowerSwitch + keysyms::SUN_AudioLowerVolume => Named::AudioVolumeDown, + keysyms::SUN_AudioMute => Named::AudioVolumeMute, + keysyms::SUN_AudioRaiseVolume => Named::AudioVolumeUp, + // SUN_VideoDegauss + keysyms::SUN_VideoLowerBrightness => Named::BrightnessDown, + keysyms::SUN_VideoRaiseBrightness => Named::BrightnessUp, + // SunPowerSwitchShift + // + _ => return Key::Unidentified, + }) +} + +use iced_runtime::keyboard::{key::Named, Key, Location}; + +pub fn keysym_location(keysym: u32) -> Location { + use xkbcommon_dl::keysyms; + match keysym { + xkeysym::key::Shift_L + | keysyms::Control_L + | keysyms::Meta_L + | keysyms::Alt_L + | keysyms::Super_L + | keysyms::Hyper_L => Location::Left, + keysyms::Shift_R + | keysyms::Control_R + | keysyms::Meta_R + | keysyms::Alt_R + | keysyms::Super_R + | keysyms::Hyper_R => Location::Right, + keysyms::KP_0 + | keysyms::KP_1 + | keysyms::KP_2 + | keysyms::KP_3 + | keysyms::KP_4 + | keysyms::KP_5 + | keysyms::KP_6 + | keysyms::KP_7 + | keysyms::KP_8 + | keysyms::KP_9 + | keysyms::KP_Space + | keysyms::KP_Tab + | keysyms::KP_Enter + | keysyms::KP_F1 + | keysyms::KP_F2 + | keysyms::KP_F3 + | keysyms::KP_F4 + | keysyms::KP_Home + | keysyms::KP_Left + | keysyms::KP_Up + | keysyms::KP_Right + | keysyms::KP_Down + | keysyms::KP_Page_Up + | keysyms::KP_Page_Down + | keysyms::KP_End + | keysyms::KP_Begin + | keysyms::KP_Insert + | keysyms::KP_Delete + | keysyms::KP_Equal + | keysyms::KP_Multiply + | keysyms::KP_Add + | keysyms::KP_Separator + | keysyms::KP_Subtract + | keysyms::KP_Decimal + | keysyms::KP_Divide => Location::Numpad, + _ => Location::Standard, + } +} diff --git a/sctk/src/lib.rs b/sctk/src/lib.rs new file mode 100644 index 0000000000..48520f8889 --- /dev/null +++ b/sctk/src/lib.rs @@ -0,0 +1,25 @@ +pub mod application; +pub mod clipboard; +pub mod commands; +pub mod conversion; +pub mod dpi; +pub mod error; +pub mod event_loop; +mod handlers; +pub mod keymap; +pub mod result; +pub mod sctk_event; +pub mod settings; +#[cfg(feature = "system")] +pub mod system; +pub mod util; +pub mod window; + +pub use application::{run, Application}; +pub use clipboard::Clipboard; +pub use error::Error; +pub use event_loop::proxy::Proxy; +pub use iced_graphics::Viewport; +pub use iced_runtime as runtime; +pub use iced_runtime::core; +pub use settings::Settings; diff --git a/sctk/src/result.rs b/sctk/src/result.rs new file mode 100644 index 0000000000..fc9af5c566 --- /dev/null +++ b/sctk/src/result.rs @@ -0,0 +1,6 @@ +use crate::error::Error; + +/// The result of running an [`Application`]. +/// +/// [`Application`]: crate::Application +pub type Result = std::result::Result<(), Error>; diff --git a/sctk/src/sctk_event.rs b/sctk/src/sctk_event.rs new file mode 100755 index 0000000000..af81d84a92 --- /dev/null +++ b/sctk/src/sctk_event.rs @@ -0,0 +1,960 @@ +use crate::{ + application::SurfaceIdWrapper, + conversion::{ + modifiers_to_native, pointer_axis_to_native, pointer_button_to_native, + }, + dpi::PhysicalSize, + keymap::{self, keysym_to_key}, +}; + +use iced_futures::core::event::{ + wayland::{LayerEvent, PopupEvent, SessionLockEvent}, + PlatformSpecific, +}; +use iced_runtime::{ + command::platform_specific::wayland::data_device::DndIcon, + core::{event::wayland, keyboard, mouse, window, Point}, + keyboard::{key, Key, Location}, + window::Id as SurfaceId, +}; +use sctk::{ + output::OutputInfo, + reexports::client::{ + backend::ObjectId, + protocol::{ + wl_data_device_manager::DndAction, wl_keyboard::WlKeyboard, + wl_output::WlOutput, wl_pointer::WlPointer, wl_seat::WlSeat, + wl_surface::WlSurface, + }, + Proxy, + }, + reexports::csd_frame::WindowManagerCapabilities, + seat::{ + keyboard::{KeyEvent, Modifiers}, + pointer::{PointerEvent, PointerEventKind}, + Capability, + }, + session_lock::SessionLockSurfaceConfigure, + shell::{ + wlr_layer::LayerSurfaceConfigure, + xdg::{popup::PopupConfigure, window::WindowConfigure}, + }, +}; +use std::{collections::HashMap, time::Instant}; +use wayland_protocols::wp::viewporter::client::wp_viewport::WpViewport; + +pub enum IcedSctkEvent { + /// Emitted when new events arrive from the OS to be processed. + /// + /// This event type is useful as a place to put code that should be done before you start + /// processing events, such as updating frame timing information for benchmarking or checking + /// the [`StartCause`][crate::event::StartCause] to see if a timer set by + /// [`ControlFlow::WaitUntil`](crate::event_loop::ControlFlow::WaitUntil) has elapsed. + NewEvents(StartCause), + + /// Any user event from iced + UserEvent(T), + + /// An event produced by sctk + SctkEvent(SctkEvent), + + #[cfg(feature = "a11y")] + A11ySurfaceCreated( + SurfaceIdWrapper, + crate::event_loop::adapter::IcedSctkAdapter, + ), + + /// emitted after first accessibility tree is requested + #[cfg(feature = "a11y")] + A11yEnabled, + + /// accessibility event + #[cfg(feature = "a11y")] + A11yEvent(ActionRequestEvent), + + /// Emitted when all of the event loop's input events have been processed and redraw processing + /// is about to begin. + /// + /// This event is useful as a place to put your code that should be run after all + /// state-changing events have been handled and you want to do stuff (updating state, performing + /// calculations, etc) that happens as the "main body" of your event loop. If your program only draws + /// graphics when something changes, it's usually better to do it in response to + /// [`Event::RedrawRequested`](crate::event::Event::RedrawRequested), which gets emitted + /// immediately after this event. Programs that draw graphics continuously, like most games, + /// can render here unconditionally for simplicity. + MainEventsCleared, + + /// Emitted after [`MainEventsCleared`] when a window should be redrawn. + /// + /// This gets triggered in two scenarios: + /// - The OS has performed an operation that's invalidated the window's contents (such as + /// resizing the window). + /// - The application has explicitly requested a redraw via [`Window::request_redraw`]. + /// + /// During each iteration of the event loop, Winit will aggregate duplicate redraw requests + /// into a single event, to help avoid duplicating rendering work. + /// + /// Mainly of interest to applications with mostly-static graphics that avoid redrawing unless + /// something changes, like most non-game GUIs. + /// + /// [`MainEventsCleared`]: Self::MainEventsCleared + RedrawRequested(ObjectId), + + /// Emitted after all [`RedrawRequested`] events have been processed and control flow is about to + /// be taken away from the program. If there are no `RedrawRequested` events, it is emitted + /// immediately after `MainEventsCleared`. + /// + /// This event is useful for doing any cleanup or bookkeeping work after all the rendering + /// tasks have been completed. + /// + /// [`RedrawRequested`]: Self::RedrawRequested + RedrawEventsCleared, + + /// Emitted when the event loop is being shut down. + /// + /// This is irreversible - if this event is emitted, it is guaranteed to be the last event that + /// gets emitted. You generally want to treat this as an "do on quit" event. + LoopDestroyed, + + /// Dnd source created with an icon surface. + DndSurfaceCreated(WlSurface, DndIcon, SurfaceId), + + /// Frame callback event + Frame(WlSurface), +} + +#[derive(Debug, Clone)] +pub enum SctkEvent { + // + // Input events + // + SeatEvent { + variant: SeatEventVariant, + id: WlSeat, + }, + PointerEvent { + variant: PointerEvent, + ptr_id: WlPointer, + seat_id: WlSeat, + }, + KeyboardEvent { + variant: KeyboardEventVariant, + kbd_id: WlKeyboard, + seat_id: WlSeat, + }, + // TODO data device & touch + + // + // Surface Events + // + WindowEvent { + variant: WindowEventVariant, + id: WlSurface, + }, + LayerSurfaceEvent { + variant: LayerSurfaceEventVariant, + id: WlSurface, + }, + PopupEvent { + variant: PopupEventVariant, + /// this may be the Id of a window or layer surface + toplevel_id: WlSurface, + /// this may be any SurfaceId + parent_id: WlSurface, + /// the id of this popup + id: WlSurface, + }, + + // + // output events + // + NewOutput { + id: WlOutput, + info: Option, + }, + UpdateOutput { + id: WlOutput, + info: OutputInfo, + }, + RemovedOutput(WlOutput), + // + // compositor events + // + ScaleFactorChanged { + factor: f64, + id: WlOutput, + inner_size: PhysicalSize, + }, + DataSource(DataSourceEvent), + DndOffer { + event: DndOfferEvent, + surface: WlSurface, + }, + /// session lock events + SessionLocked, + SessionLockFinished, + SessionLockSurfaceCreated { + surface: WlSurface, + native_id: SurfaceId, + }, + SessionLockSurfaceConfigure { + surface: WlSurface, + configure: SessionLockSurfaceConfigure, + first: bool, + }, + SessionUnlocked, +} + +#[derive(Debug, Clone)] +pub enum DataSourceEvent { + /// A DnD action has been accepted by the compositor for your source. + DndActionAccepted(DndAction), + /// A DnD mime type has been accepted by a client for your source. + MimeAccepted(Option), + /// Dnd Finished event. + DndFinished, + /// Dnd Cancelled event. + DndCancelled, + /// Dnd Drop performed event. + DndDropPerformed, + /// Send the selection data to the clipboard. + SendSelectionData { + /// The mime type of the data to be sent + mime_type: String, + }, + /// Send the DnD data to the destination. + SendDndData { + /// The mime type of the data to be sent + mime_type: String, + }, +} + +#[derive(Debug, Clone)] +pub enum DndOfferEvent { + /// A DnD offer has been introduced with the given mime types. + Enter { + x: f64, + y: f64, + mime_types: Vec, + }, + /// The DnD device has left. + Leave, + /// Drag and Drop Motion event. + Motion { + /// x coordinate of the pointer + x: f64, + /// y coordinate of the pointer + y: f64, + }, + /// A drop has been performed. + DropPerformed, + /// Read the DnD data + Data { + /// The raw data + data: Vec, + /// mime type of the data to read + mime_type: String, + }, + SourceActions(DndAction), + SelectedAction(DndAction), +} + +#[cfg(feature = "a11y")] +#[derive(Debug, Clone)] +pub struct ActionRequestEvent { + pub surface_id: ObjectId, + pub request: iced_accessibility::accesskit::ActionRequest, +} + +#[derive(Debug, Clone)] +pub enum SeatEventVariant { + New, + Remove, + NewCapability(Capability, ObjectId), + RemoveCapability(Capability, ObjectId), +} + +#[derive(Debug, Clone)] +pub enum KeyboardEventVariant { + Leave(WlSurface), + Enter(WlSurface), + Press(KeyEvent), + Repeat(KeyEvent), + Release(KeyEvent), + Modifiers(Modifiers), +} + +#[derive(Debug, Clone)] +pub enum WindowEventVariant { + Created(ObjectId, SurfaceId), + /// + Close, + /// + WmCapabilities(WindowManagerCapabilities), + /// + ConfigureBounds { + width: u32, + height: u32, + }, + /// + Configure(WindowConfigure, WlSurface, bool), + + /// window state changed + StateChanged(sctk::reexports::csd_frame::WindowState), + /// Scale Factor + ScaleFactorChanged(f64, Option), +} + +#[derive(Debug, Clone)] +pub enum PopupEventVariant { + /// Popup Created + Created(ObjectId, SurfaceId), + /// + Done, + /// + Configure(PopupConfigure, WlSurface, bool), + /// + RepositionionedPopup { token: u32 }, + /// size + Size(u32, u32), + /// Scale Factor + ScaleFactorChanged(f64, Option), +} + +#[derive(Debug, Clone)] +pub enum LayerSurfaceEventVariant { + /// sent after creation of the layer surface + Created(ObjectId, SurfaceId), + /// + Done, + /// + Configure(LayerSurfaceConfigure, WlSurface, bool), + /// Scale Factor + ScaleFactorChanged(f64, Option), +} + +/// Describes the reason the event loop is resuming. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum StartCause { + /// Sent if the time specified by [`ControlFlow::WaitUntil`] has been reached. Contains the + /// moment the timeout was requested and the requested resume time. The actual resume time is + /// guaranteed to be equal to or after the requested resume time. + /// + /// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil + ResumeTimeReached { + start: Instant, + requested_resume: Instant, + }, + + /// Sent if the OS has new events to send to the window, after a wait was requested. Contains + /// the moment the wait was requested and the resume time, if requested. + WaitCancelled { + start: Instant, + requested_resume: Option, + }, + + /// Sent if the event loop is being resumed after the loop's control flow was set to + /// [`ControlFlow::Poll`]. + /// + /// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll + Poll, + + /// Sent once, immediately after `run` is called. Indicates that the loop was just initialized. + Init, +} + +/// Pending update to a window requested by the user. +#[derive(Default, Debug, Clone, Copy)] +pub struct SurfaceUserRequest { + /// Whether `redraw` was requested. + pub redraw_requested: bool, + + /// Wether the frame should be refreshed. + pub refresh_frame: bool, +} + +// The window update coming from the compositor. +#[derive(Default, Debug, Clone)] +pub struct SurfaceCompositorUpdate { + /// New window configure. + pub configure: Option, + + /// New scale factor. + pub scale_factor: Option, +} + +impl SctkEvent { + pub fn to_native( + self, + modifiers: &mut Modifiers, + surface_ids: &HashMap, + destroyed_surface_ids: &HashMap, + ) -> Vec { + match self { + // TODO Ashley: Platform specific multi-seat events? + SctkEvent::SeatEvent { .. } => Default::default(), + SctkEvent::PointerEvent { variant, .. } => match variant.kind { + PointerEventKind::Enter { .. } => { + vec![iced_runtime::core::Event::Mouse( + mouse::Event::CursorEntered, + )] + } + PointerEventKind::Leave { .. } => { + vec![iced_runtime::core::Event::Mouse( + mouse::Event::CursorLeft, + )] + } + PointerEventKind::Motion { .. } => { + vec![iced_runtime::core::Event::Mouse( + mouse::Event::CursorMoved { + position: Point::new( + variant.position.0 as f32, + variant.position.1 as f32, + ), + }, + )] + } + PointerEventKind::Press { + time: _, + button, + serial: _, + } => pointer_button_to_native(button) + .map(|b| { + iced_runtime::core::Event::Mouse( + mouse::Event::ButtonPressed(b), + ) + }) + .into_iter() + .collect(), // TODO Ashley: conversion + PointerEventKind::Release { + time: _, + button, + serial: _, + } => pointer_button_to_native(button) + .map(|b| { + iced_runtime::core::Event::Mouse( + mouse::Event::ButtonReleased(b), + ) + }) + .into_iter() + .collect(), // TODO Ashley: conversion + PointerEventKind::Axis { + time: _, + horizontal, + vertical, + source, + } => pointer_axis_to_native(source, horizontal, vertical) + .map(|a| { + iced_runtime::core::Event::Mouse( + mouse::Event::WheelScrolled { delta: a }, + ) + }) + .into_iter() + .collect(), // TODO Ashley: conversion + }, + SctkEvent::KeyboardEvent { + variant, + kbd_id: _, + seat_id, + } => match variant { + KeyboardEventVariant::Leave(surface) => surface_ids + .get(&surface.id()) + .and_then(|id| match id { + SurfaceIdWrapper::LayerSurface(_id) => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::Layer( + LayerEvent::Unfocused, + surface, + id.inner(), + ), + ), + )) + } + SurfaceIdWrapper::Window(id) => { + Some(iced_runtime::core::Event::Window( + *id, + window::Event::Unfocused, + )) + } + SurfaceIdWrapper::Popup(_id) => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::Popup( + PopupEvent::Unfocused, + surface, + id.inner(), + ), + ), + )) + } + SurfaceIdWrapper::Dnd(_) => None, + SurfaceIdWrapper::SessionLock(_) => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::SessionLock( + SessionLockEvent::Unfocused( + surface, + id.inner(), + ), + ), + ), + )) + } + }) + .into_iter() + .chain([iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::Seat( + wayland::SeatEvent::Leave, + seat_id, + )), + )]) + .collect(), + KeyboardEventVariant::Enter(surface) => surface_ids + .get(&surface.id()) + .and_then(|id| match id { + SurfaceIdWrapper::LayerSurface(_id) => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::Layer( + LayerEvent::Focused, + surface, + id.inner(), + ), + ), + )) + } + SurfaceIdWrapper::Window(id) => { + Some(iced_runtime::core::Event::Window( + *id, + window::Event::Focused, + )) + } + SurfaceIdWrapper::Popup(_id) => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::Popup( + PopupEvent::Focused, + surface, + id.inner(), + ), + ), + )) + } + SurfaceIdWrapper::Dnd(_) => None, + SurfaceIdWrapper::SessionLock(_) => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::SessionLock( + SessionLockEvent::Focused( + surface, + id.inner(), + ), + ), + ), + )) + } + }) + .into_iter() + .chain([iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::Seat( + wayland::SeatEvent::Enter, + seat_id, + )), + )]) + .collect(), + KeyboardEventVariant::Press(ke) => { + let (key, location) = keysym_to_vkey_location( + ke.keysym.raw(), + ke.utf8.as_deref(), + ); + Some(iced_runtime::core::Event::Keyboard( + keyboard::Event::KeyPressed { + key: key, + location: location, + text: ke.utf8.map(|s| s.into()), + modifiers: modifiers_to_native(*modifiers), + }, + )) + .into_iter() + .collect() + } + KeyboardEventVariant::Repeat(KeyEvent { + raw_code, + utf8, + .. + }) => { + let (key, location) = + keysym_to_vkey_location(raw_code, utf8.as_deref()); + Some(iced_runtime::core::Event::Keyboard( + keyboard::Event::KeyPressed { + key: key, + location: location, + text: utf8.map(|s| s.into()), + modifiers: modifiers_to_native(*modifiers), + }, + )) + .into_iter() + .collect() + } + KeyboardEventVariant::Release(ke) => { + let (k, location) = keysym_to_vkey_location( + ke.keysym.raw(), + ke.utf8.as_deref(), + ); + Some(iced_runtime::core::Event::Keyboard( + keyboard::Event::KeyReleased { + key: k, + location: location, + modifiers: modifiers_to_native(*modifiers), + }, + )) + .into_iter() + .collect() + } + KeyboardEventVariant::Modifiers(new_mods) => { + *modifiers = new_mods; + vec![iced_runtime::core::Event::Keyboard( + keyboard::Event::ModifiersChanged(modifiers_to_native( + new_mods, + )), + )] + } + }, + SctkEvent::WindowEvent { + variant, + id: surface, + } => match variant { + // TODO Ashley: platform specific events for window + WindowEventVariant::Created(..) => Default::default(), + WindowEventVariant::Close => destroyed_surface_ids + .get(&surface.id()) + .map(|id| { + iced_runtime::core::Event::Window( + id.inner(), + window::Event::CloseRequested, + ) + }) + .into_iter() + .collect(), + WindowEventVariant::WmCapabilities(caps) => surface_ids + .get(&surface.id()) + .map(|id| id.inner()) + .map(|id| { + iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::Window( + wayland::WindowEvent::WmCapabilities(caps), + surface, + id, + )), + ) + }) + .into_iter() + .collect(), + WindowEventVariant::ConfigureBounds { .. } => { + Default::default() + } + WindowEventVariant::Configure(configure, surface, _) => { + if configure.is_resizing() { + surface_ids + .get(&surface.id()) + .map(|id| { + iced_runtime::core::Event::Window( + id.inner(), + window::Event::Resized { + width: configure + .new_size + .0 + .unwrap() + .get(), + height: configure + .new_size + .1 + .unwrap() + .get(), + }, + ) + }) + .into_iter() + .collect() + } else { + Default::default() + } + } + WindowEventVariant::ScaleFactorChanged(..) => { + Default::default() + } + WindowEventVariant::StateChanged(s) => surface_ids + .get(&surface.id()) + .map(|id| { + iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::Window( + wayland::WindowEvent::State(s), + surface, + id.inner(), + )), + ) + }) + .into_iter() + .collect(), + }, + SctkEvent::LayerSurfaceEvent { + variant, + id: surface, + } => match variant { + LayerSurfaceEventVariant::Done => destroyed_surface_ids + .get(&surface.id()) + .map(|id| { + iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::Layer( + LayerEvent::Done, + surface, + id.inner(), + )), + ) + }) + .into_iter() + .collect(), + _ => Default::default(), + }, + SctkEvent::PopupEvent { + variant, + id: surface, + .. + } => { + match variant { + PopupEventVariant::Done => destroyed_surface_ids + .get(&surface.id()) + .map(|id| { + iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::Popup( + PopupEvent::Done, + surface, + id.inner(), + ), + ), + ) + }) + .into_iter() + .collect(), + PopupEventVariant::Created(_, _) => Default::default(), // TODO + PopupEventVariant::Configure(_, _, _) => Default::default(), // TODO + PopupEventVariant::RepositionionedPopup { token: _ } => { + Default::default() + } + PopupEventVariant::Size(_, _) => Default::default(), + PopupEventVariant::ScaleFactorChanged(..) => { + Default::default() + } // TODO + } + } + SctkEvent::NewOutput { id, info } => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::Output( + wayland::OutputEvent::Created(info), + id, + )), + )) + .into_iter() + .collect() + } + SctkEvent::UpdateOutput { id, info } => { + vec![iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::Output( + wayland::OutputEvent::InfoUpdate(info), + id, + )), + )] + } + SctkEvent::RemovedOutput(id) => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::Output( + wayland::OutputEvent::Removed, + id, + )), + )) + .into_iter() + .collect() + } + SctkEvent::ScaleFactorChanged { + factor: _, + id: _, + inner_size: _, + } => Default::default(), + SctkEvent::DndOffer { event, .. } => match event { + DndOfferEvent::Enter { mime_types, x, y } => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::DndOffer( + wayland::DndOfferEvent::Enter { mime_types, x, y }, + )), + )) + .into_iter() + .collect() + } + DndOfferEvent::Motion { x, y } => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::DndOffer( + wayland::DndOfferEvent::Motion { x, y }, + )), + )) + .into_iter() + .collect() + } + DndOfferEvent::DropPerformed => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::DndOffer( + wayland::DndOfferEvent::DropPerformed, + )), + )) + .into_iter() + .collect() + } + DndOfferEvent::Leave => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::DndOffer( + wayland::DndOfferEvent::Leave, + )), + )) + .into_iter() + .collect() + } + DndOfferEvent::Data { mime_type, data } => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::DndOffer( + wayland::DndOfferEvent::DndData { data, mime_type }, + )), + )) + .into_iter() + .collect() + } + DndOfferEvent::SourceActions(actions) => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::DndOffer( + wayland::DndOfferEvent::SourceActions(actions), + )), + )) + .into_iter() + .collect() + } + DndOfferEvent::SelectedAction(action) => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::DndOffer( + wayland::DndOfferEvent::SelectedAction(action), + )), + )) + .into_iter() + .collect() + } + }, + SctkEvent::DataSource(event) => match event { + DataSourceEvent::DndDropPerformed => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::DataSource( + wayland::DataSourceEvent::DndDropPerformed, + )), + )) + .into_iter() + .collect() + } + DataSourceEvent::DndFinished => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::DataSource( + wayland::DataSourceEvent::DndFinished, + )), + )) + .into_iter() + .collect() + } + DataSourceEvent::DndCancelled => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::DataSource( + wayland::DataSourceEvent::Cancelled, + )), + )) + .into_iter() + .collect() + } + DataSourceEvent::MimeAccepted(mime_type) => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::DataSource( + wayland::DataSourceEvent::MimeAccepted(mime_type), + )), + )) + .into_iter() + .collect() + } + DataSourceEvent::DndActionAccepted(action) => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::DataSource( + wayland::DataSourceEvent::DndActionAccepted(action), + )), + )) + .into_iter() + .collect() + } + DataSourceEvent::SendDndData { mime_type } => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::DataSource( + wayland::DataSourceEvent::SendDndData(mime_type), + )), + )) + .into_iter() + .collect() + } + DataSourceEvent::SendSelectionData { mime_type } => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::DataSource( + wayland::DataSourceEvent::SendSelectionData( + mime_type, + ), + )), + )) + .into_iter() + .collect() + } + }, + SctkEvent::SessionLocked => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::SessionLock( + wayland::SessionLockEvent::Locked, + )), + )) + .into_iter() + .collect() + } + SctkEvent::SessionLockFinished => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::SessionLock( + wayland::SessionLockEvent::Finished, + )), + )) + .into_iter() + .collect() + } + SctkEvent::SessionLockSurfaceCreated { .. } => vec![], + SctkEvent::SessionLockSurfaceConfigure { .. } => vec![], + SctkEvent::SessionUnlocked => { + Some(iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::SessionLock( + wayland::SessionLockEvent::Unlocked, + )), + )) + .into_iter() + .collect() + } + } + } +} + +fn keysym_to_vkey_location(keysym: u32, utf8: Option<&str>) -> (Key, Location) { + let mut key = keysym_to_key(keysym); + if matches!(key, key::Key::Unidentified) { + if let Some(utf8) = utf8 { + key = Key::Character(utf8.into()); + } + } + + let location = keymap::keysym_location(keysym); + (key, location) +} diff --git a/sctk/src/settings.rs b/sctk/src/settings.rs new file mode 100644 index 0000000000..f3299b06a8 --- /dev/null +++ b/sctk/src/settings.rs @@ -0,0 +1,32 @@ +use iced_runtime::command::platform_specific::wayland::{ + layer_surface::SctkLayerSurfaceSettings, window::SctkWindowSettings, +}; + +#[derive(Debug)] +pub struct Settings { + /// The data needed to initialize an [`Application`]. + /// + /// [`Application`]: crate::Application + pub flags: Flags, + /// optional keyboard repetition config + pub kbd_repeat: Option, + /// optional name and size of a custom pointer theme + pub ptr_theme: Option<(String, u32)>, + /// surface + pub surface: InitialSurface, + /// whether the application should exit on close of all windows + pub exit_on_close_request: bool, +} + +#[derive(Debug, Clone)] +pub enum InitialSurface { + LayerSurface(SctkLayerSurfaceSettings), + XdgWindow(SctkWindowSettings), + None, +} + +impl Default for InitialSurface { + fn default() -> Self { + Self::LayerSurface(SctkLayerSurfaceSettings::default()) + } +} diff --git a/sctk/src/system.rs b/sctk/src/system.rs new file mode 100644 index 0000000000..7b700e73af --- /dev/null +++ b/sctk/src/system.rs @@ -0,0 +1,41 @@ +//! Access the native system. +use crate::runtime::command::{self, Command}; +use crate::runtime::system::{Action, Information}; +use iced_graphics::compositor; + +/// Query for available system information. +pub fn fetch_information( + f: impl Fn(Information) -> Message + Send + 'static, +) -> Command { + Command::single(command::Action::System(Action::QueryInformation( + Box::new(f), + ))) +} + +pub(crate) fn information( + graphics_info: compositor::Information, +) -> Information { + use sysinfo::{CpuExt, ProcessExt, System, SystemExt}; + let mut system = System::new_all(); + system.refresh_all(); + + let cpu = system.global_cpu_info(); + + let memory_used = sysinfo::get_current_pid() + .and_then(|pid| system.process(pid).ok_or("Process not found")) + .map(|process| process.memory()) + .ok(); + + Information { + system_name: system.name(), + system_kernel: system.kernel_version(), + system_version: system.long_os_version(), + system_short_version: system.os_version(), + cpu_brand: cpu.brand().into(), + cpu_cores: system.physical_core_count(), + memory_total: system.total_memory(), + memory_used, + graphics_adapter: graphics_info.adapter, + graphics_backend: graphics_info.backend, + } +} diff --git a/sctk/src/util.rs b/sctk/src/util.rs new file mode 100644 index 0000000000..81329b263a --- /dev/null +++ b/sctk/src/util.rs @@ -0,0 +1,128 @@ +/// The behavior of cursor grabbing. +/// +/// Use this enum with [`Window::set_cursor_grab`] to grab the cursor. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum CursorGrabMode { + /// No grabbing of the cursor is performed. + None, + + /// The cursor is confined to the window area. + /// + /// There's no guarantee that the cursor will be hidden. You should hide it by yourself if you + /// want to do so. + /// + /// ## Platform-specific + /// + /// - **macOS:** Not implemented. Always returns [`ExternalError::NotSupported`] for now. + /// - **iOS / Android / Web:** Always returns an [`ExternalError::NotSupported`]. + Confined, + + /// The cursor is locked inside the window area to the certain position. + /// + /// There's no guarantee that the cursor will be hidden. You should hide it by yourself if you + /// want to do so. + /// + /// ## Platform-specific + /// + /// - **X11 / Windows:** Not implemented. Always returns [`ExternalError::NotSupported`] for now. + /// - **iOS / Android:** Always returns an [`ExternalError::NotSupported`]. + Locked, +} + +/// Describes the appearance of the mouse cursor. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum CursorIcon { + /// The platform-dependent default cursor. + Default, + /// A simple crosshair. + Crosshair, + /// A hand (often used to indicate links in web browsers). + Hand, + /// Self explanatory. + Arrow, + /// Indicates something is to be moved. + Move, + /// Indicates text that may be selected or edited. + Text, + /// Program busy indicator. + Wait, + /// Help indicator (often rendered as a "?") + Help, + /// Progress indicator. Shows that processing is being done. But in contrast + /// with "Wait" the user may still interact with the program. Often rendered + /// as a spinning beach ball, or an arrow with a watch or hourglass. + Progress, + + /// Cursor showing that something cannot be done. + NotAllowed, + ContextMenu, + Cell, + VerticalText, + Alias, + Copy, + NoDrop, + /// Indicates something can be grabbed. + Grab, + /// Indicates something is grabbed. + Grabbing, + AllScroll, + ZoomIn, + ZoomOut, + + /// Indicate that some edge is to be moved. For example, the 'SeResize' cursor + /// is used when the movement starts from the south-east corner of the box. + EResize, + NResize, + NeResize, + NwResize, + SResize, + SeResize, + SwResize, + WResize, + EwResize, + NsResize, + NeswResize, + NwseResize, + ColResize, + RowResize, +} + +impl Default for CursorIcon { + fn default() -> Self { + CursorIcon::Default + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Theme { + Light, + Dark, +} + +/// ## Platform-specific +/// +/// - **X11:** Sets the WM's `XUrgencyHint`. No distinction between [`Critical`] and [`Informational`]. +/// +/// [`Critical`]: Self::Critical +/// [`Informational`]: Self::Informational +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UserAttentionType { + /// ## Platform-specific + /// + /// - **macOS:** Bounces the dock icon until the application is in focus. + /// - **Windows:** Flashes both the window and the taskbar button until the application is in focus. + Critical, + /// ## Platform-specific + /// + /// - **macOS:** Bounces the dock icon once. + /// - **Windows:** Flashes the taskbar button until the application is in focus. + Informational, +} + +impl Default for UserAttentionType { + fn default() -> Self { + UserAttentionType::Informational + } +} diff --git a/sctk/src/widget.rs b/sctk/src/widget.rs new file mode 100644 index 0000000000..9f09cb8f21 --- /dev/null +++ b/sctk/src/widget.rs @@ -0,0 +1,232 @@ +//! Display information and interactive controls in your application. +pub use iced_native::widget::helpers::*; + +pub use iced_native::{column, row}; + +/// A container that distributes its contents vertically. +pub type Column<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::Column<'a, Message, Renderer>; + +/// A container that distributes its contents horizontally. +pub type Row<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::Row<'a, Message, Renderer>; + +pub mod text { + //! Write some text for your users to read. + pub use iced_native::widget::text::{Appearance, StyleSheet}; + + /// A paragraph of text. + pub type Text<'a, Renderer = crate::Renderer> = + iced_native::widget::Text<'a, Renderer>; +} + +pub mod button { + //! Allow your users to perform actions by pressing a button. + pub use iced_native::widget::button::{Appearance, StyleSheet}; + + /// A widget that produces a message when clicked. + pub type Button<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::Button<'a, Message, Renderer>; +} + +pub mod checkbox { + //! Show toggle controls using checkboxes. + pub use iced_native::widget::checkbox::{Appearance, StyleSheet}; + + /// A box that can be checked. + pub type Checkbox<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::Checkbox<'a, Message, Renderer>; +} + +pub mod container { + //! Decorate content and apply alignment. + pub use iced_native::widget::container::{Appearance, StyleSheet}; + + /// An element decorating some content. + pub type Container<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::Container<'a, Message, Renderer>; +} + +pub mod pane_grid { + //! Let your users split regions of your application and organize layout dynamically. + //! + //! [![Pane grid - Iced](https://thumbs.gfycat.com/MixedFlatJellyfish-small.gif)](https://gfycat.com/mixedflatjellyfish) + //! + //! # Example + //! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing, + //! drag and drop, and hotkey support. + //! + //! [`pane_grid` example]: https://github.com/iced-rs/iced/tree/0.4/examples/pane_grid + pub use iced_native::widget::pane_grid::{ + Axis, Configuration, Direction, DragEvent, Line, Node, Pane, + ResizeEvent, Split, State, StyleSheet, + }; + + /// A collection of panes distributed using either vertical or horizontal splits + /// to completely fill the space available. + /// + /// [![Pane grid - Iced](https://thumbs.gfycat.com/MixedFlatJellyfish-small.gif)](https://gfycat.com/mixedflatjellyfish) + pub type PaneGrid<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::PaneGrid<'a, Message, Renderer>; + + /// The content of a [`Pane`]. + pub type Content<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::pane_grid::Content<'a, Message, Renderer>; + + /// The title bar of a [`Pane`]. + pub type TitleBar<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::pane_grid::TitleBar<'a, Message, Renderer>; +} + +pub mod pick_list { + //! Display a dropdown list of selectable values. + pub use iced_native::widget::pick_list::{Appearance, StyleSheet}; + + /// A widget allowing the selection of a single value from a list of options. + pub type PickList<'a, T, Message, Renderer = crate::Renderer> = + iced_native::widget::PickList<'a, T, Message, Renderer>; +} + +pub mod radio { + //! Create choices using radio buttons. + pub use iced_native::widget::radio::{Appearance, StyleSheet}; + + /// A circular button representing a choice. + pub type Radio = + iced_native::widget::Radio; +} + +pub mod scrollable { + //! Navigate an endless amount of content with a scrollbar. + pub use iced_native::widget::scrollable::{ + snap_to, style::Scrollbar, style::Scroller, Id, StyleSheet, + }; + + /// A widget that can vertically display an infinite amount of content + /// with a scrollbar. + pub type Scrollable<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::Scrollable<'a, Message, Renderer>; +} + +pub mod toggler { + //! Show toggle controls using togglers. + pub use iced_native::widget::toggler::{Appearance, StyleSheet}; + + /// A toggler widget. + pub type Toggler<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::Toggler<'a, Message, Renderer>; +} + +pub mod text_input { + //! Display fields that can be filled with text. + pub use iced_native::widget::text_input::{ + focus, Appearance, Id, StyleSheet, + }; + + /// A field that can be filled with text. + pub type TextInput<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::TextInput<'a, Message, Renderer>; +} + +pub mod tooltip { + //! Display a widget over another. + pub use iced_native::widget::tooltip::Position; + + /// A widget allowing the selection of a single value from a list of options. + pub type Tooltip<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::Tooltip<'a, Message, Renderer>; +} + +pub use iced_native::widget::progress_bar; +pub use iced_native::widget::rule; +pub use iced_native::widget::slider; +pub use iced_native::widget::Space; + +pub use button::Button; +pub use checkbox::Checkbox; +pub use container::Container; +pub use pane_grid::PaneGrid; +pub use pick_list::PickList; +pub use progress_bar::ProgressBar; +pub use radio::Radio; +pub use rule::Rule; +pub use scrollable::Scrollable; +pub use slider::Slider; +pub use text::Text; +pub use text_input::TextInput; +pub use toggler::Toggler; +pub use tooltip::Tooltip; + +#[cfg(feature = "canvas")] +#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] +pub use iced_graphics::widget::canvas; + +#[cfg(feature = "canvas")] +#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] +/// Creates a new [`Canvas`]. +pub fn canvas(program: P) -> Canvas +where + P: canvas::Program, +{ + Canvas::new(program) +} + +#[cfg(feature = "image")] +#[cfg_attr(docsrs, doc(cfg(feature = "image")))] +pub mod image { + //! Display images in your user interface. + pub use iced_native::image::Handle; + + /// A frame that displays an image. + pub type Image = iced_native::widget::Image; + + pub use iced_native::widget::image::viewer; + pub use viewer::Viewer; +} + +#[cfg(feature = "qr_code")] +#[cfg_attr(docsrs, doc(cfg(feature = "qr_code")))] +pub use iced_graphics::widget::qr_code; + +#[cfg(feature = "svg")] +#[cfg_attr(docsrs, doc(cfg(feature = "svg")))] +pub mod svg { + //! Display vector graphics in your application. + pub use iced_native::svg::Handle; + pub use iced_native::widget::Svg; +} + +#[cfg(feature = "canvas")] +#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] +pub use canvas::Canvas; + +#[cfg(feature = "image")] +#[cfg_attr(docsrs, doc(cfg(feature = "image")))] +pub use image::Image; + +#[cfg(feature = "qr_code")] +#[cfg_attr(docsrs, doc(cfg(feature = "qr_code")))] +pub use qr_code::QRCode; + +#[cfg(feature = "svg")] +#[cfg_attr(docsrs, doc(cfg(feature = "svg")))] +pub use svg::Svg; + +use crate::Command; +use iced_native::widget::operation; + +/// Focuses the previous focusable widget. +pub fn focus_previous() -> Command +where + Message: 'static, +{ + Command::widget(operation::focusable::focus_previous()) +} + +/// Focuses the next focusable widget. +pub fn focus_next() -> Command +where + Message: 'static, +{ + Command::widget(operation::focusable::focus_next()) +} diff --git a/sctk/src/window.rs b/sctk/src/window.rs new file mode 100644 index 0000000000..2353a0c641 --- /dev/null +++ b/sctk/src/window.rs @@ -0,0 +1,3 @@ +pub fn resize() { + todo!() +} diff --git a/src/application.rs b/src/application.rs index 01b2032f43..67175ad5f4 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,4 +1,6 @@ //! Build interactive cross-platform applications. +use iced_core::window::Id; + use crate::{Command, Element, Executor, Settings, Subscription}; pub use crate::style::application::{Appearance, StyleSheet}; diff --git a/src/error.rs b/src/error.rs index 111bedf245..a1d2640057 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,6 @@ use crate::futures; use crate::graphics; +#[cfg(any(feature = "winit", feature = "wayland"))] use crate::shell; /// An error that occurred while running an application. @@ -18,15 +19,21 @@ pub enum Error { GraphicsCreationFailed(graphics::Error), } +#[cfg(any(feature = "winit", feature = "wayland"))] impl From for Error { fn from(error: shell::Error) -> Error { match error { shell::Error::ExecutorCreationFailed(error) => { Error::ExecutorCreationFailed(error) } + #[cfg(feature = "winit")] shell::Error::WindowCreationFailed(error) => { Error::WindowCreationFailed(Box::new(error)) } + #[cfg(feature = "wayland")] + shell::Error::WindowCreationFailed(error) => { + Error::WindowCreationFailed(error) + } shell::Error::GraphicsCreationFailed(error) => { Error::GraphicsCreationFailed(error) } diff --git a/src/lib.rs b/src/lib.rs index d82bc8868c..d0597982ea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -159,30 +159,55 @@ rustdoc::broken_intra_doc_links )] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![cfg_attr(docsrs, feature(doc_cfg))] + +#[cfg(all(feature = "wayland", feature = "winit"))] +compile_error!("cannot use `wayland` feature with `winit"); + +pub use iced_futures::futures; use iced_widget::graphics; use iced_widget::renderer; use iced_widget::style; -use iced_winit as shell; -use iced_winit::core; -use iced_winit::runtime; -pub use iced_futures::futures; +#[cfg(feature = "wayland")] +use iced_sctk as shell; +#[cfg(feature = "winit")] +use iced_winit as shell; +#[cfg(any(feature = "winit", feature = "wayland"))] +use shell::core; +#[cfg(any(feature = "winit", feature = "wayland"))] +use shell::runtime; #[cfg(feature = "highlighter")] pub use iced_highlighter as highlighter; +#[cfg(not(any(feature = "winit", feature = "wayland")))] +pub use iced_widget::core; +#[cfg(not(any(feature = "winit", feature = "wayland")))] +pub use iced_widget::runtime; mod error; -mod sandbox; -pub mod application; pub mod settings; pub mod time; pub mod window; +#[cfg(feature = "winit")] +pub mod application; +#[cfg(feature = "winit")] +mod sandbox; + +/// wayland application +#[cfg(feature = "wayland")] +pub mod wayland; +#[cfg(feature = "wayland")] +pub use wayland::sandbox; +#[cfg(feature = "wayland")] +pub use wayland::Application; + #[cfg(feature = "advanced")] pub mod advanced; -#[cfg(feature = "multi-window")] +#[cfg(all(feature = "winit", feature = "multi-window"))] pub mod multi_window; pub use style::theme; @@ -226,6 +251,8 @@ pub mod font { pub mod event { //! Handle events of a user interface. + #[cfg(feature = "wayland")] + pub use crate::core::event::wayland; pub use crate::core::event::{Event, MacOS, PlatformSpecific, Status}; pub use iced_futures::event::{listen, listen_raw, listen_with}; } @@ -300,6 +327,7 @@ pub mod widget { mod runtime {} } +#[cfg(feature = "winit")] pub use application::Application; pub use command::Command; pub use error::Error; @@ -307,6 +335,7 @@ pub use event::Event; pub use executor::Executor; pub use font::Font; pub use renderer::Renderer; +#[cfg(any(feature = "winit", feature = "wayland"))] pub use sandbox::Sandbox; pub use settings::Settings; pub use subscription::Subscription; diff --git a/src/settings.rs b/src/settings.rs index d9476b614e..d908b9aaaf 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,11 +1,15 @@ -//! Configure your application. +//! Configure your application + +#[cfg(feature = "winit")] use crate::window; use crate::{Font, Pixels}; +#[cfg(feature = "wayland")] +use iced_sctk::settings::InitialSurface; use std::borrow::Cow; /// The settings of an application. -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct Settings { /// The identifier of the application. /// @@ -16,8 +20,13 @@ pub struct Settings { /// The window settings. /// /// They will be ignored on the Web. + #[cfg(feature = "winit")] pub window: window::Settings, + /// The window settings. + #[cfg(feature = "wayland")] + pub initial_surface: InitialSurface, + /// The data needed to initialize the [`Application`]. /// /// [`Application`]: crate::Application @@ -46,15 +55,53 @@ pub struct Settings { /// /// [`Canvas`]: crate::widget::Canvas pub antialiasing: bool, + + /// If set to true the application will exit when the main window is closed. + pub exit_on_close_request: bool, } +#[cfg(not(any(feature = "winit", feature = "wayland")))] impl Settings { /// Initialize [`Application`] settings using the given data. /// /// [`Application`]: crate::Application pub fn with_flags(flags: Flags) -> Self { let default_settings = Settings::<()>::default(); + Self { + flags, + id: default_settings.id, + default_font: default_settings.default_font, + default_text_size: default_settings.default_text_size, + antialiasing: default_settings.antialiasing, + exit_on_close_request: default_settings.exit_on_close_request, + } + } +} +#[cfg(not(any(feature = "winit", feature = "wayland")))] +impl Default for Settings +where + Flags: Default, +{ + fn default() -> Self { + Self { + id: None, + flags: Default::default(), + default_font: Default::default(), + default_text_size: 16.0, + antialiasing: false, + exit_on_close_request: true, + } + } +} + +#[cfg(feature = "winit")] +impl Settings { + /// Initialize [`Application`] settings using the given data. + /// + /// [`Application`]: crate::Application + pub fn with_flags(flags: Flags) -> Self { + let default_settings = Settings::<()>::default(); Self { flags, id: default_settings.id, @@ -63,10 +110,12 @@ impl Settings { default_font: default_settings.default_font, default_text_size: default_settings.default_text_size, antialiasing: default_settings.antialiasing, + exit_on_close_request: default_settings.exit_on_close_request, } } } +#[cfg(feature = "winit")] impl Default for Settings where Flags: Default, @@ -80,10 +129,12 @@ where default_font: Font::default(), default_text_size: Pixels(16.0), antialiasing: false, + exit_on_close_request: false, } } } +#[cfg(feature = "winit")] impl From> for iced_winit::Settings { fn from(settings: Settings) -> iced_winit::Settings { iced_winit::Settings { @@ -94,3 +145,56 @@ impl From> for iced_winit::Settings { } } } + +#[cfg(feature = "wayland")] +impl Settings { + /// Initialize [`Application`] settings using the given data. + /// + /// [`Application`]: crate::Application + pub fn with_flags(flags: Flags) -> Self { + let default_settings = Settings::<()>::default(); + + Self { + flags, + id: default_settings.id, + initial_surface: default_settings.initial_surface, + default_font: default_settings.default_font, + default_text_size: default_settings.default_text_size, + antialiasing: default_settings.antialiasing, + exit_on_close_request: default_settings.exit_on_close_request, + fonts: default_settings.fonts, + } + } +} + +#[cfg(feature = "wayland")] +impl Default for Settings +where + Flags: Default, +{ + fn default() -> Self { + Self { + id: None, + initial_surface: Default::default(), + flags: Default::default(), + default_font: Default::default(), + default_text_size: Pixels(16.0), + antialiasing: false, + fonts: Vec::new(), + exit_on_close_request: true, + } + } +} + +#[cfg(feature = "wayland")] +impl From> for iced_sctk::Settings { + fn from(settings: Settings) -> iced_sctk::Settings { + iced_sctk::Settings { + kbd_repeat: Default::default(), + surface: settings.initial_surface, + flags: settings.flags, + exit_on_close_request: settings.exit_on_close_request, + ptr_theme: None, + } + } +} diff --git a/src/wayland/mod.rs b/src/wayland/mod.rs new file mode 100644 index 0000000000..0265f16482 --- /dev/null +++ b/src/wayland/mod.rs @@ -0,0 +1,196 @@ +use crate::runtime::window::Id; +use crate::{Command, Element, Executor, Settings, Subscription}; + +/// wayland sandbox +pub mod sandbox; +pub use crate::runtime::command::platform_specific::wayland as actions; +pub use crate::style::application::{Appearance, StyleSheet}; +use iced_renderer::graphics::Antialiasing; +pub use iced_sctk::{application::SurfaceIdWrapper, commands::*, settings::*}; + +/// A pure version of [`Application`]. +/// +/// Unlike the impure version, the `view` method of this trait takes an +/// immutable reference to `self` and returns a pure [`Element`]. +/// +/// [`Application`]: crate::Application +/// [`Element`]: pure::Element +pub trait Application: Sized { + /// The [`Executor`] that will run commands and subscriptions. + /// + /// The [default executor] can be a good starting point! + /// + /// [`Executor`]: Self::Executor + /// [default executor]: crate::executor::Default + type Executor: Executor; + + /// The type of __messages__ your [`Application`] will produce. + type Message: std::fmt::Debug + Send; + + /// The theme of your [`Application`]. + type Theme: Default + StyleSheet; + + /// The data needed to initialize your [`Application`]. + type Flags: Clone; + + /// Initializes the [`Application`] with the flags provided to + /// [`run`] as part of the [`Settings`]. + /// + /// Here is where you should return the initial state of your app. + /// + /// Additionally, you can return a [`Command`] if you need to perform some + /// async action in the background on startup. This is useful if you want to + /// load state from a file, perform an initial HTTP request, etc. + /// + /// [`run`]: Self::run + fn new(flags: Self::Flags) -> (Self, Command); + + /// Returns the current title of the [`Application`]. + /// + /// This title can be dynamic! The runtime will automatically update the + /// title of your application when necessary. + fn title(&self, id: Id) -> String; + + /// Handles a __message__ and updates the state of the [`Application`]. + /// + /// This is where you define your __update logic__. All the __messages__, + /// produced by either user interactions or commands, will be handled by + /// this method. + /// + /// Any [`Command`] returned will be executed immediately in the background. + fn update(&mut self, message: Self::Message) -> Command; + + /// Returns the current [`Theme`] of the [`Application`]. + /// + /// [`Theme`]: Self::Theme + fn theme(&self, id: Id) -> Self::Theme { + Self::Theme::default() + } + + /// Returns the current [`Style`] of the [`Theme`]. + /// + /// [`Style`]: ::Style + /// [`Theme`]: Self::Theme + fn style(&self) -> ::Style { + Default::default() + } + + /// Returns the event [`Subscription`] for the current state of the + /// application. + /// + /// A [`Subscription`] will be kept alive as long as you keep returning it, + /// and the __messages__ produced will be handled by + /// [`update`](#tymethod.update). + /// + /// By default, this method returns an empty [`Subscription`]. + fn subscription(&self) -> Subscription { + Subscription::none() + } + + /// Returns the widgets to display in the [`Application`]. + /// + /// These widgets can produce __messages__ based on user interaction. + fn view( + &self, + id: Id, + ) -> Element<'_, Self::Message, Self::Theme, crate::Renderer>; + + /// Returns the scale factor of the [`Application`]. + /// + /// It can be used to dynamically control the size of the UI at runtime + /// (i.e. zooming). + /// + /// For instance, a scale factor of `2.0` will make widgets twice as big, + /// while a scale factor of `0.5` will shrink them to half their size. + /// + /// By default, it returns `1.0`. + fn scale_factor(&self, id: Id) -> f64 { + 1.0 + } + + /// Runs the [`Application`]. + /// + /// On native platforms, this method will take control of the current thread + /// until the [`Application`] exits. + /// + /// On the web platform, this method __will NOT return__ unless there is an + /// [`Error`] during startup. + /// + /// [`Error`]: crate::Error + fn run(settings: Settings) -> crate::Result + where + Self: 'static, + { + #[allow(clippy::needless_update)] + let renderer_settings = crate::renderer::Settings { + default_font: settings.default_font, + default_text_size: settings.default_text_size, + antialiasing: if settings.antialiasing { + Some(Antialiasing::MSAAx4) + } else { + None + }, + ..crate::renderer::Settings::default() + }; + + Ok(crate::shell::application::run::< + Instance, + Self::Executor, + crate::renderer::Compositor, + >(settings.into(), renderer_settings)?) + } +} + +struct Instance(A); + +impl crate::runtime::multi_window::Program for Instance +where + A: Application, +{ + type Theme = A::Theme; + type Renderer = crate::Renderer; + type Message = A::Message; + + fn update(&mut self, message: Self::Message) -> Command { + self.0.update(message) + } + + fn view( + &self, + id: Id, + ) -> Element<'_, Self::Message, Self::Theme, Self::Renderer> { + self.0.view(id) + } +} + +impl crate::shell::Application for Instance +where + A: Application, +{ + type Flags = A::Flags; + + fn new(flags: Self::Flags) -> (Self, Command) { + let (app, command) = A::new(flags); + + (Instance(app), command) + } + + fn title(&self, window: Id) -> String { + self.0.title(window) + } + + fn theme(&self, id: Id) -> A::Theme { + self.0.theme(id) + } + + fn style(&self) -> ::Style { + self.0.style() + } + fn subscription(&self) -> Subscription { + self.0.subscription() + } + + fn scale_factor(&self, window: Id) -> f64 { + self.0.scale_factor(window) + } +} diff --git a/src/wayland/sandbox.rs b/src/wayland/sandbox.rs new file mode 100644 index 0000000000..ead4975350 --- /dev/null +++ b/src/wayland/sandbox.rs @@ -0,0 +1,207 @@ +use iced_core::window::Id; + +use crate::style::Theme; +use crate::theme::{self}; +use crate::{ + wayland::Application, Command, Element, Error, Settings, Subscription, +}; + +/// A sandboxed [`Application`]. +/// +/// If you are a just getting started with the library, this trait offers a +/// simpler interface than [`Application`]. +/// +/// Unlike an [`Application`], a [`Sandbox`] cannot run any asynchronous +/// actions or be initialized with some external flags. However, both traits +/// are very similar and upgrading from a [`Sandbox`] is very straightforward. +/// +/// Therefore, it is recommended to always start by implementing this trait and +/// upgrade only once necessary. +/// +/// # Examples +/// [The repository has a bunch of examples] that use the [`Sandbox`] trait: +/// +/// - [`bezier_tool`], a Paint-like tool for drawing Bézier curves using the +/// [`Canvas widget`]. +/// - [`counter`], the classic counter example explained in [the overview]. +/// - [`custom_widget`], a demonstration of how to build a custom widget that +/// draws a circle. +/// - [`geometry`], a custom widget showcasing how to draw geometry with the +/// `Mesh2D` primitive in [`iced_wgpu`]. +/// - [`pane_grid`], a grid of panes that can be split, resized, and +/// reorganized. +/// - [`progress_bar`], a simple progress bar that can be filled by using a +/// slider. +/// - [`styling`], an example showcasing custom styling with a light and dark +/// theme. +/// - [`svg`], an application that renders the [Ghostscript Tiger] by leveraging +/// the [`Svg` widget]. +/// - [`tour`], a simple UI tour that can run both on native platforms and the +/// web! +/// +/// [The repository has a bunch of examples]: https://github.com/iced-rs/iced/tree/0.4/examples +/// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.4/examples/bezier_tool +/// [`counter`]: https://github.com/iced-rs/iced/tree/0.4/examples/counter +/// [`custom_widget`]: https://github.com/iced-rs/iced/tree/0.4/examples/custom_widget +/// [`geometry`]: https://github.com/iced-rs/iced/tree/0.4/examples/geometry +/// [`pane_grid`]: https://github.com/iced-rs/iced/tree/0.4/examples/pane_grid +/// [`progress_bar`]: https://github.com/iced-rs/iced/tree/0.4/examples/progress_bar +/// [`styling`]: https://github.com/iced-rs/iced/tree/0.4/examples/styling +/// [`svg`]: https://github.com/iced-rs/iced/tree/0.4/examples/svg +/// [`tour`]: https://github.com/iced-rs/iced/tree/0.4/examples/tour +/// [`Canvas widget`]: crate::widget::Canvas +/// [the overview]: index.html#overview +/// [`iced_wgpu`]: https://github.com/iced-rs/iced/tree/0.4/wgpu +/// [`Svg` widget]: crate::widget::Svg +/// [Ghostscript Tiger]: https://commons.wikimedia.org/wiki/File:Ghostscript_Tiger.svg +/// +/// ## A simple "Hello, world!" +/// +/// If you just want to get started, here is a simple [`Sandbox`] that +/// says "Hello, world!": +/// +/// ```no_run +/// use iced::{Element, Sandbox, Settings}; +/// +/// pub fn main() -> iced::Result { +/// Hello::run(Settings::default()) +/// } +/// +/// struct Hello; +/// +/// impl Sandbox for Hello { +/// type Message = (); +/// +/// fn new() -> Hello { +/// Hello +/// } +/// +/// fn title(&self) -> String { +/// String::from("A cool application") +/// } +/// +/// fn update(&mut self, _message: Self::Message) { +/// // This application has no interactions +/// } +/// +/// fn view(&self) -> Element { +/// "Hello, world!".into() +/// } +/// } +/// ``` +pub trait Sandbox { + /// The type of __messages__ your [`Sandbox`] will produce. + type Message: std::fmt::Debug + Send; + + /// Initializes the [`Sandbox`]. + /// + /// Here is where you should return the initial state of your app. + fn new() -> Self; + + /// Returns the current title of the [`Sandbox`]. + /// + /// This title can be dynamic! The runtime will automatically update the + /// title of your application when necessary. + fn title(&self) -> String; + + /// Handles a __message__ and updates the state of the [`Sandbox`]. + /// + /// This is where you define your __update logic__. All the __messages__, + /// produced by user interactions, will be handled by this method. + fn update(&mut self, message: Self::Message); + + /// Returns the widgets to display in the [`Sandbox`] window. + /// + /// These widgets can produce __messages__ based on user interaction. + fn view(&self, id: Id) -> Element<'_, Self::Message, Theme>; + + /// Returns the current [`Theme`] of the [`Sandbox`]. + /// + /// If you want to use your own custom theme type, you will have to use an + /// [`Application`]. + /// + /// By default, it returns [`Theme::default`]. + fn theme(&self) -> Theme { + Theme::default() + } + + /// Returns the current style variant of [`theme::Application`]. + /// + /// By default, it returns [`theme::Application::default`]. + fn style(&self) -> theme::Application { + theme::Application::default() + } + + /// Returns the scale factor of the [`Sandbox`]. + /// + /// It can be used to dynamically control the size of the UI at runtime + /// (i.e. zooming). + /// + /// For instance, a scale factor of `2.0` will make widgets twice as big, + /// while a scale factor of `0.5` will shrink them to half their size. + /// + /// By default, it returns `1.0`. + fn scale_factor(&self) -> f64 { + 1.0 + } + + /// Runs the [`Sandbox`]. + /// + /// On native platforms, this method will take control of the current thread + /// and __will NOT return__. + /// + /// It should probably be that last thing you call in your `main` function. + fn run(settings: Settings<()>) -> Result<(), Error> + where + Self: 'static + Sized, + { + ::run(settings) + } +} + +impl Application for T +where + T: Sandbox, +{ + type Executor = iced_futures::backend::null::Executor; + type Flags = (); + type Message = T::Message; + type Theme = Theme; + + fn new(_flags: ()) -> (Self, Command) { + (T::new(), Command::none()) + } + + fn title(&self, _id: Id) -> String { + T::title(self) + } + + fn update(&mut self, message: T::Message) -> Command { + T::update(self, message); + + Command::none() + } + + fn theme(&self, _id: Id) -> Self::Theme { + T::theme(self) + } + + fn style(&self) -> theme::Application { + T::style(self) + } + + fn subscription(&self) -> Subscription { + Subscription::none() + } + + fn scale_factor(&self, _id: Id) -> f64 { + T::scale_factor(self) + } + + /// Returns the widgets to display in the [`Sandbox`] window. + /// + /// These widgets can produce __messages__ based on user interaction. + fn view(&self, id: Id) -> Element<'_, Self::Message> { + T::view(self, id) + } +} diff --git a/src/window.rs b/src/window.rs index 9f96da5245..1ceb3f9094 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,8 +1,13 @@ //! Configure the window of your application in native platforms. +#[cfg(feature = "winit")] pub mod icon; +#[cfg(feature = "winit")] pub use icon::Icon; +#[cfg(feature = "winit")] +pub use settings::{PlatformSpecific, Settings}; + pub use crate::core::window::*; pub use crate::runtime::window::*; diff --git a/widget/Cargo.toml b/widget/Cargo.toml index 0880477a0b..64dde4be8a 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -22,6 +22,7 @@ canvas = ["iced_renderer/geometry"] qr_code = ["canvas", "qrcode"] wgpu = ["iced_renderer/wgpu"] a11y = ["iced_accessibility"] +wayland = ["sctk"] [dependencies] iced_renderer.workspace = true @@ -29,7 +30,8 @@ iced_runtime.workspace = true iced_style.workspace = true iced_accessibility.workspace = true iced_accessibility.optional = true - +sctk.workspace = true +sctk.optional = true num-traits.workspace = true thiserror.workspace = true unicode-segmentation.workspace = true diff --git a/widget/src/dnd_listener.rs b/widget/src/dnd_listener.rs new file mode 100644 index 0000000000..1529af6a37 --- /dev/null +++ b/widget/src/dnd_listener.rs @@ -0,0 +1,511 @@ +//! A container for capturing mouse events. + +use crate::core::event::wayland::DndOfferEvent; +use crate::core::event::{self, Event, PlatformSpecific}; +use crate::core::layout; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::widget::OperationOutputWrapper; +use crate::core::widget::{tree, Operation, Tree}; +use crate::core::{ + overlay, Clipboard, Element, Layout, Length, Point, Rectangle, Shell, + Widget, +}; +use sctk::reexports::client::protocol::wl_data_device_manager::DndAction; + +use std::u32; + +/// Emit messages on mouse events. +#[allow(missing_debug_implementations)] +pub struct DndListener<'a, Message, Theme, Renderer> { + content: Element<'a, Message, Theme, Renderer>, + + /// Sets the message to emit on a drag enter. + on_enter: + Option, (f32, f32)) -> Message + 'a>>, + + /// Sets the message to emit on a drag motion. + /// x and y are the coordinates of the pointer relative to the widget in the range (0.0, 1.0) + on_motion: Option Message + 'a>>, + + /// Sets the message to emit on a drag exit. + on_exit: Option, + + /// Sets the message to emit on a drag drop. + on_drop: Option, + + /// Sets the message to emit on a drag mime type event. + on_mime_type: Option Message + 'a>>, + + /// Sets the message to emit on a drag action event. + on_source_actions: Option Message + 'a>>, + + /// Sets the message to emit on a drag action event. + on_selected_action: Option Message + 'a>>, + + /// Sets the message to emit on a Data event. + on_data: Option) -> Message + 'a>>, +} + +impl<'a, Message, Theme, Renderer> DndListener<'a, Message, Theme, Renderer> { + /// The message to emit on a drag enter. + #[must_use] + pub fn on_enter( + mut self, + message: impl Fn(DndAction, Vec, (f32, f32)) -> Message + 'a, + ) -> Self { + self.on_enter = Some(Box::new(message)); + self + } + + /// The message to emit on a drag motion. + #[must_use] + pub fn on_motion( + mut self, + message: impl Fn(f32, f32) -> Message + 'a, + ) -> Self { + self.on_motion = Some(Box::new(message)); + self + } + + /// The message to emit on a selected drag action. + #[must_use] + pub fn on_selected_action( + mut self, + message: impl Fn(DndAction) -> Message + 'a, + ) -> Self { + self.on_selected_action = Some(Box::new(message)); + self + } + + /// The message to emit on a drag exit. + #[must_use] + pub fn on_exit(mut self, message: Message) -> Self { + self.on_exit = Some(message); + self + } + + /// The message to emit on a drag drop. + #[must_use] + pub fn on_drop(mut self, message: Message) -> Self { + self.on_drop = Some(message); + self + } + + /// The message to emit on a drag mime type event. + #[must_use] + pub fn on_mime_type( + mut self, + message: impl Fn(String) -> Message + 'a, + ) -> Self { + self.on_mime_type = Some(Box::new(message)); + self + } + + /// The message to emit on a drag action event. + #[must_use] + pub fn on_action( + mut self, + message: impl Fn(DndAction) -> Message + 'a, + ) -> Self { + self.on_source_actions = Some(Box::new(message)); + self + } + + /// The message to emit on a drag read data event. + #[must_use] + pub fn on_data( + mut self, + message: impl Fn(String, Vec) -> Message + 'a, + ) -> Self { + self.on_data = Some(Box::new(message)); + self + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +enum DndState { + #[default] + None, + External(DndAction, Vec), + Hovered(DndAction, Vec), + Dropped, +} + +/// Local state of the [`DndListener`]. +#[derive(Default)] +struct State { + dnd: DndState, +} + +impl<'a, Message, Theme, Renderer> DndListener<'a, Message, Theme, Renderer> { + /// Creates an empty [`DndListener`]. + pub fn new( + content: impl Into>, + ) -> Self { + DndListener { + content: content.into(), + on_enter: None, + on_motion: None, + on_exit: None, + on_drop: None, + on_mime_type: None, + on_source_actions: None, + on_selected_action: None, + on_data: None, + } + } +} + +impl<'a, Message, Theme, Renderer> Widget + for DndListener<'a, Message, Theme, Renderer> +where + Renderer: crate::core::Renderer, + Message: Clone, +{ + fn children(&self) -> Vec { + vec![Tree::new(&self.content)] + } + + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(std::slice::from_mut(&mut self.content)); + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let size = self.size(); + + layout( + renderer, + limits, + size.width, + size.height, + u32::MAX, + u32::MAX, + |renderer, limits| { + self.content.as_widget().layout(tree, renderer, limits) + }, + ) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation>, + ) { + operation.container(None, layout.bounds(), &mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + operation, + ); + }); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + if let event::Status::Captured = self.content.as_widget_mut().on_event( + &mut tree.children[0], + event.clone(), + layout.children().next().unwrap(), + cursor_position, + renderer, + clipboard, + shell, + viewport, + ) { + return event::Status::Captured; + } + + update( + self, + &event, + layout, + shell, + tree.state.downcast_mut::(), + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout.children().next().unwrap(), + cursor_position, + viewport, + renderer, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + renderer_style: &renderer::Style, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + viewport: &Rectangle, + ) { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + renderer_style, + layout.children().next().unwrap(), + cursor_position, + viewport, + ); + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option> { + self.content.as_widget_mut().overlay( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + ) + } + + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::default()) + } + + fn size(&self) -> iced_renderer::core::Size { + self.content.as_widget().size() + } +} + +impl<'a, Message, Theme, Renderer> + From> + for Element<'a, Message, Theme, Renderer> +where + Message: 'a + Clone, + Renderer: 'a + crate::core::Renderer, + Theme: 'a, +{ + fn from( + listener: DndListener<'a, Message, Theme, Renderer>, + ) -> Element<'a, Message, Theme, Renderer> { + Element::new(listener) + } +} + +/// Processes the given [`Event`] and updates the [`State`] of an [`DndListener`] +/// accordingly. +fn update( + widget: &mut DndListener<'_, Message, Theme, Renderer>, + event: &Event, + layout: Layout<'_>, + shell: &mut Shell<'_, Message>, + state: &mut State, +) -> event::Status { + match event { + Event::PlatformSpecific(PlatformSpecific::Wayland( + event::wayland::Event::DndOffer(DndOfferEvent::Enter { + x, + y, + mime_types, + }), + )) => { + let bounds = layout.bounds(); + let p = Point { + x: *x as f32, + y: *y as f32, + }; + if layout.bounds().contains(p) { + state.dnd = + DndState::Hovered(DndAction::empty(), mime_types.clone()); + if let Some(message) = widget.on_enter.as_ref() { + let normalized_x: f32 = (p.x - bounds.x) / bounds.width; + let normalized_y: f32 = (p.y - bounds.y) / bounds.height; + shell.publish(message( + DndAction::empty(), + mime_types.clone(), + (normalized_x, normalized_y), + )); + return event::Status::Captured; + } + } else { + state.dnd = + DndState::External(DndAction::empty(), mime_types.clone()); + } + } + Event::PlatformSpecific(PlatformSpecific::Wayland( + event::wayland::Event::DndOffer(DndOfferEvent::Motion { x, y }), + )) => { + let bounds = layout.bounds(); + let p = Point { + x: *x as f32, + y: *y as f32, + }; + // motion can trigger an enter, motion or leave event on the widget + if let DndState::Hovered(action, mime_types) = &state.dnd { + if !bounds.contains(p) { + state.dnd = DndState::External(*action, mime_types.clone()); + if let Some(message) = widget.on_exit.clone() { + shell.publish(message); + return event::Status::Captured; + } + } else if let Some(message) = widget.on_motion.as_ref() { + let normalized_x: f32 = (p.x - bounds.x) / bounds.width; + let normalized_y: f32 = (p.y - bounds.y) / bounds.height; + shell.publish(message(normalized_x, normalized_y)); + return event::Status::Captured; + } + } else if bounds.contains(p) { + state.dnd = match &state.dnd { + DndState::External(a, m) => { + DndState::Hovered(*a, m.clone()) + } + _ => DndState::Hovered(DndAction::empty(), vec![]), + }; + let (action, mime_types) = match &state.dnd { + DndState::Hovered(action, mime_types) => { + (action, mime_types) + } + _ => return event::Status::Ignored, + }; + + if let Some(message) = widget.on_enter.as_ref() { + let normalized_x: f32 = (p.x - bounds.x) / bounds.width; + let normalized_y: f32 = (p.y - bounds.y) / bounds.height; + shell.publish(message( + *action, + mime_types.clone(), + (normalized_x, normalized_y), + )); + return event::Status::Captured; + } + } + } + Event::PlatformSpecific(PlatformSpecific::Wayland( + event::wayland::Event::DndOffer(DndOfferEvent::Leave), + )) => { + if !matches!(state.dnd, DndState::Dropped) { + state.dnd = DndState::None; + } + + if let Some(message) = widget.on_exit.clone() { + shell.publish(message); + return event::Status::Captured; + } + } + Event::PlatformSpecific(PlatformSpecific::Wayland( + event::wayland::Event::DndOffer(DndOfferEvent::DropPerformed), + )) => { + if matches!(state.dnd, DndState::Hovered(..)) { + state.dnd = DndState::Dropped; + } + if let Some(message) = widget.on_drop.clone() { + shell.publish(message); + return event::Status::Captured; + } + } + Event::PlatformSpecific(PlatformSpecific::Wayland( + event::wayland::Event::DndOffer(DndOfferEvent::DndData { + mime_type, + data, + }), + )) => { + match &mut state.dnd { + DndState::Hovered(_, mime_types) => { + if !mime_types.contains(mime_type) { + return event::Status::Ignored; + } + } + DndState::None | DndState::External(..) => { + return event::Status::Ignored + } + DndState::Dropped => {} + }; + if let Some(message) = widget.on_data.as_ref() { + shell.publish(message(mime_type.clone(), data.clone())); + return event::Status::Captured; + } + } + Event::PlatformSpecific(PlatformSpecific::Wayland( + event::wayland::Event::DndOffer(DndOfferEvent::SourceActions( + actions, + )), + )) => { + match &mut state.dnd { + DndState::Hovered(ref mut action, _) => *action = *actions, + DndState::External(ref mut action, _) => *action = *actions, + DndState::Dropped => {} + DndState::None => { + state.dnd = DndState::External(*actions, vec![]) + } + }; + if let Some(message) = widget.on_source_actions.as_ref() { + shell.publish(message(*actions)); + return event::Status::Captured; + } + } + Event::PlatformSpecific(PlatformSpecific::Wayland( + event::wayland::Event::DndOffer(DndOfferEvent::SelectedAction( + action, + )), + )) => { + if let Some(message) = widget.on_selected_action.as_ref() { + shell.publish(message(*action)); + return event::Status::Captured; + } + } + _ => {} + }; + event::Status::Ignored +} + +/// Computes the layout of a [`DndListener`]. +pub fn layout( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + height: Length, + max_height: u32, + max_width: u32, + layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, +) -> layout::Node { + let limits = limits + .loose() + .max_height(max_height as f32) + .max_width(max_width as f32) + .width(width) + .height(height); + + let content = layout_content(renderer, &limits); + let size = limits.resolve(width, height, content.size()); + + layout::Node::with_children(size, vec![content]) +} diff --git a/widget/src/dnd_source.rs b/widget/src/dnd_source.rs new file mode 100644 index 0000000000..bf306c64f1 --- /dev/null +++ b/widget/src/dnd_source.rs @@ -0,0 +1,423 @@ +//! A widget that can be dragged and dropped. + +use sctk::reexports::client::protocol::wl_data_device_manager::DndAction; + +use crate::core::{ + event, layout, mouse, overlay, touch, Clipboard, Element, Event, Length, + Point, Rectangle, Shell, Size, Widget, +}; + +use crate::core::widget::{ + operation::OperationOutputWrapper, tree, Operation, Tree, +}; + +/// A widget that can be dragged and dropped. +#[allow(missing_debug_implementations)] +pub struct DndSource<'a, Message, Theme, Renderer> { + content: Element<'a, Message, Theme, Renderer>, + + on_drag: Option Message + 'a>>, + + on_cancelled: Option, + + on_finished: Option, + + on_dropped: Option, + + on_selection_action: Option Message + 'a>>, + + drag_threshold: f32, + + /// Whether or not captured events should be handled by the widget. + handle_captured_events: bool, +} + +impl<'a, Message, Widget, Renderer> DndSource<'a, Message, Widget, Renderer> { + /// The message to produce when the drag starts. + /// + /// Receives the size of the source widget, so the caller is able to size the + /// drag surface to match. + #[must_use] + pub fn on_drag(mut self, f: F) -> Self + where + F: Fn(Size) -> Message + 'a, + { + self.on_drag = Some(Box::new(f)); + self + } + + /// The message to produce when the drag is cancelled. + #[must_use] + pub fn on_cancelled(mut self, message: Message) -> Self { + self.on_cancelled = Some(message); + self + } + + /// The message to produce when the drag is finished. + #[must_use] + pub fn on_finished(mut self, message: Message) -> Self { + self.on_finished = Some(message); + self + } + + /// The message to produce when the drag is dropped. + #[must_use] + pub fn on_dropped(mut self, message: Message) -> Self { + self.on_dropped = Some(message); + self + } + + /// The message to produce when the selection action is triggered. + #[must_use] + pub fn on_selection_action(mut self, f: F) -> Self + where + F: Fn(DndAction) -> Message + 'a, + { + self.on_selection_action = Some(Box::new(f)); + self + } + + /// The drag radius threshold. + /// if the mouse is moved more than this radius while pressed, the drag event is triggered + #[must_use] + pub fn drag_threshold(mut self, radius: f32) -> Self { + self.drag_threshold = radius.powi(2); + self + } + + /// Whether or not captured events should be handled by the widget. + #[must_use] + pub fn handle_captured_events( + mut self, + handle_captured_events: bool, + ) -> Self { + self.handle_captured_events = handle_captured_events; + self + } +} + +/// Local state of the [`MouseListener`]. +#[derive(Default)] +struct State { + hovered: bool, + left_pressed_position: Option, + is_dragging: bool, +} + +impl<'a, Message, Widget, Renderer> DndSource<'a, Message, Widget, Renderer> { + /// Creates a new [`DndSource`]. + #[must_use] + pub fn new( + content: impl Into>, + ) -> Self { + Self { + content: content.into(), + on_drag: None, + on_cancelled: None, + on_finished: None, + on_dropped: None, + on_selection_action: None, + drag_threshold: 25.0, + handle_captured_events: true, + } + } +} + +impl<'a, Message, Theme, Renderer> From> + for Element<'a, Message, Theme, Renderer> +where + Renderer: crate::core::Renderer + 'a, + Message: Clone + 'a, + Theme: 'a, +{ + fn from( + dnd_source: DndSource<'a, Message, Theme, Renderer>, + ) -> Element<'a, Message, Theme, Renderer> { + Element::new(dnd_source) + } +} + +impl<'a, Message, Theme, Renderer> Widget + for DndSource<'a, Message, Theme, Renderer> +where + Renderer: crate::core::Renderer, + Message: Clone, +{ + fn children(&self) -> Vec { + vec![Tree::new(&self.content)] + } + + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(std::slice::from_mut(&mut self.content)); + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let size = self.size(); + layout( + renderer, + limits, + size.width, + size.height, + u32::MAX, + u32::MAX, + |renderer, limits| { + self.content.as_widget().layout(tree, renderer, limits) + }, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + renderer_style: &crate::core::renderer::Style, + layout: crate::core::Layout<'_>, + cursor_position: mouse::Cursor, + viewport: &crate::core::Rectangle, + ) { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + renderer_style, + layout.children().next().unwrap(), + cursor_position, + viewport, + ); + } + + fn operate( + &self, + tree: &mut Tree, + layout: layout::Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation>, + ) { + operation.container(None, layout.bounds(), &mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + operation, + ); + }); + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: layout::Layout<'_>, + renderer: &Renderer, + ) -> Option> { + self.content.as_widget_mut().overlay( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + ) + } + + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::default()) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: layout::Layout<'_>, + cursor_position: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + let captured = self.content.as_widget_mut().on_event( + &mut tree.children[0], + event.clone(), + layout.children().next().unwrap(), + cursor_position, + renderer, + clipboard, + shell, + viewport, + ); + + if captured == event::Status::Captured && !self.handle_captured_events { + return event::Status::Captured; + } + + let state = tree.state.downcast_mut::(); + + if matches!( + event, + Event::PlatformSpecific(event::PlatformSpecific::Wayland( + event::wayland::Event::Seat( + event::wayland::SeatEvent::Leave, + _ + ) + )) | Event::Mouse(mouse::Event::ButtonReleased( + mouse::Button::Left + )) | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) + ) { + state.left_pressed_position = None; + return event::Status::Captured; + } + + if state.is_dragging { + if let Event::PlatformSpecific(event::PlatformSpecific::Wayland( + event::wayland::Event::DataSource( + event::wayland::DataSourceEvent::Cancelled, + ), + )) = event + { + if let Some(on_cancelled) = self.on_cancelled.clone() { + state.is_dragging = false; + shell.publish(on_cancelled); + return event::Status::Captured; + } + } + + if let Event::PlatformSpecific(event::PlatformSpecific::Wayland( + event::wayland::Event::DataSource( + event::wayland::DataSourceEvent::DndFinished, + ), + )) = event + { + if let Some(on_finished) = self.on_finished.clone() { + state.is_dragging = false; + shell.publish(on_finished); + return event::Status::Captured; + } + } + + if let Event::PlatformSpecific(event::PlatformSpecific::Wayland( + event::wayland::Event::DataSource( + event::wayland::DataSourceEvent::DndDropPerformed, + ), + )) = event + { + if let Some(on_dropped) = self.on_dropped.clone() { + shell.publish(on_dropped); + return event::Status::Captured; + } + } + + if let Event::PlatformSpecific(event::PlatformSpecific::Wayland( + event::wayland::Event::DataSource( + event::wayland::DataSourceEvent::DndActionAccepted(action), + ), + )) = event + { + if let Some(on_action) = self.on_selection_action.as_deref() { + shell.publish(on_action(action)); + return event::Status::Captured; + } + } + } + + let Some(cursor_position) = cursor_position.position() else { + return captured; + }; + + if cursor_position.x > 0.0 + && cursor_position.y > 0.0 + && !layout.bounds().contains(cursor_position) + { + // XXX if the widget is not hovered but the mouse is pressed, + // we are triggering on_drag + if let (Some(on_drag), Some(_)) = + (self.on_drag.as_ref(), state.left_pressed_position.take()) + { + shell.publish(on_drag(layout.bounds().size())); + state.is_dragging = true; + return event::Status::Captured; + }; + return captured; + } + + state.hovered = true; + if let (Some(on_drag), Some(pressed_pos)) = + (self.on_drag.as_ref(), state.left_pressed_position.clone()) + { + if cursor_position.x < 0.0 || cursor_position.y < 0.0 { + return captured; + } + let distance = (cursor_position.x - pressed_pos.x).powi(2) + + (cursor_position.y - pressed_pos.y).powi(2); + if distance > self.drag_threshold { + state.left_pressed_position = None; + state.is_dragging = true; + shell.publish(on_drag(layout.bounds().size())); + return event::Status::Captured; + } + } + + if self.on_drag.is_some() { + if let Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerPressed { .. }) = event + { + state.left_pressed_position = Some(cursor_position); + return event::Status::Captured; + } + } + + captured + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: layout::Layout<'_>, + cursor_position: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout.children().next().unwrap(), + cursor_position, + viewport, + renderer, + ) + } + + fn size(&self) -> Size { + self.content.as_widget().size() + } +} + +/// Computes the layout of a [`DndSource`]. +pub fn layout( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + height: Length, + max_height: u32, + max_width: u32, + layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, +) -> layout::Node { + let limits = limits + .loose() + .max_height(max_height as f32) + .max_width(max_width as f32) + .width(width) + .height(height); + + let content = layout_content(renderer, &limits); + let size = limits.resolve(width, height, content.size()); + + layout::Node::with_children(size, vec![content]) +} diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 2f22858786..bfc8abea91 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -22,6 +22,11 @@ use crate::toggler::{self, Toggler}; use crate::tooltip::{self, Tooltip}; use crate::{Column, MouseArea, Row, Space, Themer, VerticalSlider}; +#[cfg(feature = "wayland")] +use crate::dnd_listener::DndListener; +#[cfg(feature = "wayland")] +use crate::dnd_source::DndSource; + use std::borrow::Cow; use std::ops::RangeInclusive; @@ -367,7 +372,9 @@ pub fn image<'a, Handle>( /// [`Svg`]: crate::Svg /// [`Handle`]: crate::svg::Handle #[cfg(feature = "svg")] -pub fn svg(handle: impl Into) -> crate::Svg +pub fn svg<'a, Theme>( + handle: impl Into, +) -> crate::Svg<'a, Theme> where Theme: crate::svg::StyleSheet, { @@ -435,3 +442,25 @@ where { Themer::new(theme, content) } + +#[cfg(feature = "wayland")] +/// A container for a dnd source +pub fn dnd_source<'a, Message, Theme, Renderer>( + widget: impl Into>, +) -> DndSource<'a, Message, Theme, Renderer> +where + Renderer: core::Renderer, +{ + DndSource::new(widget) +} + +#[cfg(feature = "wayland")] +/// A container for a dnd target +pub fn dnd_listener<'a, Message, Theme, Renderer>( + widget: impl Into>, +) -> DndListener<'a, Message, Theme, Renderer> +where + Renderer: core::Renderer, +{ + DndListener::new(widget) +} diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index f4ba3f1bbf..06ae31c9a3 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -512,11 +512,11 @@ where ) }) } - fn id(&self) -> Option { + fn id(&self) -> Option { self.with_element(|element| element.as_widget().id()) } - fn set_id(&mut self, _id: iced_accessibility::Id) { + fn set_id(&mut self, _id: iced_runtime::core::id::Id) { self.with_element_mut(|element| element.as_widget_mut().set_id(_id)); } diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index 7eb28fab58..946fb159b6 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -347,7 +347,7 @@ where self.content.borrow().element.as_widget().id() } - fn set_id(&mut self, _id: iced_accessibility::Id) { + fn set_id(&mut self, _id: iced_runtime::core::id::Id) { self.content .borrow_mut() .element diff --git a/widget/src/lib.rs b/widget/src/lib.rs index cefafdbebe..d89a01058c 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -137,3 +137,9 @@ pub use qr_code::QRCode; pub use renderer::Renderer; pub use style::theme::{self, Theme}; +#[cfg(feature = "wayland")] +#[doc(no_inline)] +pub mod dnd_listener; +#[cfg(feature = "wayland")] +#[doc(no_inline)] +pub mod dnd_source; diff --git a/widget/src/slider.rs b/widget/src/slider.rs index 2e22828704..5a82aac3c1 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -15,13 +15,16 @@ use crate::core::{ Shell, Size, Widget, }; -use std::borrow::Cow; use std::ops::RangeInclusive; +use iced_renderer::core::border::Radius; pub use iced_style::slider::{ Appearance, Handle, HandleShape, Rail, StyleSheet, }; +#[cfg(feature = "a11y")] +use std::borrow::Cow; + /// An horizontal bar and a handle that selects a single value from a range of /// values. /// @@ -610,16 +613,35 @@ pub fn draw( } else { theme.active(style) }; + let border_width = style + .handle + .border_width + .min(bounds.height / 2.0) + .min(bounds.width / 2.0); let (handle_width, handle_height, handle_border_radius) = match style.handle.shape { HandleShape::Circle { radius } => { - (radius * 2.0, radius * 2.0, radius.into()) + let radius = (radius) + .max(2.0 * border_width) + .min(bounds.height / 2.0) + .min(bounds.width / 2.0); + (radius * 2.0, radius * 2.0, Radius::from(radius)) } HandleShape::Rectangle { width, border_radius, - } => (f32::from(width), bounds.height, border_radius), + } => { + let width = (f32::from(width)) + .max(2.0 * border_width) + .min(bounds.width); + let height = bounds.height; + let mut border_radius: [f32; 4] = border_radius.into(); + for r in &mut border_radius { + *r = (*r).min(height / 2.0).min(width / 2.0).max(0.0); + } + (width, height, border_radius.into()) + } }; let value = value.into() as f32; @@ -638,6 +660,7 @@ pub fn draw( let rail_y = bounds.y + bounds.height / 2.0; + // rail renderer.fill_quad( renderer::Quad { bounds: Rectangle { @@ -652,6 +675,7 @@ pub fn draw( style.rail.colors.0, ); + // right rail renderer.fill_quad( renderer::Quad { bounds: Rectangle { @@ -666,11 +690,12 @@ pub fn draw( style.rail.colors.1, ); + // handle renderer.fill_quad( renderer::Quad { bounds: Rectangle { x: bounds.x + offset, - y: rail_y - handle_height / 2.0, + y: rail_y - (handle_height / 2.0), width: handle_width, height: handle_height, }, diff --git a/widget/src/svg.rs b/widget/src/svg.rs index eee5bf5a2d..6ee0dfd9c6 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -23,7 +23,7 @@ pub use svg::Handle; /// [`Svg`] images can have a considerable rendering cost when resized, /// specially when they are complex. #[allow(missing_debug_implementations)] -pub struct Svg +pub struct Svg<'a, Theme = crate::Theme> where Theme: StyleSheet, { @@ -41,7 +41,7 @@ where style: ::Style, } -impl Svg +impl<'a, Theme> Svg<'a, Theme> where Theme: StyleSheet, { @@ -138,7 +138,8 @@ where } } -impl Widget for Svg +impl<'a, Message, Theme, Renderer> Widget + for Svg<'a, Theme> where Theme: iced_style::svg::StyleSheet, Renderer: svg::Renderer, @@ -293,13 +294,13 @@ where } } -impl<'a, Message, Theme, Renderer> From> +impl<'a, Message, Theme, Renderer> From> for Element<'a, Message, Theme, Renderer> where Theme: iced_style::svg::StyleSheet + 'a, Renderer: svg::Renderer + 'a, { - fn from(icon: Svg) -> Element<'a, Message, Theme, Renderer> { + fn from(icon: Svg<'a, Theme>) -> Element<'a, Message, Theme, Renderer> { Element::new(icon) } } diff --git a/widget/src/text_input/mod.rs b/widget/src/text_input/mod.rs new file mode 100644 index 0000000000..8289bc2ba9 --- /dev/null +++ b/widget/src/text_input/mod.rs @@ -0,0 +1,10 @@ +//! Display fields that can be filled with text. +//! +//! A [`TextInput`] has some local [`State`]. +pub(crate) mod editor; +pub(crate) mod value; + +pub mod cursor; + +mod text_input; +pub use text_input::*; diff --git a/widget/src/text_input.rs b/widget/src/text_input/text_input.rs similarity index 99% rename from widget/src/text_input.rs rename to widget/src/text_input/text_input.rs index b5e45ea868..3c65e100c9 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input/text_input.rs @@ -1,16 +1,14 @@ //! Display fields that can be filled with text. //! //! A [`TextInput`] has some local [`State`]. -mod editor; -mod value; +pub use super::cursor::Cursor; +pub use super::value::Value; -pub mod cursor; - -pub use cursor::Cursor; +use super::cursor; +use super::editor; +use super::editor::Editor; +use super::value; use iced_renderer::core::widget::OperationOutputWrapper; -pub use value::Value; - -use editor::Editor; use crate::core::alignment; use crate::core::event::{self, Event}; diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index 489c9e8902..5b4652b359 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -152,6 +152,9 @@ pub fn window_event( WindowEvent::CloseRequested => { Some(Event::Window(id, window::Event::CloseRequested)) } + WindowEvent::CloseRequested => { + Some(Event::Window(id, window::Event::CloseRequested)) + } WindowEvent::CursorMoved { position, .. } => { let position = position.to_logical::(scale_factor);