diff --git a/Cargo.toml b/Cargo.toml index 9c45b2f588..dc6317b1eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,23 +12,27 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"] categories = ["gui"] [features] -default = ["wgpu"] +default = ["dyrend", "winit"] # Enables the `Image` widget -image = ["iced_wgpu?/image", "iced_glow?/image", "image_rs"] +image = ["iced_wgpu?/image", "iced_glow?/image", "iced_softbuffer?/image", "iced_dyrend?/image", "image_rs"] # Enables the `Svg` widget -svg = ["iced_wgpu?/svg", "iced_glow?/svg"] +svg = ["iced_wgpu?/svg", "iced_glow?/svg", "iced_softbuffer?/svg", "iced_dyrend?/svg"] # Enables the `Canvas` widget canvas = ["iced_graphics/canvas"] # Enables the `QRCode` widget qr_code = ["iced_graphics/qr_code"] -# Enables the `iced_wgpu` renderer +# Enables the `iced_dyrend` renderer +dyrend = ["iced_dyrend"] +# Enables the `iced_wgpu` renderer. Conflicts with `iced_glow` and `iced_softbuffer` wgpu = ["iced_wgpu"] +# Enables the `iced_softbuffer` renderer. Conflicts with `iced_wgpu` and `iced_glow` +softbuffer = ["iced_softbuffer"] +# Enables the `iced_glow` renderer. Conflicts with `iced_wgpu` and `iced_softbuffer` +glow = ["iced_glow", "iced_glutin"] # Enables using system fonts default_system_font = ["iced_wgpu?/default_system_font", "iced_glow?/default_system_font"] -# Enables the `iced_glow` renderer. Overrides `iced_wgpu` -glow = ["iced_glow", "iced_glutin"] # Enables a debug view in native platforms (press F12) -debug = ["iced_winit/debug"] +debug = ["iced_winit?/debug"] # Enables `tokio` as the `executor::Default` on native platforms tokio = ["iced_futures/tokio"] # Enables `async-std` as the `executor::Default` on native platforms @@ -38,7 +42,15 @@ smol = ["iced_futures/smol"] # Enables advanced color conversion via `palette` palette = ["iced_core/palette"] # Enables querying system information -system = ["iced_winit/system"] +system = ["iced_winit?/system"] +# Enables the glutin shell. Conflicts with `sctk` and `winit`. +# Forces the `glow` renderer, further conflicting with `wgpu` and `softbuffer`. +glutin = ["iced_glutin", "iced_glow"] +# Enables the wayland shell. Conflicts with `winit` and `glutin`. +# Incompatible with the `softbuffer` and `wgpu` renderer. +wayland = ["iced_sctk"] +# Enables the winit shell. Conflicts with `wayland` and `glutin`. +winit = ["iced_winit"] [badges] maintenance = { status = "actively-developed" } @@ -46,26 +58,32 @@ maintenance = { status = "actively-developed" } [workspace] members = [ "core", + "dyrend", "futures", "graphics", "glow", "glutin", "lazy", "native", + "softbuffer", "style", "wgpu", "winit", + "sctk", "examples/*", ] [dependencies] iced_core = { version = "0.6", path = "core" } +iced_dyrend = { version = "0.1", path = "dyrend", optional = true } iced_futures = { version = "0.5", path = "futures" } iced_native = { version = "0.7", path = "native" } iced_graphics = { version = "0.5", path = "graphics" } -iced_winit = { version = "0.6", path = "winit", features = ["application"] } +iced_winit = { version = "0.6", path = "winit", features = ["application"], optional = true } iced_glutin = { version = "0.5", path = "glutin", optional = true } iced_glow = { version = "0.5", path = "glow", optional = true } +iced_sctk = { path = "./sctk", optional = true } +iced_softbuffer = { version = "0.1", path = "softbuffer", optional = true } thiserror = "1.0" [dependencies.image_rs] @@ -92,3 +110,7 @@ incremental = false opt-level = 3 overflow-checks = false strip = "debuginfo" + +[patch."https://github.com/iced-rs/winit.git".winit] +git = "https://github.com/pop-os/winit.git" +branch = "iced" diff --git a/core/src/lib.rs b/core/src/lib.rs index f95d61f642..7dd9372885 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -36,6 +36,7 @@ mod font; mod length; mod padding; mod point; +mod radius; mod rectangle; mod size; mod vector; @@ -48,6 +49,7 @@ pub use font::Font; pub use length::Length; pub use padding::Padding; pub use point::Point; +pub use radius::BorderRadius; pub use rectangle::Rectangle; pub use size::Size; pub use vector::Vector; diff --git a/core/src/radius.rs b/core/src/radius.rs new file mode 100644 index 0000000000..b9a302c062 --- /dev/null +++ b/core/src/radius.rs @@ -0,0 +1,22 @@ +/// The border radi for the corners of a graphics primitive in the order: +/// top-left, top-right, bottom-right, bottom-left. +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub struct BorderRadius([f32; 4]); + +impl From for BorderRadius { + fn from(w: f32) -> Self { + Self([w; 4]) + } +} + +impl From<[f32; 4]> for BorderRadius { + fn from(radi: [f32; 4]) -> Self { + Self(radi) + } +} + +impl From for [f32; 4] { + fn from(radi: BorderRadius) -> Self { + radi.0 + } +} diff --git a/dyrend/Cargo.toml b/dyrend/Cargo.toml new file mode 100644 index 0000000000..30ef6f6430 --- /dev/null +++ b/dyrend/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "iced_dyrend" +version = "0.1.0" +authors = ["Jeremy Soller "] +edition = "2021" +description = "A dynamicly chosen renderer for Iced" +license = "MIT AND OFL-1.1" +repository = "https://github.com/iced-rs/iced" + +[dependencies] +log = "0.4" +raw-window-handle = "0.5" + +[dependencies.iced_native] +path = "../native" + +[dependencies.iced_graphics] +path = "../graphics" +features = ["font-fallback", "font-icons"] + +[dependencies.iced_glow] +path = "../glow" +default-features = false +optional = true + +[dependencies.iced_glutin] +path = "../glutin" +default-features = false +optional = true + + +[dependencies.iced_softbuffer] +path = "../softbuffer" +default-features = false +optional = true + +[dependencies.iced_wgpu] +path = "../wgpu" +default-features = false +optional = true + +[features] +default = ["softbuffer"] +image = ["iced_graphics/image", "iced_glow?/image", "iced_softbuffer?/image", "iced_wgpu?/image"] +svg = ["iced_graphics/svg", "iced_glow?/svg", "iced_softbuffer?/svg", "iced_wgpu?/svg"] +glow = ["iced_glow", "iced_glutin"] +softbuffer = ["iced_softbuffer"] +wgpu = ["iced_wgpu"] + +[package.metadata.docs.rs] +rustdoc-args = ["--cfg", "docsrs"] +all-features = true diff --git a/dyrend/README.md b/dyrend/README.md new file mode 100644 index 0000000000..e656fd2e4c --- /dev/null +++ b/dyrend/README.md @@ -0,0 +1,3 @@ +# `iced_softbuffer` + +Software rendering for Iced \ No newline at end of file diff --git a/dyrend/src/lib.rs b/dyrend/src/lib.rs new file mode 100644 index 0000000000..f541fb5cb3 --- /dev/null +++ b/dyrend/src/lib.rs @@ -0,0 +1,13 @@ +//! A [`softbuffer`] renderer for [`iced_native`]. +#![doc( + html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" +)] +#![cfg_attr(docsrs, feature(doc_cfg))] + +mod renderer; +pub use self::renderer::Renderer; + +pub mod settings; +pub use self::settings::Settings; + +pub mod window; diff --git a/dyrend/src/renderer.rs b/dyrend/src/renderer.rs new file mode 100644 index 0000000000..c938fd4cf8 --- /dev/null +++ b/dyrend/src/renderer.rs @@ -0,0 +1,274 @@ +use iced_graphics::{Backend, Vector}; +#[cfg(feature = "image")] +use iced_native::image; +use iced_native::layout; +use iced_native::renderer; +#[cfg(feature = "svg")] +use iced_native::svg; +use iced_native::text::{self, Text}; +use iced_native::{Background, Color, Element, Font, Point, Rectangle, Size}; + +pub enum Renderer { + #[cfg(feature = "glow")] + Glow(iced_glow::Renderer), + #[cfg(feature = "softbuffer")] + softbuffer(iced_softbuffer::Renderer), + #[cfg(feature = "wgpu")] + Wgpu(iced_wgpu::Renderer), +} + +impl iced_native::Renderer for Renderer { + type Theme = T; + + fn layout<'a, Message>( + &mut self, + element: &Element<'a, Message, Self>, + limits: &layout::Limits, + ) -> layout::Node { + let layout = element.as_widget().layout(self, limits); + + match self { + #[cfg(feature = "glow")] + Renderer::Glow(renderer) => { + renderer.backend_mut().trim_measurements(); + } + #[cfg(feature = "softbuffer")] + Renderer::softbuffer(renderer) => { + renderer.backend_mut().trim_measurements(); + } + #[cfg(feature = "wgpu")] + Renderer::Wgpu(renderer) => { + renderer.backend_mut().trim_measurements(); + } + } + + layout + } + + fn with_layer(&mut self, bounds: Rectangle, f: impl FnOnce(&mut Self)) { + let self_ptr = self as *mut _; + match self { + #[cfg(feature = "glow")] + Renderer::Glow(renderer) => unsafe { + // TODO: find a way to do this safely + renderer.with_layer(bounds, |_| f(&mut *self_ptr)) + }, + #[cfg(feature = "softbuffer")] + Renderer::softbuffer(renderer) => unsafe { + // TODO: find a way to do this safely + renderer.with_layer(bounds, |_| f(&mut *self_ptr)) + }, + #[cfg(feature = "wgpu")] + Renderer::Wgpu(renderer) => unsafe { + // TODO: find a way to do this safely + renderer.with_layer(bounds, |_| f(&mut *self_ptr)) + }, + } + } + + fn with_translation( + &mut self, + translation: Vector, + f: impl FnOnce(&mut Self), + ) { + let self_ptr = self as *mut _; + match self { + #[cfg(feature = "glow")] + Renderer::Glow(renderer) => unsafe { + // TODO: find a way to do this safely + renderer.with_translation(translation, |_| f(&mut *self_ptr)) + }, + #[cfg(feature = "softbuffer")] + Renderer::softbuffer(renderer) => unsafe { + // TODO: find a way to do this safely + renderer.with_translation(translation, |_| f(&mut *self_ptr)) + }, + #[cfg(feature = "wgpu")] + Renderer::Wgpu(renderer) => unsafe { + // TODO: find a way to do this safely + renderer.with_translation(translation, |_| f(&mut *self_ptr)) + }, + } + } + + fn fill_quad( + &mut self, + quad: renderer::Quad, + background: impl Into, + ) { + match self { + #[cfg(feature = "glow")] + Renderer::Glow(renderer) => renderer.fill_quad(quad, background), + #[cfg(feature = "softbuffer")] + Renderer::softbuffer(renderer) => { + renderer.fill_quad(quad, background) + } + #[cfg(feature = "wgpu")] + Renderer::Wgpu(renderer) => renderer.fill_quad(quad, background), + } + } + + fn clear(&mut self) { + match self { + #[cfg(feature = "glow")] + Renderer::Glow(renderer) => renderer.clear(), + #[cfg(feature = "softbuffer")] + Renderer::softbuffer(renderer) => renderer.clear(), + #[cfg(feature = "wgpu")] + Renderer::Wgpu(renderer) => renderer.clear(), + } + } +} + +impl text::Renderer for Renderer { + type Font = Font; + + //TODO: use the right values here for each backend + const ICON_FONT: Font = Font::Default; + const CHECKMARK_ICON: char = '✓'; + const ARROW_DOWN_ICON: char = '⌄'; + + fn default_size(&self) -> u16 { + match self { + #[cfg(feature = "glow")] + Renderer::Glow(renderer) => renderer.default_size(), + #[cfg(feature = "softbuffer")] + Renderer::softbuffer(renderer) => renderer.default_size(), + #[cfg(feature = "wgpu")] + Renderer::Wgpu(renderer) => renderer.default_size(), + } + } + + fn measure( + &self, + content: &str, + size: u16, + font: Font, + bounds: Size, + ) -> (f32, f32) { + match self { + #[cfg(feature = "glow")] + Renderer::Glow(renderer) => { + renderer.measure(content, size, font, bounds) + } + #[cfg(feature = "softbuffer")] + Renderer::softbuffer(renderer) => { + renderer.measure(content, size, font, bounds) + } + #[cfg(feature = "wgpu")] + Renderer::Wgpu(renderer) => { + renderer.measure(content, size, font, bounds) + } + } + } + + fn hit_test( + &self, + content: &str, + size: f32, + font: Font, + bounds: Size, + point: Point, + nearest_only: bool, + ) -> Option { + match self { + #[cfg(feature = "glow")] + Renderer::Glow(renderer) => renderer.hit_test( + content, + size, + font, + bounds, + point, + nearest_only, + ), + #[cfg(feature = "softbuffer")] + Renderer::softbuffer(renderer) => renderer.hit_test( + content, + size, + font, + bounds, + point, + nearest_only, + ), + #[cfg(feature = "wgpu")] + Renderer::Wgpu(renderer) => renderer.hit_test( + content, + size, + font, + bounds, + point, + nearest_only, + ), + } + } + + fn fill_text(&mut self, text: Text<'_, Self::Font>) { + match self { + #[cfg(feature = "glow")] + Renderer::Glow(renderer) => renderer.fill_text(text), + #[cfg(feature = "softbuffer")] + Renderer::softbuffer(renderer) => renderer.fill_text(text), + #[cfg(feature = "wgpu")] + Renderer::Wgpu(renderer) => renderer.fill_text(text), + } + } +} + +#[cfg(feature = "image")] +impl image::Renderer for Renderer { + type Handle = image::Handle; + + fn dimensions(&self, handle: &image::Handle) -> Size { + match self { + #[cfg(feature = "glow")] + Renderer::Glow(renderer) => renderer.dimensions(handle), + #[cfg(feature = "softbuffer")] + Renderer::softbuffer(renderer) => renderer.dimensions(handle), + #[cfg(feature = "wgpu")] + Renderer::Wgpu(renderer) => renderer.dimensions(handle), + } + } + + fn draw(&mut self, handle: image::Handle, bounds: Rectangle) { + match self { + #[cfg(feature = "glow")] + Renderer::Glow(renderer) => renderer.draw(handle, bounds), + #[cfg(feature = "softbuffer")] + Renderer::softbuffer(renderer) => renderer.draw(handle, bounds), + #[cfg(feature = "wgpu")] + Renderer::Wgpu(renderer) => renderer.draw(handle, bounds), + } + } +} + +#[cfg(feature = "svg")] +impl svg::Renderer for Renderer { + fn dimensions(&self, handle: &svg::Handle) -> Size { + match self { + #[cfg(feature = "glow")] + Renderer::Glow(renderer) => renderer.dimensions(handle), + #[cfg(feature = "softbuffer")] + Renderer::softbuffer(renderer) => renderer.dimensions(handle), + #[cfg(feature = "wgpu")] + Renderer::Wgpu(renderer) => renderer.dimensions(handle), + } + } + + fn draw( + &mut self, + handle: svg::Handle, + color: Option, + bounds: Rectangle, + ) { + match self { + #[cfg(feature = "glow")] + Renderer::Glow(renderer) => renderer.draw(handle, color, bounds), + #[cfg(feature = "softbuffer")] + Renderer::softbuffer(renderer) => { + renderer.draw(handle, color, bounds) + } + #[cfg(feature = "wgpu")] + Renderer::Wgpu(renderer) => renderer.draw(handle, color, bounds), + } + } +} diff --git a/dyrend/src/settings.rs b/dyrend/src/settings.rs new file mode 100644 index 0000000000..ac4a09c7e1 --- /dev/null +++ b/dyrend/src/settings.rs @@ -0,0 +1,47 @@ +//! Configure a renderer. +pub use iced_graphics::Antialiasing; + +/// The settings of a [`Backend`]. +/// +/// [`Backend`]: crate::Backend +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Settings { + /// The bytes of the font that will be used by default. + /// + /// If `None` is provided, a default system font will be chosen. + pub default_font: Option<&'static [u8]>, + + /// The default size of text. + /// + /// By default, it will be set to 20. + pub default_text_size: u16, + + /// If enabled, spread text workload in multiple threads when multiple cores + /// are available. + /// + /// By default, it is disabled. + pub text_multithreading: bool, + + /// The antialiasing strategy that will be used for triangle primitives. + /// + /// By default, it is `None`. + pub antialiasing: Option, +} + +impl Settings { + /// Creates new [`Settings`] using environment configuration. + pub fn from_env() -> Self { + Settings::default() + } +} + +impl Default for Settings { + fn default() -> Settings { + Settings { + default_font: None, + default_text_size: 20, + text_multithreading: false, + antialiasing: None, + } + } +} diff --git a/dyrend/src/window.rs b/dyrend/src/window.rs new file mode 100644 index 0000000000..aac5fb9ed8 --- /dev/null +++ b/dyrend/src/window.rs @@ -0,0 +1,4 @@ +//! Display rendering results on windows. +mod compositor; + +pub use compositor::Compositor; diff --git a/dyrend/src/window/compositor.rs b/dyrend/src/window/compositor.rs new file mode 100644 index 0000000000..477ba2105e --- /dev/null +++ b/dyrend/src/window/compositor.rs @@ -0,0 +1,300 @@ +#[cfg(feature = "glow")] +use iced_glow::window::Compositor as GlowCompositor; +#[cfg(feature = "glow")] +use iced_glutin::Compositor as GlutinCompositor; +use iced_graphics::{ + compositor::{self, Compositor as _, Information, SurfaceError}, + Color, Error, Viewport, +}; +#[cfg(feature = "softbuffer")] +use iced_softbuffer::window::Compositor as softbufferCompositor; +#[cfg(feature = "wgpu")] +use iced_wgpu::window::Compositor as WgpuCompositor; +use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; +use std::env; + +use crate::Renderer; + +/// A window graphics backend for iced powered by `glow`. +pub enum Compositor { + #[cfg(feature = "glow")] + Glow(GlutinCompositor>), + #[cfg(feature = "softbuffer")] + softbuffer(softbufferCompositor), + #[cfg(feature = "wgpu")] + Wgpu(WgpuCompositor), +} + +pub enum Surface { + #[cfg(feature = "glow")] + Glow(> as compositor::Compositor>::Surface), + #[cfg(feature = "softbuffer")] + softbuffer( + as compositor::Compositor>::Surface, + ), + #[cfg(feature = "wgpu")] + Wgpu( as compositor::Compositor>::Surface), +} + +impl Compositor { + #[cfg(feature = "glow")] + fn new_glow( + settings: crate::Settings, + compatible_window: Option<&W>, + ) -> Result<(Self, Renderer), Error> { + match GlutinCompositor::new( + iced_glutin::Settings { + gl_settings: iced_glow::Settings { + default_font: settings.default_font, + default_text_size: settings.default_text_size, + text_multithreading: settings.text_multithreading, + antialiasing: settings.antialiasing, + ..iced_glow::Settings::from_env() + }, + try_opengles_first: false, // XXX + }, + compatible_window, + ) { + Ok((compositor, renderer)) => { + Ok((Compositor::Glow(compositor), Renderer::Glow(renderer))) + } + Err(err) => Err(err), + } + } + + #[cfg(feature = "softbuffer")] + fn new_softbuffer( + settings: crate::Settings, + compatible_window: Option<&W>, + ) -> Result<(Self, Renderer), Error> { + match softbufferCompositor::new( + iced_softbuffer::Settings { + default_font: settings.default_font, + default_text_size: settings.default_text_size, + text_multithreading: settings.text_multithreading, + antialiasing: settings.antialiasing, + ..iced_softbuffer::Settings::from_env() + }, + compatible_window, + ) { + Ok((compositor, renderer)) => Ok(( + Compositor::softbuffer(compositor), + Renderer::softbuffer(renderer), + )), + Err(err) => Err(err), + } + } + + #[cfg(feature = "wgpu")] + fn new_wgpu( + settings: crate::Settings, + compatible_window: Option<&W>, + ) -> Result<(Self, Renderer), Error> { + match WgpuCompositor::new( + iced_wgpu::Settings { + default_font: settings.default_font, + default_text_size: settings.default_text_size, + text_multithreading: settings.text_multithreading, + antialiasing: settings.antialiasing, + ..iced_wgpu::Settings::from_env() + }, + compatible_window, + ) { + Ok((compositor, renderer)) => { + Ok((Compositor::Wgpu(compositor), Renderer::Wgpu(renderer))) + } + Err(err) => Err(err), + } + } +} + +/// A graphics compositor that can draw to windows. +impl compositor::Compositor for Compositor { + /// The settings of the backend. + type Settings = crate::Settings; + + /// The iced renderer of the backend. + type Renderer = Renderer; + + /// The surface of the backend. + type Surface = Surface; + + /// Creates a new [`Compositor`]. + fn new( + settings: Self::Settings, + compatible_window: Option<&W>, + ) -> Result<(Self, Self::Renderer), Error> { + //TODO: move to settings! + if let Ok(var) = env::var("ICED_DYREND") { + return match var.as_str() { + #[cfg(feature = "glow")] + "glow" => Self::new_glow(settings, compatible_window), + #[cfg(feature = "softbuffer")] + "softbuffer" => { + Self::new_softbuffer(settings, compatible_window) + } + #[cfg(feature = "wgpu")] + "wgpu" => Self::new_wgpu(settings, compatible_window), + _ => Err(Error::BackendError(format!( + "ICED_DYREND value {:?} not supported", + var + ))), + }; + } + + #[cfg(feature = "wgpu")] + { + eprintln!("trying wgpu compositor"); + match Self::new_wgpu(settings, compatible_window) { + Ok(ok) => { + eprintln!("initialized wgpu compositor"); + return Ok(ok); + } + Err(err) => { + eprintln!( + "failed to initialize wgpu compositor: {:?}", + err + ); + } + } + } + + #[cfg(feature = "glow")] + { + eprintln!("trying glow compositor"); + match Self::new_glow(settings, compatible_window) { + Ok(ok) => { + eprintln!("initialized glow compositor"); + return Ok(ok); + } + Err(err) => { + eprintln!( + "failed to initialize glow compositor: {:?}", + err + ); + } + } + } + + #[cfg(feature = "softbuffer")] + { + eprintln!("trying softbuffer compositor"); + match Self::new_softbuffer(settings, compatible_window) { + Ok(ok) => { + eprintln!("initialized softbuffer compositor"); + return Ok(ok); + } + Err(err) => { + eprintln!( + "failed to initialize softbuffer compositor: {:?}", + err + ); + } + } + } + + Err(Error::GraphicsAdapterNotFound) + } + + /// Crates a new [`Surface`] for the given window. + /// + /// [`Surface`]: Self::Surface + fn create_surface( + &mut self, + window: &W, + ) -> Self::Surface { + match self { + #[cfg(feature = "glow")] + Compositor::Glow(compositor) => { + Surface::Glow(compositor.create_surface(window)) + } + #[cfg(feature = "softbuffer")] + Compositor::softbuffer(compositor) => { + Surface::softbuffer(compositor.create_surface(window)) + } + #[cfg(feature = "wgpu")] + Compositor::Wgpu(compositor) => { + Surface::Wgpu(compositor.create_surface(window)) + } + } + } + + /// Configures a new [`Surface`] with the given dimensions. + /// + /// [`Surface`]: Self::Surface + fn configure_surface( + &mut self, + surface: &mut Self::Surface, + width: u32, + height: u32, + ) { + match (self, surface) { + #[cfg(feature = "glow")] + (Compositor::Glow(compositor), Surface::Glow(surface)) => { + compositor.configure_surface(surface, width, height) + } + #[cfg(feature = "softbuffer")] + ( + Compositor::softbuffer(compositor), + Surface::softbuffer(surface), + ) => compositor.configure_surface(surface, width, height), + #[cfg(feature = "wgpu")] + (Compositor::Wgpu(compositor), Surface::Wgpu(surface)) => { + compositor.configure_surface(surface, width, height) + } + _ => panic!("dyrand configuring incorrect surface"), + } + } + + /// Returns [`Information`] used by this [`Compositor`]. + fn fetch_information(&self) -> Information { + match self { + #[cfg(feature = "glow")] + Compositor::Glow(compositor) => compositor.fetch_information(), + #[cfg(feature = "softbuffer")] + Compositor::softbuffer(compositor) => { + compositor.fetch_information() + } + #[cfg(feature = "wgpu")] + Compositor::Wgpu(compositor) => compositor.fetch_information(), + } + } + + /// Presents the [`Renderer`] primitives to the next frame of the given [`Surface`]. + /// + /// [`Renderer`]: Self::Renderer + /// [`Surface`]: Self::Surface + fn present>( + &mut self, + renderer: &mut Self::Renderer, + surface: &mut Self::Surface, + viewport: &Viewport, + background: Color, + overlay: &[T], + ) -> Result<(), SurfaceError> { + match (self, renderer, surface) { + #[cfg(feature = "glow")] + ( + Compositor::Glow(compositor), + Renderer::Glow(renderer), + Surface::Glow(surface), + ) => compositor + .present(renderer, surface, viewport, background, overlay), + #[cfg(feature = "softbuffer")] + ( + Compositor::softbuffer(compositor), + Renderer::softbuffer(renderer), + Surface::softbuffer(surface), + ) => compositor + .present(renderer, surface, viewport, background, overlay), + #[cfg(feature = "wgpu")] + ( + Compositor::Wgpu(compositor), + Renderer::Wgpu(renderer), + Surface::Wgpu(surface), + ) => compositor + .present(renderer, surface, viewport, background, overlay), + _ => panic!("dyrand presenting incorrect renderer or surface"), + } + } +} diff --git a/examples/autosize_layer/Cargo.toml b/examples/autosize_layer/Cargo.toml new file mode 100644 index 0000000000..bdc3fb07f2 --- /dev/null +++ b/examples/autosize_layer/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "autosize_layer" +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", "softbuffer"] } +sctk = { package = "smithay-client-toolkit", git = "https://github.com/Smithay/client-toolkit", rev = "a257bf7" } +iced_native = { path = "../../native" } +iced_style = { path = "../../style" } + diff --git a/examples/autosize_layer/src/main.rs b/examples/autosize_layer/src/main.rs new file mode 100644 index 0000000000..c5e97febf5 --- /dev/null +++ b/examples/autosize_layer/src/main.rs @@ -0,0 +1,169 @@ +use iced::alignment::{self, Alignment}; +use iced::event::{self, Event}; +use iced::keyboard; +use iced::subscription; +use iced::theme::{self, Theme}; +use iced::wayland::actions::layer_surface::SctkLayerSurfaceSettings; +use iced::wayland::actions::popup::SctkPopupSettings; +use iced::wayland::actions::popup::SctkPositioner; +use iced::wayland::actions::window::SctkWindowSettings; +use iced::wayland::layer_surface::{get_layer_surface, Anchor}; +use iced::wayland::popup::destroy_popup; +use iced::wayland::popup::get_popup; +use iced::wayland::window::get_window; +use iced::wayland::InitialSurface; +use iced::wayland::SurfaceIdWrapper; +use iced::widget::{ + self, button, checkbox, column, container, horizontal_space, row, + scrollable, text, text_input, Column, Row, Text, +}; +use iced::Rectangle; +use iced::{window, Application, Element}; +use iced::{Color, Command, Font, Length, Settings, Subscription}; +use iced_style::application; + +pub fn main() -> iced::Result { + Todos::run(Settings { + initial_surface: InitialSurface::LayerSurface(Default::default()), + ..Default::default() + }) +} + +#[derive(Debug, Default)] +struct Todos { + size: u32, + id_ctr: u32, + popup: Option, +} + +#[derive(Debug, Clone)] +enum Message { + Tick, + Popup, + Ignore, +} + +impl Application for Todos { + type Message = Message; + type Theme = Theme; + type Executor = iced::executor::Default; + type Flags = (); + + fn new(_flags: ()) -> (Todos, Command) { + ( + Todos { + size: 1, + id_ctr: 2, + ..Default::default() + }, + get_layer_surface(SctkLayerSurfaceSettings { + id: window::Id::new(1), + anchor: Anchor::RIGHT, + ..Default::default() + }), + ) + } + + fn update(&mut self, message: Message) -> Command { + match message { + Message::Tick => { + self.size = (self.size - 1) % 3; + if self.size == 0 { + self.size = 3; + } + } + Message::Popup => { + if let Some(p) = self.popup.take() { + return destroy_popup(p); + } else { + self.id_ctr += 1; + let new_id = window::Id::new(self.id_ctr); + self.popup.replace(new_id); + return get_popup(SctkPopupSettings { + parent: window::Id::new(0), + id: new_id, + positioner: SctkPositioner { + anchor_rect: Rectangle { + x: 10, + y: 10, + width: 1, + height: 1, + }, + ..Default::default() + }, + parent_size: None, + grab: true, + }); + } + } + Message::Ignore => {} + } + Command::none() + } + + fn view(&self, id: SurfaceIdWrapper) -> Element { + match id { + SurfaceIdWrapper::LayerSurface(_) | SurfaceIdWrapper::Popup(_) => { + Column::with_children( + (0..self.size) + .map(|_| { + Row::with_children( + (0..self.size) + .map(|i| { + button(horizontal_space(Length::Units( + 20, + ))) + .on_press(Message::Popup) + .width(Length::Units(20)) + .height(Length::Units(20)) + .into() + }) + .collect(), + ) + .spacing(12) + .width(Length::Shrink) + .height(Length::Shrink) + .into() + }) + .collect(), + ) + .spacing(12) + .width(Length::Shrink) + .height(Length::Shrink) + .into() + } + SurfaceIdWrapper::Window(_) => unimplemented!(), + } + } + + fn close_requested(&self, id: SurfaceIdWrapper) -> Self::Message { + Message::Ignore + } + + fn style(&self) -> ::Style { + ::Style::Custom(Box::new( + CustomTheme, + )) + } + fn title(&self) -> String { + String::from("autosize") + } + + fn subscription(&self) -> Subscription { + iced::time::every(std::time::Duration::from_millis(1000)) + .map(|_| Message::Tick) + } +} + +pub struct CustomTheme; + +impl application::StyleSheet for CustomTheme { + type Style = iced::Theme; + + fn appearance(&self, style: &Self::Style) -> application::Appearance { + application::Appearance { + background_color: Color::from_rgba(1.0, 1.0, 1.0, 0.8), + text_color: Color::BLACK, + } + } +} diff --git a/examples/clock_sctk_layer_surface/Cargo.toml b/examples/clock_sctk_layer_surface/Cargo.toml new file mode 100644 index 0000000000..4c47b9d482 --- /dev/null +++ b/examples/clock_sctk_layer_surface/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "clock_sctk_layer_surface" +version = "0.1.0" +authors = ["Ashley Wulber "] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../..", default-features = false, features = ["canvas", "tokio", "debug", "wayland"] } +time = { version = "0.3.5", features = ["local-offset"] } +iced_native = { path = "../../native" } +sctk = { package = "smithay-client-toolkit", git = "https://github.com/Smithay/client-toolkit", rev = "a257bf7" } diff --git a/examples/clock_sctk_layer_surface/README.md b/examples/clock_sctk_layer_surface/README.md new file mode 100644 index 0000000000..175091805b --- /dev/null +++ b/examples/clock_sctk_layer_surface/README.md @@ -0,0 +1,16 @@ +## Clock + +An application that uses the `Canvas` widget to draw a clock and its hands to display the current time. + +The __[`main`]__ file contains all the code of the example. + +
+ +
+ +You can run it with `cargo run`: +``` +cargo run --package clock +``` + +[`main`]: src/main.rs diff --git a/examples/clock_sctk_layer_surface/src/main.rs b/examples/clock_sctk_layer_surface/src/main.rs new file mode 100644 index 0000000000..fe6c914f0f --- /dev/null +++ b/examples/clock_sctk_layer_surface/src/main.rs @@ -0,0 +1,192 @@ +use iced::executor; +use iced::wayland::layer_surface::{get_layer_surface, set_size}; +use iced::wayland::SurfaceIdWrapper; +use iced::widget::canvas::{ + stroke, Cache, Cursor, Geometry, LineCap, Path, Stroke, +}; +use iced::widget::{canvas, container}; +use iced::{ + sctk_settings::InitialSurface, Application, Color, Command, Element, + Length, Point, Rectangle, Settings, Subscription, Theme, Vector, +}; +use iced_native::command::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings; +use iced_native::window::Id; +use sctk::shell::layer::Anchor; + +pub fn main() -> iced::Result { + Clock::run(Settings { + antialiasing: true, + initial_surface: InitialSurface::LayerSurface( + SctkLayerSurfaceSettings { + anchor: Anchor::LEFT.union(Anchor::RIGHT).union(Anchor::TOP), + exclusive_zone: 200, + ..Default::default() + }, + ), + ..Settings::default() + }) +} + +struct Clock { + now: time::OffsetDateTime, + clock: Cache, + count: u32, + to_destroy: Id, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + Tick(time::OffsetDateTime), +} + +impl Application for Clock { + type Message = Message; + type Theme = Theme; + type Executor = executor::Default; + type Flags = (); + + fn new(_flags: ()) -> (Self, Command) { + let to_destroy = Id::new(10); + ( + Clock { + now: time::OffsetDateTime::now_local() + .unwrap_or_else(|_| time::OffsetDateTime::now_utc()), + clock: Default::default(), + count: 0, + to_destroy, + }, + get_layer_surface(SctkLayerSurfaceSettings { + // XXX id must be unique! + id: to_destroy, + size: None, + anchor: Anchor::BOTTOM, + exclusive_zone: 100, + ..Default::default() + }), + ) + } + + fn title(&self) -> String { + String::from("Clock - Iced") + } + + fn update(&mut self, message: Message) -> Command { + match message { + Message::Tick(local_time) => { + // let now = local_time; + + // if now != self.now { + // self.now = now; + // self.clock.clear(); + // } + // // destroy the second layer surface after counting to 10. + // self.count += 1; + // if self.count == 10 { + // println!("time to remove the bottom clock!"); + // return set_size::( + // self.to_destroy, + // None, + // Some(200), + // ); + // } + } + } + + Command::none() + } + + fn subscription(&self) -> Subscription { + iced::time::every(std::time::Duration::from_millis(500)).map(|_| { + Message::Tick( + time::OffsetDateTime::now_local() + .unwrap_or_else(|_| time::OffsetDateTime::now_utc()), + ) + }) + } + + fn view( + &self, + _id: SurfaceIdWrapper, + ) -> Element<'_, Self::Message, iced::Renderer> { + let canvas = canvas(self as &Self).height(Length::Units(200)).width(Length::Units(200)); + + container(canvas) + .padding(20) + .into() + } + + fn close_requested(&self, _id: SurfaceIdWrapper) -> Self::Message { + unimplemented!() + } +} + +impl canvas::Program for Clock { + type State = (); + + fn draw( + &self, + _state: &Self::State, + _theme: &Theme, + bounds: Rectangle, + _cursor: Cursor, + ) -> Vec { + let clock = self.clock.draw(bounds.size(), |frame| { + let center = frame.center(); + let radius = frame.width().min(frame.height()) / 2.0; + + let background = Path::circle(center, radius); + frame.fill(&background, Color::from_rgb8(0x12, 0x93, 0xD8)); + + let short_hand = + Path::line(Point::ORIGIN, Point::new(0.0, -0.5 * radius)); + + let long_hand = + Path::line(Point::ORIGIN, Point::new(0.0, -0.8 * radius)); + + let width = radius / 100.0; + + let thin_stroke = || -> Stroke { + Stroke { + width, + style: stroke::Style::Solid(Color::WHITE), + line_cap: LineCap::Round, + ..Stroke::default() + } + }; + + let wide_stroke = || -> Stroke { + Stroke { + width: width * 3.0, + style: stroke::Style::Solid(Color::WHITE), + line_cap: LineCap::Round, + ..Stroke::default() + } + }; + + frame.translate(Vector::new(center.x, center.y)); + + frame.with_save(|frame| { + frame.rotate(hand_rotation(self.now.hour(), 12)); + frame.stroke(&short_hand, wide_stroke()); + }); + + frame.with_save(|frame| { + frame.rotate(hand_rotation(self.now.minute(), 60)); + frame.stroke(&long_hand, wide_stroke()); + }); + + frame.with_save(|frame| { + frame.rotate(hand_rotation(self.now.second(), 60)); + frame.stroke(&long_hand, thin_stroke()); + }) + }); + + vec![clock] + } +} + +fn hand_rotation(n: u8, total: u8) -> f32 { + let turns = n as f32 / total as f32; + + 2.0 * std::f32::consts::PI * turns +} diff --git a/examples/clock_sctk_window/Cargo.toml b/examples/clock_sctk_window/Cargo.toml new file mode 100644 index 0000000000..cfe40dfacd --- /dev/null +++ b/examples/clock_sctk_window/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "clock_sctk_window" +version = "0.1.0" +authors = ["Ashley Wulber "] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../..", default-features = false, features = ["canvas", "tokio", "debug", "wayland", "glow"] } +time = { version = "0.3.5", features = ["local-offset"] } +iced_native = { path = "../../native" } +sctk = { package = "smithay-client-toolkit", git = "https://github.com/Smithay/client-toolkit", rev = "a257bf7" } diff --git a/examples/clock_sctk_window/README.md b/examples/clock_sctk_window/README.md new file mode 100644 index 0000000000..175091805b --- /dev/null +++ b/examples/clock_sctk_window/README.md @@ -0,0 +1,16 @@ +## Clock + +An application that uses the `Canvas` widget to draw a clock and its hands to display the current time. + +The __[`main`]__ file contains all the code of the example. + +
+ +
+ +You can run it with `cargo run`: +``` +cargo run --package clock +``` + +[`main`]: src/main.rs diff --git a/examples/clock_sctk_window/src/main.rs b/examples/clock_sctk_window/src/main.rs new file mode 100644 index 0000000000..553df8bbbb --- /dev/null +++ b/examples/clock_sctk_window/src/main.rs @@ -0,0 +1,265 @@ +use iced::executor; +use iced::wayland::actions::layer_surface::SctkLayerSurfaceSettings; +use iced::wayland::layer_surface::KeyboardInteractivity; +use iced::wayland::{ + popup::{destroy_popup, get_popup}, + window::{close_window, get_window}, + SurfaceIdWrapper, +}; +use iced::widget::canvas::{ + stroke, Cache, Cursor, Geometry, LineCap, Path, Stroke, +}; +use iced::widget::{button, canvas, column, container, text, text_input}; +use iced::{ + wayland::InitialSurface, Application, Color, Command, Element, Length, + Point, Rectangle, Settings, Subscription, Theme, Vector, +}; +use iced_native::command::platform_specific::wayland::popup::{ + SctkPopupSettings, SctkPositioner, +}; +use iced_native::command::platform_specific::wayland::window::SctkWindowSettings; +use iced_native::window::{self, Id}; +use iced_native::Widget; + +pub fn main() -> iced::Result { + Clock::run(Settings { + antialiasing: true, + initial_surface: InitialSurface::XdgWindow(SctkWindowSettings { + autosize: true, + ..Default::default() + }), + ..Settings::default() + }) +} + +struct Clock { + now: time::OffsetDateTime, + clock: Cache, + count: u32, + to_destroy: Id, + id_ctr: u32, + popup: Option, + input: String, +} + +#[derive(Debug, Clone)] +enum Message { + Tick(time::OffsetDateTime), + Click(window::Id), + SurfaceClosed, + Input(String), +} + +impl Application for Clock { + type Message = Message; + type Theme = Theme; + type Executor = executor::Default; + type Flags = (); + + fn new(_flags: ()) -> (Self, Command) { + let to_destroy = Id::new(1); + ( + Clock { + now: time::OffsetDateTime::now_local() + .unwrap_or_else(|_| time::OffsetDateTime::now_utc()), + clock: Default::default(), + count: 0, + popup: None, + to_destroy, + id_ctr: 2, + input: String::new(), + }, + get_window(SctkWindowSettings { + window_id: to_destroy, + ..Default::default() + }), + ) + } + + fn title(&self) -> String { + String::from("Clock - Iced") + } + + fn update(&mut self, message: Message) -> Command { + match message { + Message::Tick(local_time) => { + let now = local_time; + + if now != self.now { + self.now = now; + self.clock.clear(); + } + // destroy the second window after counting to 10. + self.count += 1; + if self.count == 10 { + println!("time to remove the bottom clock!"); + return close_window::(self.to_destroy); + // return close_window(self.to_destroy); + } + } + Message::Click(parent_id) => { + if let Some(p) = self.popup.take() { + return destroy_popup(p); + } else { + self.id_ctr += 1; + let new_id = window::Id::new(self.id_ctr); + self.popup.replace(new_id); + return get_popup(SctkPopupSettings { + parent: parent_id, + id: new_id, + positioner: SctkPositioner { + anchor_rect: Rectangle { + x: 100, + y: 100, + width: 160, + height: 260, + }, + // size: Some((100, 200)), + ..Default::default() + }, + parent_size: None, + grab: true, + }); + } + } + Message::SurfaceClosed => { + // ignored + } + Message::Input(input) => { + self.input = input; + } + } + + Command::none() + } + + fn subscription(&self) -> Subscription { + iced::time::every(std::time::Duration::from_millis(2000)).map(|_| { + Message::Tick( + time::OffsetDateTime::now_local() + .unwrap_or_else(|_| time::OffsetDateTime::now_utc()), + ) + }) + } + + fn view( + &self, + id: SurfaceIdWrapper, + ) -> Element<'_, Self::Message, iced::Renderer> { + match id { + SurfaceIdWrapper::LayerSurface(_) => { + let canvas = canvas(self as &Self) + .width(Length::Units(100)) + .height(Length::Units(100)); + + container(column![ + text_input("hello", &self.input, Message::Input) + .width(Length::Fill), + button("Popup").on_press(Message::Click(id.inner())), + canvas, + ]) + .padding(20) + .into() + } + SurfaceIdWrapper::Window(_) => { + let canvas = canvas(self as &Self) + .width(Length::Units(100)) + .height(Length::Units(100)); + + container(column![ + text_input("hello", &self.input, Message::Input) + .width(Length::Fill), + button("Popup").on_press(Message::Click(id.inner())), + canvas, + ]) + .padding(20) + .into() + } + SurfaceIdWrapper::Popup(_) => { + let mut s = String::with_capacity(self.count as usize); + for i in 0..self.count { + s.push('X'); + } + button(text(format!("{}", s))) + .on_press(Message::Click(id.inner())) + .padding(20) + .into() + } + } + } + + fn close_requested(&self, id: SurfaceIdWrapper) -> Self::Message { + Message::SurfaceClosed + } +} + +impl canvas::Program for Clock { + type State = (); + + fn draw( + &self, + _state: &Self::State, + _theme: &Theme, + bounds: Rectangle, + _cursor: Cursor, + ) -> Vec { + let clock = self.clock.draw(bounds.size(), |frame| { + let center = frame.center(); + let radius = frame.width().min(frame.height()) / 2.0; + + let background = Path::circle(center, radius); + frame.fill(&background, Color::from_rgb8(0x12, 0x93, 0xD8)); + + let short_hand = + Path::line(Point::ORIGIN, Point::new(0.0, -0.5 * radius)); + + let long_hand = + Path::line(Point::ORIGIN, Point::new(0.0, -0.8 * radius)); + + let width = radius / 100.0; + + let thin_stroke = || -> Stroke { + Stroke { + width, + style: stroke::Style::Solid(Color::WHITE), + line_cap: LineCap::Round, + ..Stroke::default() + } + }; + + let wide_stroke = || -> Stroke { + Stroke { + width: width * 3.0, + style: stroke::Style::Solid(Color::WHITE), + line_cap: LineCap::Round, + ..Stroke::default() + } + }; + + frame.translate(Vector::new(center.x, center.y)); + + frame.with_save(|frame| { + frame.rotate(hand_rotation(self.now.hour(), 12)); + frame.stroke(&short_hand, wide_stroke()); + }); + + frame.with_save(|frame| { + frame.rotate(hand_rotation(self.now.minute(), 60)); + frame.stroke(&long_hand, wide_stroke()); + }); + + frame.with_save(|frame| { + frame.rotate(hand_rotation(self.now.second(), 60)); + frame.stroke(&long_hand, thin_stroke()); + }) + }); + + vec![clock] + } +} + +fn hand_rotation(n: u8, total: u8) -> f32 { + let turns = n as f32 / total as f32; + + 2.0 * std::f32::consts::PI * turns +} diff --git a/examples/styling/Cargo.toml b/examples/styling/Cargo.toml index f771708c8e..3524f0d862 100644 --- a/examples/styling/Cargo.toml +++ b/examples/styling/Cargo.toml @@ -6,4 +6,4 @@ edition = "2021" publish = false [dependencies] -iced = { path = "../.." } +iced = { path = "../..", features = ["debug"] } diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index e16860adc9..fdbf614c49 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -75,7 +75,7 @@ impl Sandbox for Styling { [ThemeType::Light, ThemeType::Dark, ThemeType::Custom] .iter() .fold( - column![text("Choose a theme:")].spacing(10), + column![text("Choose a theme: 😊")].spacing(10), |column, theme| { column.push(radio( format!("{:?}", theme), @@ -150,12 +150,17 @@ impl Sandbox for Styling { .padding(20) .max_width(600); - container(content) + let element: Element<_> = container(content) .width(Length::Fill) .height(Length::Fill) .center_x() .center_y() - .into() + .into(); + if self.toggler_value { + element.explain(Color::BLACK) + } else { + element + } } fn theme(&self) -> Theme { diff --git a/examples/todos/Cargo.toml b/examples/todos/Cargo.toml index 7ad4d558bd..c0beb0693d 100644 --- a/examples/todos/Cargo.toml +++ b/examples/todos/Cargo.toml @@ -7,6 +7,7 @@ publish = false [dependencies] iced = { path = "../..", features = ["async-std", "debug"] } +iced_style = { path = "../../style" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" once_cell = "1.15" diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 690d9c0979..0a5cd56ea0 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -1,6 +1,5 @@ use iced::alignment::{self, Alignment}; use iced::event::{self, Event}; -use iced::keyboard; use iced::subscription; use iced::theme::{self, Theme}; use iced::widget::{ @@ -8,6 +7,7 @@ use iced::widget::{ text_input, Text, }; use iced::window; +use iced::{application, keyboard}; use iced::{Application, Element}; use iced::{Color, Command, Font, Length, Settings, Subscription}; @@ -378,6 +378,26 @@ impl Task { } } } + + fn style(&self) -> ::Style { + ::Style::Custom(Box::new( + CustomTheme, + )) + } +} + +pub struct CustomTheme; + +impl application::StyleSheet for CustomTheme { + type Style = iced::Theme; + + fn appearance(&self, style: &Self::Style) -> application::Appearance { + dbg!(Color::TRANSPARENT); + application::Appearance { + background_color: Color::TRANSPARENT, + text_color: Color::BLACK, + } + } } fn view_controls(tasks: &[Task], current_filter: Filter) -> Element { diff --git a/examples/todos_sctk/Cargo.toml b/examples/todos_sctk/Cargo.toml new file mode 100644 index 0000000000..ea257c40c1 --- /dev/null +++ b/examples/todos_sctk/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "todos_sctk" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../..", default-features=false, features = ["async-std", "wayland", "debug", "image", "dyrend"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +once_cell = "1.15" +sctk = { package = "smithay-client-toolkit", git = "https://github.com/Smithay/client-toolkit", rev = "a257bf7" } +iced_native = { path = "../../native" } +iced_style = { path = "../../style" } + +[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"], +] diff --git a/examples/todos_sctk/README.md b/examples/todos_sctk/README.md new file mode 100644 index 0000000000..9c2598b95e --- /dev/null +++ b/examples/todos_sctk/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/todos_sctk/fonts/icons.ttf b/examples/todos_sctk/fonts/icons.ttf new file mode 100644 index 0000000000..4498299db2 Binary files /dev/null and b/examples/todos_sctk/fonts/icons.ttf differ diff --git a/examples/todos_sctk/iced-todos.desktop b/examples/todos_sctk/iced-todos.desktop new file mode 100644 index 0000000000..dd7ce53dad --- /dev/null +++ b/examples/todos_sctk/iced-todos.desktop @@ -0,0 +1,4 @@ +[Desktop Entry] +Name=Todos - Iced +Exec=iced-todos +Type=Application diff --git a/examples/todos_sctk/index.html b/examples/todos_sctk/index.html new file mode 100644 index 0000000000..ee5570fb9e --- /dev/null +++ b/examples/todos_sctk/index.html @@ -0,0 +1,12 @@ + + + + + + Todos - Iced + + + + + + diff --git a/examples/todos_sctk/src/main.rs b/examples/todos_sctk/src/main.rs new file mode 100644 index 0000000000..5bc8652a20 --- /dev/null +++ b/examples/todos_sctk/src/main.rs @@ -0,0 +1,657 @@ +use iced::alignment::{self, Alignment}; +use iced::event::{self, Event}; +use iced::keyboard; +use iced::subscription; +use iced::theme::{self, Theme}; +use iced::wayland::actions::popup::SctkPopupSettings; +use iced::wayland::actions::window::SctkWindowSettings; +use iced::wayland::popup::get_popup; +use iced::wayland::window::get_window; +use iced::wayland::InitialSurface; +use iced::wayland::SurfaceIdWrapper; +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_style::application; + +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; + +static INPUT_ID: Lazy = Lazy::new(text_input::Id::unique); + +pub fn main() -> iced::Result { + Todos::run(Settings { + initial_surface: InitialSurface::XdgWindow(Default::default()), + ..Settings::default() + }) +} + +#[derive(Debug)] +enum Todos { + Loading, + Loaded(State), +} + +#[derive(Debug, Default)] +struct State { + input_value: String, + filter: Filter, + tasks: Vec, + dirty: bool, + saving: bool, +} + +#[derive(Debug, Clone)] +enum Message { + Loaded(Result), + Saved(Result<(), SaveError>), + InputChanged(String), + CreateTask, + FilterChanged(Filter), + TaskMessage(usize, TaskMessage), + TabPressed { shift: bool }, + CloseRequested(SurfaceIdWrapper), +} + +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), + get_window(SctkWindowSettings { + window_id: window::Id::new(1), + ..Default::default() + }), + ]), + ) + } + + fn title(&self) -> 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, + ..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(); + } + + return get_popup(SctkPopupSettings { + parent: window::Id::new(0), + id: window::Id::new(2), + positioner: Default::default(), + parent_size: None, + grab: true, + }); + } + 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(s) => { + dbg!(s); + 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: SurfaceIdWrapper) -> Element { + match id { + SurfaceIdWrapper::LayerSurface(_) => todo!(), + SurfaceIdWrapper::Window(_) => match self { + Todos::Loading => loading_message(), + Todos::Loaded(State { + input_value, + filter, + tasks, + .. + }) => { + 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, + Message::InputChanged, + ) + .id(INPUT_ID.clone()) + .padding(15) + .size(30) + .on_submit(Message::CreateTask); + + 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() + } + }, + SurfaceIdWrapper::Popup(_) => container(text("hello")) + .width(Length::Fill) + .height(Length::Fill) + .into(), + } + } + + fn subscription(&self) -> Subscription { + subscription::events_with(|event, status| match (event, status) { + ( + Event::Keyboard(keyboard::Event::KeyPressed { + key_code: keyboard::KeyCode::Tab, + modifiers, + .. + }), + event::Status::Ignored, + ) => Some(Message::TabPressed { + shift: modifiers.shift(), + }), + _ => None, + }) + } + + fn close_requested(&self, id: SurfaceIdWrapper) -> Self::Message { + Message::CloseRequested(id) + } + + fn style(&self) -> ::Style { + ::Style::Custom(Box::new( + CustomTheme, + )) + } +} + +pub struct CustomTheme; + +impl application::StyleSheet for CustomTheme { + type Style = iced::Theme; + + fn appearance(&self, style: &Self::Style) -> application::Appearance { + application::Appearance { + background_color: Color::TRANSPARENT, + text_color: Color::BLACK, + } + } +} + +#[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) -> text_input::Id { + text_input::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, + TaskMessage::DescriptionEdited, + ) + .id(Self::text_input_id(i)) + .on_submit(TaskMessage::FinishEdition) + .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::Units(200)) + .center_y() + .into() +} + +// Fonts +const ICONS: Font = Font::External { + name: "Icons", + bytes: include_bytes!("../../todos/fonts/icons.ttf"), +}; + +fn icon(unicode: char) -> Text<'static> { + text(unicode.to_string()) + .font(ICONS) + .width(Length::Units(20)) + .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/tour_sctk/Cargo.toml b/examples/tour_sctk/Cargo.toml new file mode 100644 index 0000000000..fd88fb6707 --- /dev/null +++ b/examples/tour_sctk/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "tour_sctk" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../..", default-features = false, features = ["wayland", "debug", "image", "dyrend"] } +env_logger = "0.8" diff --git a/examples/tour_sctk/README.md b/examples/tour_sctk/README.md new file mode 100644 index 0000000000..731e7e66f0 --- /dev/null +++ b/examples/tour_sctk/README.md @@ -0,0 +1,33 @@ +## Tour + +A simple UI tour that can run both on native platforms and the web! It showcases different widgets that can be built using Iced. + +The __[`main`]__ file contains all the code of the example! All the cross-platform GUI is defined in terms of __state__, __messages__, __update logic__ and __view logic__. + + + +[`main`]: src/main.rs +[`iced_winit`]: ../../winit +[`iced_native`]: ../../native +[`iced_wgpu`]: ../../wgpu +[`iced_web`]: https://github.com/iced-rs/iced_web +[`winit`]: https://github.com/rust-windowing/winit +[`wgpu`]: https://github.com/gfx-rs/wgpu-rs + +You can run the native version with `cargo run`: +``` +cargo run --package tour +``` + +The web version can be run with [`trunk`]: + +``` +cd examples/tour +trunk serve +``` + +[`trunk`]: https://trunkrs.dev/ diff --git a/examples/tour_sctk/images/ferris.png b/examples/tour_sctk/images/ferris.png new file mode 100644 index 0000000000..9e88383482 Binary files /dev/null and b/examples/tour_sctk/images/ferris.png differ diff --git a/examples/tour_sctk/index.html b/examples/tour_sctk/index.html new file mode 100644 index 0000000000..c64af912c5 --- /dev/null +++ b/examples/tour_sctk/index.html @@ -0,0 +1,12 @@ + + + + + + Tour - Iced + + + + + + diff --git a/examples/tour_sctk/src/main.rs b/examples/tour_sctk/src/main.rs new file mode 100644 index 0000000000..592a0a277b --- /dev/null +++ b/examples/tour_sctk/src/main.rs @@ -0,0 +1,681 @@ +use iced::alignment; +use iced::theme; +use iced::wayland::InitialSurface; +use iced::wayland::SurfaceIdWrapper; +use iced::widget::{ + checkbox, column, container, horizontal_space, image, radio, row, + scrollable, slider, text, text_input, toggler, vertical_space, +}; +use iced::widget::{Button, Column, Container, Slider}; +use iced::{Color, Element, Length, Renderer, Sandbox, Settings}; + +pub fn main() -> iced::Result { + env_logger::init(); + + Tour::run(Settings { + initial_surface: InitialSurface::XdgWindow(Default::default()), + ..Settings::default() + }) +} + +pub struct Tour { + steps: Steps, + debug: bool, +} + +impl Sandbox for Tour { + type Message = Message; + + fn new() -> Tour { + Tour { + steps: Steps::new(), + debug: false, + } + } + + fn title(&self) -> String { + format!("{} - Iced", self.steps.title()) + } + + fn update(&mut self, event: Message) { + match event { + Message::BackPressed => { + self.steps.go_back(); + } + Message::NextPressed => { + self.steps.advance(); + } + Message::StepMessage(step_msg) => { + self.steps.update(step_msg, &mut self.debug); + } + } + } + + fn view(&self, _: SurfaceIdWrapper) -> Element { + let Tour { steps, .. } = self; + + let mut controls = row![]; + + if steps.has_previous() { + controls = controls.push( + button("Back") + .on_press(Message::BackPressed) + .style(theme::Button::Secondary), + ); + } + + controls = controls.push(horizontal_space(Length::Fill)); + + if steps.can_continue() { + controls = controls.push( + button("Next") + .on_press(Message::NextPressed) + .style(theme::Button::Primary), + ); + } + + let content: Element<_> = column![ + steps.view(self.debug).map(Message::StepMessage), + controls, + ] + .max_width(540) + .spacing(20) + .padding(20) + .into(); + + let scrollable = scrollable( + container(if self.debug { + content.explain(Color::BLACK) + } else { + content + }) + .width(Length::Fill) + .center_x(), + ); + + container(scrollable).height(Length::Fill).center_y().into() + } + + fn close_requested(&self, _: SurfaceIdWrapper) -> Message { + todo!() + } +} + +#[derive(Debug, Clone)] +pub enum Message { + BackPressed, + NextPressed, + StepMessage(StepMessage), +} + +struct Steps { + steps: Vec, + current: usize, +} + +impl Steps { + fn new() -> Steps { + Steps { + steps: vec![ + Step::Welcome, + Step::Slider { value: 50 }, + Step::RowsAndColumns { + layout: Layout::Row, + spacing: 20, + }, + Step::Text { + size: 30, + color: Color::BLACK, + }, + Step::Radio { selection: None }, + Step::Toggler { + can_continue: false, + }, + Step::Image { width: 300 }, + Step::Scrollable, + Step::TextInput { + value: String::new(), + is_secure: false, + }, + Step::Debugger, + Step::End, + ], + current: 0, + } + } + + fn update(&mut self, msg: StepMessage, debug: &mut bool) { + self.steps[self.current].update(msg, debug); + } + + fn view(&self, debug: bool) -> Element { + self.steps[self.current].view(debug) + } + + fn advance(&mut self) { + if self.can_continue() { + self.current += 1; + } + } + + fn go_back(&mut self) { + if self.has_previous() { + self.current -= 1; + } + } + + fn has_previous(&self) -> bool { + self.current > 0 + } + + fn can_continue(&self) -> bool { + self.current + 1 < self.steps.len() + && self.steps[self.current].can_continue() + } + + fn title(&self) -> &str { + self.steps[self.current].title() + } +} + +enum Step { + Welcome, + Slider { value: u8 }, + RowsAndColumns { layout: Layout, spacing: u16 }, + Text { size: u16, color: Color }, + Radio { selection: Option }, + Toggler { can_continue: bool }, + Image { width: u16 }, + Scrollable, + TextInput { value: String, is_secure: bool }, + Debugger, + End, +} + +#[derive(Debug, Clone)] +pub enum StepMessage { + SliderChanged(u8), + LayoutChanged(Layout), + SpacingChanged(u16), + TextSizeChanged(u16), + TextColorChanged(Color), + LanguageSelected(Language), + ImageWidthChanged(u16), + InputChanged(String), + ToggleSecureInput(bool), + DebugToggled(bool), + TogglerChanged(bool), +} + +impl<'a> Step { + fn update(&mut self, msg: StepMessage, debug: &mut bool) { + match msg { + StepMessage::DebugToggled(value) => { + if let Step::Debugger = self { + *debug = value; + } + } + StepMessage::LanguageSelected(language) => { + if let Step::Radio { selection } = self { + *selection = Some(language); + } + } + StepMessage::SliderChanged(new_value) => { + if let Step::Slider { value, .. } = self { + *value = new_value; + } + } + StepMessage::TextSizeChanged(new_size) => { + if let Step::Text { size, .. } = self { + *size = new_size; + } + } + StepMessage::TextColorChanged(new_color) => { + if let Step::Text { color, .. } = self { + *color = new_color; + } + } + StepMessage::LayoutChanged(new_layout) => { + if let Step::RowsAndColumns { layout, .. } = self { + *layout = new_layout; + } + } + StepMessage::SpacingChanged(new_spacing) => { + if let Step::RowsAndColumns { spacing, .. } = self { + *spacing = new_spacing; + } + } + StepMessage::ImageWidthChanged(new_width) => { + if let Step::Image { width, .. } = self { + *width = new_width; + } + } + StepMessage::InputChanged(new_value) => { + if let Step::TextInput { value, .. } = self { + *value = new_value; + } + } + StepMessage::ToggleSecureInput(toggle) => { + if let Step::TextInput { is_secure, .. } = self { + *is_secure = toggle; + } + } + StepMessage::TogglerChanged(value) => { + if let Step::Toggler { can_continue, .. } = self { + *can_continue = value; + } + } + }; + } + + fn title(&self) -> &str { + match self { + Step::Welcome => "Welcome", + Step::Radio { .. } => "Radio button", + Step::Toggler { .. } => "Toggler", + Step::Slider { .. } => "Slider", + Step::Text { .. } => "Text", + Step::Image { .. } => "Image", + Step::RowsAndColumns { .. } => "Rows and columns", + Step::Scrollable => "Scrollable", + Step::TextInput { .. } => "Text input", + Step::Debugger => "Debugger", + Step::End => "End", + } + } + + fn can_continue(&self) -> bool { + match self { + Step::Welcome => true, + Step::Radio { selection } => *selection == Some(Language::Rust), + Step::Toggler { can_continue } => *can_continue, + Step::Slider { .. } => true, + Step::Text { .. } => true, + Step::Image { .. } => true, + Step::RowsAndColumns { .. } => true, + Step::Scrollable => true, + Step::TextInput { value, .. } => !value.is_empty(), + Step::Debugger => true, + Step::End => false, + } + } + + fn view(&self, debug: bool) -> Element { + match self { + Step::Welcome => Self::welcome(), + Step::Radio { selection } => Self::radio(*selection), + Step::Toggler { can_continue } => Self::toggler(*can_continue), + Step::Slider { value } => Self::slider(*value), + Step::Text { size, color } => Self::text(*size, *color), + Step::Image { width } => Self::image(*width), + Step::RowsAndColumns { layout, spacing } => { + Self::rows_and_columns(*layout, *spacing) + } + Step::Scrollable => Self::scrollable(), + Step::TextInput { value, is_secure } => { + Self::text_input(value, *is_secure) + } + Step::Debugger => Self::debugger(debug), + Step::End => Self::end(), + } + .into() + } + + fn container(title: &str) -> Column<'a, StepMessage> { + column![text(title).size(50)].spacing(20) + } + + fn welcome() -> Column<'a, StepMessage> { + Self::container("Welcome!") + .push( + "This is a simple tour meant to showcase a bunch of widgets \ + that can be easily implemented on top of Iced.", + ) + .push( + "Iced is a cross-platform GUI library for Rust focused on \ + simplicity and type-safety. It is heavily inspired by Elm.", + ) + .push( + "It was originally born as part of Coffee, an opinionated \ + 2D game engine for Rust.", + ) + .push( + "On native platforms, Iced provides by default a renderer \ + built on top of wgpu, a graphics library supporting Vulkan, \ + Metal, DX11, and DX12.", + ) + .push( + "Additionally, this tour can also run on WebAssembly thanks \ + to dodrio, an experimental VDOM library for Rust.", + ) + .push( + "You will need to interact with the UI in order to reach the \ + end!", + ) + } + + fn slider(value: u8) -> Column<'a, StepMessage> { + Self::container("Slider") + .push( + "A slider allows you to smoothly select a value from a range \ + of values.", + ) + .push( + "The following slider lets you choose an integer from \ + 0 to 100:", + ) + .push(slider(0..=100, value, StepMessage::SliderChanged)) + .push( + text(value.to_string()) + .width(Length::Fill) + .horizontal_alignment(alignment::Horizontal::Center), + ) + } + + fn rows_and_columns( + layout: Layout, + spacing: u16, + ) -> Column<'a, StepMessage> { + let row_radio = + radio("Row", Layout::Row, Some(layout), StepMessage::LayoutChanged); + + let column_radio = radio( + "Column", + Layout::Column, + Some(layout), + StepMessage::LayoutChanged, + ); + + let layout_section: Element<_> = match layout { + Layout::Row => { + row![row_radio, column_radio].spacing(spacing).into() + } + Layout::Column => { + column![row_radio, column_radio].spacing(spacing).into() + } + }; + + let spacing_section = column![ + slider(0..=80, spacing, StepMessage::SpacingChanged), + text(format!("{} px", spacing)) + .width(Length::Fill) + .horizontal_alignment(alignment::Horizontal::Center), + ] + .spacing(10); + + Self::container("Rows and columns") + .spacing(spacing) + .push( + "Iced uses a layout model based on flexbox to position UI \ + elements.", + ) + .push( + "Rows and columns can be used to distribute content \ + horizontally or vertically, respectively.", + ) + .push(layout_section) + .push("You can also easily change the spacing between elements:") + .push(spacing_section) + } + + fn text(size: u16, color: Color) -> Column<'a, StepMessage> { + let size_section = column![ + "You can change its size:", + text(format!("This text is {} pixels", size)).size(size), + slider(10..=70, size, StepMessage::TextSizeChanged), + ] + .padding(20) + .spacing(20); + + let color_sliders = row![ + color_slider(color.r, move |r| Color { r, ..color }), + color_slider(color.g, move |g| Color { g, ..color }), + color_slider(color.b, move |b| Color { b, ..color }), + ] + .spacing(10); + + let color_section = column![ + "And its color:", + text(format!("{:?}", color)).style(color), + color_sliders, + ] + .padding(20) + .spacing(20); + + Self::container("Text") + .push( + "Text is probably the most essential widget for your UI. \ + It will try to adapt to the dimensions of its container.", + ) + .push(size_section) + .push(color_section) + } + + fn radio(selection: Option) -> Column<'a, StepMessage> { + let question = column![ + text("Iced is written in...").size(24), + column( + Language::all() + .iter() + .cloned() + .map(|language| { + radio( + language, + language, + selection, + StepMessage::LanguageSelected, + ) + }) + .map(Element::from) + .collect() + ) + .spacing(10) + ] + .padding(20) + .spacing(10); + + Self::container("Radio button") + .push( + "A radio button is normally used to represent a choice... \ + Surprise test!", + ) + .push(question) + .push( + "Iced works very well with iterators! The list above is \ + basically created by folding a column over the different \ + choices, creating a radio button for each one of them!", + ) + } + + fn toggler(can_continue: bool) -> Column<'a, StepMessage> { + Self::container("Toggler") + .push("A toggler is mostly used to enable or disable something.") + .push( + Container::new(toggler( + "Toggle me to continue...".to_owned(), + can_continue, + StepMessage::TogglerChanged, + )) + .padding([0, 40]), + ) + } + + fn image(width: u16) -> Column<'a, StepMessage> { + Self::container("Image") + .push("An image that tries to keep its aspect ratio.") + .push(ferris(width)) + .push(slider(100..=500, width, StepMessage::ImageWidthChanged)) + .push( + text(format!("Width: {} px", width)) + .width(Length::Fill) + .horizontal_alignment(alignment::Horizontal::Center), + ) + } + + fn scrollable() -> Column<'a, StepMessage> { + Self::container("Scrollable") + .push( + "Iced supports scrollable content. Try it out! Find the \ + button further below.", + ) + .push( + text("Tip: You can use the scrollbar to scroll down faster!") + .size(16), + ) + .push(vertical_space(Length::Units(4096))) + .push( + text("You are halfway there!") + .width(Length::Fill) + .size(30) + .horizontal_alignment(alignment::Horizontal::Center), + ) + .push(vertical_space(Length::Units(4096))) + .push(ferris(300)) + .push( + text("You made it!") + .width(Length::Fill) + .size(50) + .horizontal_alignment(alignment::Horizontal::Center), + ) + } + + fn text_input(value: &str, is_secure: bool) -> Column<'a, StepMessage> { + let text_input = text_input( + "Type something to continue...", + value, + StepMessage::InputChanged, + ) + .padding(10) + .size(30); + + Self::container("Text input") + .push("Use a text input to ask for different kinds of information.") + .push(if is_secure { + text_input.password() + } else { + text_input + }) + .push(checkbox( + "Enable password mode", + is_secure, + StepMessage::ToggleSecureInput, + )) + .push( + "A text input produces a message every time it changes. It is \ + very easy to keep track of its contents:", + ) + .push( + text(if value.is_empty() { + "You have not typed anything yet..." + } else { + value + }) + .width(Length::Fill) + .horizontal_alignment(alignment::Horizontal::Center), + ) + } + + fn debugger(debug: bool) -> Column<'a, StepMessage> { + Self::container("Debugger") + .push( + "You can ask Iced to visually explain the layouting of the \ + different elements comprising your UI!", + ) + .push( + "Give it a shot! Check the following checkbox to be able to \ + see element boundaries.", + ) + .push(if cfg!(target_arch = "wasm32") { + Element::new( + text("Not available on web yet!") + .style(Color::from([0.7, 0.7, 0.7])) + .horizontal_alignment(alignment::Horizontal::Center), + ) + } else { + checkbox("Explain layout", debug, StepMessage::DebugToggled) + .into() + }) + .push("Feel free to go back and take a look.") + } + + fn end() -> Column<'a, StepMessage> { + Self::container("You reached the end!") + .push("This tour will be updated as more features are added.") + .push("Make sure to keep an eye on it!") + } +} + +fn ferris<'a>(width: u16) -> Container<'a, StepMessage> { + container( + // This should go away once we unify resource loading on native + // platforms + if cfg!(target_arch = "wasm32") { + image("tour/images/ferris.png") + } else { + image(format!("{}/images/ferris.png", env!("CARGO_MANIFEST_DIR"))) + } + .width(Length::Units(width)), + ) + .width(Length::Fill) + .center_x() +} + +fn button<'a, Message: Clone>(label: &str) -> Button<'a, Message> { + iced::widget::button( + text(label).horizontal_alignment(alignment::Horizontal::Center), + ) + .padding(12) + .width(Length::Units(100)) +} + +fn color_slider<'a>( + component: f32, + update: impl Fn(f32) -> Color + 'a, +) -> Slider<'a, f64, StepMessage, Renderer> { + slider(0.0..=1.0, f64::from(component), move |c| { + StepMessage::TextColorChanged(update(c as f32)) + }) + .step(0.01) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Language { + Rust, + Elm, + Ruby, + Haskell, + C, + Other, +} + +impl Language { + fn all() -> [Language; 6] { + [ + Language::C, + Language::Elm, + Language::Ruby, + Language::Haskell, + Language::Rust, + Language::Other, + ] + } +} + +impl From for String { + fn from(language: Language) -> String { + String::from(match language { + Language::Rust => "Rust", + Language::Elm => "Elm", + Language::Ruby => "Ruby", + Language::Haskell => "Haskell", + Language::C => "C", + Language::Other => "Other", + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Layout { + Row, + Column, +} diff --git a/glutin/Cargo.toml b/glutin/Cargo.toml index 022457b186..e80890f36d 100644 --- a/glutin/Cargo.toml +++ b/glutin/Cargo.toml @@ -11,16 +11,15 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"] categories = ["gui"] [features] +#trace = ["iced_winit/trace"] +trace = [] debug = ["iced_winit/debug"] system = ["iced_winit/system"] -[dependencies.log] -version = "0.4" - -[dependencies.glutin] -version = "0.29" -git = "https://github.com/iced-rs/glutin" -rev = "da8d291486b4c9bec12487a46c119c4b1d386abf" +[dependencies] +glutin = "0.30.3" +log = "0.4" +raw-window-handle = "0.5.0" [dependencies.iced_native] version = "0.7" @@ -35,3 +34,7 @@ features = ["application"] version = "0.5" path = "../graphics" features = ["opengl"] + +[dependencies.tracing] +version = "0.1.6" +optional = true diff --git a/glutin/README.md b/glutin/README.md index 263cc0af7d..1d87387454 100644 --- a/glutin/README.md +++ b/glutin/README.md @@ -20,7 +20,7 @@ It exposes a renderer-agnostic `Application` trait that can be implemented and t Add `iced_glutin` as a dependency in your `Cargo.toml`: ```toml -iced_glutin = "0.2" +iced_glutin = "0.6" ``` __Iced moves fast and the `master` branch can contain breaking changes!__ If diff --git a/glutin/src/application.rs b/glutin/src/application.rs index 3e9d11f9ff..9395c595d3 100644 --- a/glutin/src/application.rs +++ b/glutin/src/application.rs @@ -1,431 +1,30 @@ //! Create interactive, native cross-platform applications. -use crate::mouse; -use crate::{Error, Executor, Runtime}; - -pub use iced_winit::application::StyleSheet; -pub use iced_winit::Application; use iced_graphics::window; -use iced_winit::application; -use iced_winit::conversion; -use iced_winit::futures; -use iced_winit::futures::channel::mpsc; -use iced_winit::renderer; -use iced_winit::user_interface; -use iced_winit::{Clipboard, Command, Debug, Proxy, Settings}; +use iced_winit::{application::StyleSheet, Executor, Settings}; -use glutin::window::Window; -use std::mem::ManuallyDrop; +pub use iced_winit::Application; + +use crate::compositor; /// Runs an [`Application`] with an executor, compositor, and the provided /// settings. pub fn run( settings: Settings, compositor_settings: C::Settings, -) -> Result<(), Error> +) -> Result<(), iced_winit::Error> where A: Application + 'static, E: Executor + 'static, C: window::GLCompositor + 'static, - ::Theme: StyleSheet, + ::Theme: StyleSheet, { - use futures::task; - use futures::Future; - use glutin::event_loop::EventLoopBuilder; - use glutin::platform::run_return::EventLoopExtRunReturn; - use glutin::ContextBuilder; - - let mut debug = Debug::new(); - debug.startup_started(); - - let mut event_loop = EventLoopBuilder::with_user_event().build(); - let proxy = event_loop.create_proxy(); - - let runtime = { - let executor = E::new().map_err(Error::ExecutorCreationFailed)?; - let proxy = Proxy::new(event_loop.create_proxy()); - - Runtime::new(executor, proxy) - }; - - let (application, init_command) = { - let flags = settings.flags; - - runtime.enter(|| A::new(flags)) - }; - - let context = { - let builder = settings.window.into_builder( - &application.title(), - event_loop.primary_monitor(), - settings.id, - ); - - log::info!("Window builder: {:#?}", builder); - - let opengl_builder = ContextBuilder::new() - .with_vsync(true) - .with_multisampling(C::sample_count(&compositor_settings) as u16); - - let opengles_builder = opengl_builder.clone().with_gl( - glutin::GlRequest::Specific(glutin::Api::OpenGlEs, (2, 0)), - ); - - let (first_builder, second_builder) = if settings.try_opengles_first { - (opengles_builder, opengl_builder) - } else { - (opengl_builder, opengles_builder) - }; - - log::info!("Trying first builder: {:#?}", first_builder); - - let context = first_builder - .build_windowed(builder.clone(), &event_loop) - .or_else(|_| { - log::info!("Trying second builder: {:#?}", second_builder); - second_builder.build_windowed(builder, &event_loop) - }) - .map_err(|error| { - use glutin::CreationError; - use iced_graphics::Error as ContextError; - - match error { - CreationError::Window(error) => { - Error::WindowCreationFailed(error) - } - CreationError::OpenGlVersionNotSupported => { - Error::GraphicsCreationFailed( - ContextError::VersionNotSupported, - ) - } - CreationError::NoAvailablePixelFormat => { - Error::GraphicsCreationFailed( - ContextError::NoAvailablePixelFormat, - ) - } - error => Error::GraphicsCreationFailed( - ContextError::BackendError(error.to_string()), - ), - } - })?; - - #[allow(unsafe_code)] - unsafe { - context.make_current().expect("Make OpenGL context current") - } + let compositor_settings = compositor::Settings { + gl_settings: compositor_settings, + try_opengles_first: settings.try_opengles_first, }; - - #[allow(unsafe_code)] - let (compositor, renderer) = unsafe { - C::new(compositor_settings, |address| { - context.get_proc_address(address) - })? - }; - - let (mut sender, receiver) = mpsc::unbounded(); - - let mut instance = Box::pin(run_instance::( - application, - compositor, - renderer, - runtime, - proxy, - debug, - receiver, - context, - init_command, - settings.exit_on_close_request, - )); - - let mut context = task::Context::from_waker(task::noop_waker_ref()); - - let _ = event_loop.run_return(move |event, _, control_flow| { - use glutin::event_loop::ControlFlow; - - if let ControlFlow::ExitWithCode(_) = control_flow { - return; - } - - let event = match event { - glutin::event::Event::WindowEvent { - event: - glutin::event::WindowEvent::ScaleFactorChanged { - new_inner_size, - .. - }, - window_id, - } => Some(glutin::event::Event::WindowEvent { - event: glutin::event::WindowEvent::Resized(*new_inner_size), - window_id, - }), - _ => event.to_static(), - }; - - if let Some(event) = event { - sender.start_send(event).expect("Send event"); - - let poll = instance.as_mut().poll(&mut context); - - *control_flow = match poll { - task::Poll::Pending => ControlFlow::Wait, - task::Poll::Ready(_) => ControlFlow::Exit, - }; - } - }); - - Ok(()) -} - -async fn run_instance( - mut application: A, - mut compositor: C, - mut renderer: A::Renderer, - mut runtime: Runtime, A::Message>, - mut proxy: glutin::event_loop::EventLoopProxy, - mut debug: Debug, - mut receiver: mpsc::UnboundedReceiver>, - mut context: glutin::ContextWrapper, - init_command: Command, - exit_on_close_request: bool, -) where - A: Application + 'static, - E: Executor + 'static, - C: window::GLCompositor + 'static, - ::Theme: StyleSheet, -{ - use glutin::event; - use iced_winit::futures::stream::StreamExt; - - let mut clipboard = Clipboard::connect(context.window()); - let mut cache = user_interface::Cache::default(); - let mut state = application::State::new(&application, context.window()); - let mut viewport_version = state.viewport_version(); - let mut should_exit = false; - - application::run_command( - &application, - &mut cache, - &state, - &mut renderer, - init_command, - &mut runtime, - &mut clipboard, - &mut should_exit, - &mut proxy, - &mut debug, - context.window(), - || compositor.fetch_information(), - ); - runtime.track(application.subscription()); - - let mut user_interface = - ManuallyDrop::new(application::build_user_interface( - &application, - user_interface::Cache::default(), - &mut renderer, - state.logical_size(), - &mut debug, - )); - - let mut mouse_interaction = mouse::Interaction::default(); - let mut events = Vec::new(); - let mut messages = Vec::new(); - - debug.startup_finished(); - - while let Some(event) = receiver.next().await { - match event { - event::Event::MainEventsCleared => { - if events.is_empty() && messages.is_empty() { - continue; - } - - debug.event_processing_started(); - - let (interface_state, statuses) = user_interface.update( - &events, - state.cursor_position(), - &mut renderer, - &mut clipboard, - &mut messages, - ); - - debug.event_processing_finished(); - - for event in events.drain(..).zip(statuses.into_iter()) { - runtime.broadcast(event); - } - - if !messages.is_empty() - || matches!( - interface_state, - user_interface::State::Outdated - ) - { - let mut cache = - ManuallyDrop::into_inner(user_interface).into_cache(); - - // Update application - application::update( - &mut application, - &mut cache, - &state, - &mut renderer, - &mut runtime, - &mut clipboard, - &mut should_exit, - &mut proxy, - &mut debug, - &mut messages, - context.window(), - || compositor.fetch_information(), - ); - - // Update window - state.synchronize(&application, context.window()); - - user_interface = - ManuallyDrop::new(application::build_user_interface( - &application, - cache, - &mut renderer, - state.logical_size(), - &mut debug, - )); - - if should_exit { - break; - } - } - - debug.draw_started(); - let new_mouse_interaction = user_interface.draw( - &mut renderer, - state.theme(), - &renderer::Style { - text_color: state.text_color(), - }, - state.cursor_position(), - ); - debug.draw_finished(); - - if new_mouse_interaction != mouse_interaction { - context.window().set_cursor_icon( - conversion::mouse_interaction(new_mouse_interaction), - ); - - mouse_interaction = new_mouse_interaction; - } - - context.window().request_redraw(); - } - event::Event::PlatformSpecific(event::PlatformSpecific::MacOS( - event::MacOS::ReceivedUrl(url), - )) => { - use iced_native::event; - events.push(iced_native::Event::PlatformSpecific( - event::PlatformSpecific::MacOS(event::MacOS::ReceivedUrl( - url, - )), - )); - } - event::Event::UserEvent(message) => { - messages.push(message); - } - event::Event::RedrawRequested(_) => { - debug.render_started(); - - #[allow(unsafe_code)] - unsafe { - if !context.is_current() { - context = context - .make_current() - .expect("Make OpenGL context current"); - } - } - - let current_viewport_version = state.viewport_version(); - - if viewport_version != current_viewport_version { - let physical_size = state.physical_size(); - let logical_size = state.logical_size(); - - debug.layout_started(); - user_interface = ManuallyDrop::new( - ManuallyDrop::into_inner(user_interface) - .relayout(logical_size, &mut renderer), - ); - debug.layout_finished(); - - debug.draw_started(); - let new_mouse_interaction = user_interface.draw( - &mut renderer, - state.theme(), - &renderer::Style { - text_color: state.text_color(), - }, - state.cursor_position(), - ); - debug.draw_finished(); - - if new_mouse_interaction != mouse_interaction { - context.window().set_cursor_icon( - conversion::mouse_interaction( - new_mouse_interaction, - ), - ); - - mouse_interaction = new_mouse_interaction; - } - - context.resize(glutin::dpi::PhysicalSize::new( - physical_size.width, - physical_size.height, - )); - - compositor.resize_viewport(physical_size); - - viewport_version = current_viewport_version; - } - - compositor.present( - &mut renderer, - state.viewport(), - state.background_color(), - &debug.overlay(), - ); - - context.swap_buffers().expect("Swap buffers"); - - debug.render_finished(); - - // TODO: Handle animations! - // Maybe we can use `ControlFlow::WaitUntil` for this. - } - event::Event::WindowEvent { - event: window_event, - .. - } => { - if application::requests_exit(&window_event, state.modifiers()) - && exit_on_close_request - { - break; - } - - state.update(context.window(), &window_event, &mut debug); - - if let Some(event) = conversion::window_event( - &window_event, - state.scale_factor(), - state.modifiers(), - ) { - events.push(event); - } - } - _ => {} - } - } - - // Manually drop the user interface - drop(ManuallyDrop::into_inner(user_interface)); + iced_winit::application::run::>( + settings, + compositor_settings, + ) } diff --git a/glutin/src/compositor.rs b/glutin/src/compositor.rs new file mode 100644 index 0000000000..630a24f93e --- /dev/null +++ b/glutin/src/compositor.rs @@ -0,0 +1,244 @@ +use glutin::{ + context::{NotCurrentGlContext, PossiblyCurrentContextGlSurfaceAccessor}, + display::GlDisplay, + surface::GlSurface, +}; +use iced_graphics::{ + compositor::{Information, SurfaceError}, + window, Color, Error, Size, Viewport, +}; +use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; +use std::{ffi::CString, num::NonZeroU32}; + +pub use iced_winit::Application; + +/// Settings for the [`Compositor`] +#[derive(Debug, Default)] +pub struct Settings { + /// Settings of the underlying [`window::GLCompositor`]. + pub gl_settings: T, + /// Try to build the context using OpenGL ES first then OpenGL. + pub try_opengles_first: bool, +} + +/// Wraps a [`window::GLCompositor`] with a [`window::Compositor`] that uses Glutin for OpenGL +/// context creation. +#[derive(Debug)] +pub struct Compositor { + gl_compositor: C, + config: glutin::config::Config, + display: glutin::display::Display, + context: glutin::context::PossiblyCurrentContext, +} + +impl window::Compositor for Compositor { + type Settings = Settings; + type Renderer = C::Renderer; + type Surface = glutin::surface::Surface; + + fn new( + settings: Self::Settings, + compatible_window: Option<&W>, + ) -> Result<(Self, Self::Renderer), Error> { + let compatible_window = compatible_window.unwrap(); // XXX None? + + let display = create_display(&compatible_window).map_err(glutin_err)?; + + // XXX Is a different config (and context) potentially needed for + // different windows? + let sample_count = C::sample_count(&settings.gl_settings) as u8; + let config = get_config(&display, compatible_window, sample_count) + .map_err(glutin_err)?; + let context = create_context( + &display, + compatible_window, + &config, + settings.try_opengles_first, + ) + .map_err(glutin_err)?; + + // `C::new` seems to segfault in glow without a current context + let surface = + create_surface(&display, compatible_window, &config).unwrap(); + context + .make_current(&surface) + .expect("Make OpenGL context current"); + + #[allow(unsafe_code)] + let (gl_compositor, renderer) = unsafe { + C::new(settings.gl_settings, |address| { + display.get_proc_address(&CString::new(address).unwrap()) + }) + }?; + + Ok(( + Self { + gl_compositor, + config, + display, + context, + }, + renderer, + )) + } + + fn create_surface( + &mut self, + window: &W, + ) -> Self::Surface { + // XXX unwrap + let surface = + create_surface(&self.display, window, &self.config).unwrap(); + + // Enable vsync + self.context + .make_current(&surface) + .expect("Make OpenGL context current"); + surface + .set_swap_interval( + &self.context, + glutin::surface::SwapInterval::Wait( + NonZeroU32::new(1).unwrap(), + ), + ) + .expect("Set swap interval"); + + surface + } + + fn configure_surface( + &mut self, + surface: &mut Self::Surface, + width: u32, + height: u32, + ) { + surface.resize( + &self.context, + NonZeroU32::new(width).unwrap_or(NonZeroU32::new(1).unwrap()), + NonZeroU32::new(height).unwrap_or(NonZeroU32::new(1).unwrap()), + ); + self.gl_compositor.resize_viewport(Size { width, height }); + } + + fn fetch_information(&self) -> Information { + self.gl_compositor.fetch_information() + } + + fn present>( + &mut self, + renderer: &mut Self::Renderer, + surface: &mut Self::Surface, + viewport: &Viewport, + background_color: Color, + overlay: &[T], + ) -> Result<(), SurfaceError> { + self.context + .make_current(surface) + .expect("Make OpenGL context current"); + self.gl_compositor.present( + renderer, + viewport, + background_color, + overlay, + ); + surface.swap_buffers(&self.context).expect("Swap buffers"); + Ok(()) + } +} + +fn create_display( + window: &W, +) -> Result { + #[cfg(target_os = "windows")] + let api_preference = glutin::display::DisplayApiPreference::WglThenEgl( + Some(window.raw_window_handle()), + ); + #[cfg(target_os = "macos")] + let api_preference = glutin::display::DisplayApiPreference::Cgl; + #[cfg(not(any(target_os = "windows", target_os = "macos")))] + let api_preference = glutin::display::DisplayApiPreference::EglThenGlx( + Box::new(iced_winit::winit::platform::unix::register_xlib_error_hook), + ); + + #[allow(unsafe_code)] + unsafe { + glutin::display::Display::new( + window.raw_display_handle(), + api_preference, + ) + } +} + +fn get_config( + display: &glutin::display::Display, + window: &W, + sample_count: u8, +) -> Result { + let mut template_builder = glutin::config::ConfigTemplateBuilder::new() + .compatible_with_native_window(window.raw_window_handle()) + .with_transparency(true); + if sample_count != 0 { + template_builder = template_builder.with_multisampling(sample_count); + } + let template = template_builder.build(); + + #[allow(unsafe_code)] + Ok(unsafe { display.find_configs(template) }?.next().unwrap()) // XXX unwrap; first config? +} + +fn create_context( + display: &glutin::display::Display, + window: &W, + config: &glutin::config::Config, + try_opengles_first: bool, +) -> Result { + let opengl_attributes = glutin::context::ContextAttributesBuilder::new() + .build(Some(window.raw_window_handle())); + let opengles_attributes = glutin::context::ContextAttributesBuilder::new() + .with_context_api(glutin::context::ContextApi::Gles(Some( + glutin::context::Version { major: 2, minor: 0 }, + ))) + .build(Some(window.raw_window_handle())); + + let (first_attributes, second_attributes) = if try_opengles_first { + (opengles_attributes, opengl_attributes) + } else { + (opengl_attributes, opengles_attributes) + }; + + #[allow(unsafe_code)] + Ok(unsafe { display.create_context(config, &first_attributes) } + .or_else(|_| { + log::info!("Trying second attributes: {:#?}", second_attributes); + unsafe { display.create_context(config, &second_attributes) } + })? + .treat_as_possibly_current()) +} + +fn create_surface( + display: &glutin::display::Display, + window: &W, + config: &glutin::config::Config, +) -> Result< + glutin::surface::Surface, + glutin::error::Error, +> { + let surface_attributes = glutin::surface::SurfaceAttributesBuilder::< + glutin::surface::WindowSurface, + >::new() + .build( + window.raw_window_handle(), + NonZeroU32::new(1).unwrap(), + NonZeroU32::new(1).unwrap(), + ); + + #[allow(unsafe_code)] + unsafe { + display.create_window_surface(config, &surface_attributes) + } +} + +fn glutin_err(err: glutin::error::Error) -> iced_graphics::Error { + // TODO: match error kind? Doesn't seem to match `iced_grapihcs::Error` well + iced_graphics::Error::BackendError(err.to_string()) +} diff --git a/glutin/src/lib.rs b/glutin/src/lib.rs index 33afd66435..d86cf7b585 100644 --- a/glutin/src/lib.rs +++ b/glutin/src/lib.rs @@ -28,6 +28,8 @@ pub use glutin; pub use iced_winit::*; pub mod application; +mod compositor; #[doc(no_inline)] pub use application::Application; +pub use compositor::{Compositor, Settings}; diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index aabdf7fc45..8f94766d11 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -35,6 +35,11 @@ impl Renderer { &self.backend } + /// Returns the [`Backend`] of the [`Renderer`], mutably. + pub fn backend_mut(&mut self) -> &mut B { + &mut self.backend + } + /// Enqueues the given [`Primitive`] in the [`Renderer`] for drawing. pub fn draw_primitive(&mut self, primitive: Primitive) { self.primitives.push(primitive); diff --git a/lazy/src/component.rs b/lazy/src/component.rs index 3d7b8758bc..4f1df6504c 100644 --- a/lazy/src/component.rs +++ b/lazy/src/component.rs @@ -11,7 +11,7 @@ use iced_native::{ }; use ouroboros::self_referencing; -use std::cell::RefCell; +use std::cell::{Ref, RefCell}; use std::marker::PhantomData; /// A reusable, custom widget that uses The Elm Architecture. @@ -322,25 +322,25 @@ where } fn overlay<'b>( - &'b mut self, + &'b self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option> { let overlay = OverlayBuilder { instance: self, + instance_ref_builder: |instance| instance.state.borrow(), tree, types: PhantomData, overlay_builder: |instance, tree| { - instance.state.get_mut().as_mut().unwrap().with_element_mut( - move |element| { - element.as_mut().unwrap().as_widget_mut().overlay( - &mut tree.children[0], - layout, - renderer, - ) - }, - ) + instance + .as_ref() + .unwrap() + .borrow_element() + .as_ref() + .unwrap() + .as_widget() + .overlay(&mut tree.children[0], layout, renderer) }, } .build(); @@ -362,11 +362,15 @@ where #[self_referencing] struct Overlay<'a, 'b, Message, Renderer, Event, S> { - instance: &'a mut Instance<'b, Message, Renderer, Event, S>, + instance: &'a Instance<'b, Message, Renderer, Event, S>, tree: &'a mut Tree, types: PhantomData<(Message, Event, S)>, - #[borrows(mut instance, mut tree)] + #[borrows(instance)] + #[covariant] + instance_ref: Ref<'this, Option>>, + + #[borrows(instance_ref, mut tree)] #[covariant] overlay: Option>, } @@ -510,6 +514,7 @@ where self.overlay = Some( OverlayBuilder { instance: overlay.instance, + instance_ref_builder: |instance| instance.state.borrow(), tree: overlay.tree, types: PhantomData, overlay_builder: |_, _| None, diff --git a/lazy/src/lazy.rs b/lazy/src/lazy.rs index 2611dd1092..d61cc77e46 100644 --- a/lazy/src/lazy.rs +++ b/lazy/src/lazy.rs @@ -207,7 +207,7 @@ where } fn overlay<'b>( - &'b mut self, + &'b self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, @@ -216,12 +216,12 @@ where cached: self, tree: &mut tree.children[0], types: PhantomData, - overlay_builder: |cached, tree| { - Rc::get_mut(cached.element.get_mut().as_mut().unwrap()) - .unwrap() - .get_mut() - .as_widget_mut() - .overlay(tree, layout, renderer) + element_ref_builder: |cached| cached.element.borrow(), + element_builder: |element_ref| { + element_ref.as_ref().unwrap().borrow() + }, + overlay_builder: |element, tree| { + element.as_widget().overlay(tree, layout, renderer) }, } .build(); @@ -237,11 +237,20 @@ where #[self_referencing] struct Overlay<'a, 'b, Message, Renderer, Dependency, View> { - cached: &'a mut Lazy<'b, Message, Renderer, Dependency, View>, + cached: &'a Lazy<'b, Message, Renderer, Dependency, View>, tree: &'a mut Tree, types: PhantomData<(Message, Dependency, View)>, - #[borrows(mut cached, mut tree)] + #[borrows(cached)] + #[covariant] + element_ref: + Ref<'this, Option>>>>, + + #[borrows(element_ref)] + #[covariant] + element: Ref<'this, Element<'static, Message, Renderer>>, + + #[borrows(element, mut tree)] #[covariant] overlay: Option>, } diff --git a/lazy/src/responsive.rs b/lazy/src/responsive.rs index 5e1b5dff1c..0b7ae6de10 100644 --- a/lazy/src/responsive.rs +++ b/lazy/src/responsive.rs @@ -235,20 +235,18 @@ where } fn overlay<'b>( - &'b mut self, + &'b self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option> { - use std::ops::DerefMut; - let state = tree.state.downcast_ref::(); let overlay = OverlayBuilder { content: self.content.borrow_mut(), tree: state.tree.borrow_mut(), types: PhantomData, - overlay_builder: |content: &mut RefMut>, tree| { + overlay_builder: |content, tree| { content.update( tree, renderer, @@ -256,18 +254,16 @@ where &self.view, ); - let Content { - element, layout, .. - } = content.deref_mut(); - let content_layout = Layout::with_offset( - layout.bounds().position() - Point::ORIGIN, - layout, + layout.position() - Point::ORIGIN, + &content.layout, ); - element - .as_widget_mut() - .overlay(tree, content_layout, renderer) + content.element.as_widget().overlay( + tree, + content_layout, + renderer, + ) }, } .build(); diff --git a/native/Cargo.toml b/native/Cargo.toml index bbf9295122..2555f2c1c2 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -8,12 +8,15 @@ license = "MIT" repository = "https://github.com/iced-rs/iced" [features] +default = [] +wayland = ["sctk"] debug = [] [dependencies] twox-hash = { version = "1.5", default-features = false } unicode-segmentation = "1.6" num-traits = "0.2" +sctk = { package = "smithay-client-toolkit", git = "https://github.com/Smithay/client-toolkit", rev = "3776d4a", optional = true } [dependencies.iced_core] version = "0.6" diff --git a/native/src/command.rs b/native/src/command.rs index 89ee7375af..9d70c5ebb8 100644 --- a/native/src/command.rs +++ b/native/src/command.rs @@ -1,5 +1,7 @@ //! Run asynchronous actions. mod action; +/// platform specific actions +pub mod platform_specific; pub use action::Action; diff --git a/native/src/command/action.rs b/native/src/command/action.rs index a6954f8f75..e6c00aaf8e 100644 --- a/native/src/command/action.rs +++ b/native/src/command/action.rs @@ -1,4 +1,5 @@ use crate::clipboard; +use crate::command::platform_specific; use crate::system; use crate::widget; use crate::window; @@ -20,13 +21,16 @@ pub enum Action { Clipboard(clipboard::Action), /// Run a window action. - Window(window::Action), + Window(window::Id, window::Action), /// Run a system action. System(system::Action), /// Run a widget action. Widget(widget::Action), + + /// Run a platform specific action + PlatformSpecific(platform_specific::Action), } impl Action { @@ -46,9 +50,12 @@ impl Action { match self { Self::Future(future) => Action::Future(Box::pin(future.map(f))), Self::Clipboard(action) => Action::Clipboard(action.map(f)), - Self::Window(window) => Action::Window(window.map(f)), + Self::Window(id, window) => Action::Window(id, window.map(f)), Self::System(system) => Action::System(system.map(f)), Self::Widget(widget) => Action::Widget(widget.map(f)), + Self::PlatformSpecific(action) => { + Action::PlatformSpecific(action.map(f)) + } } } } @@ -60,9 +67,14 @@ impl fmt::Debug for Action { Self::Clipboard(action) => { write!(f, "Action::Clipboard({:?})", action) } - Self::Window(action) => write!(f, "Action::Window({:?})", action), + Self::Window(id, action) => { + write!(f, "Action::Window({:?}, {:?})", id, action) + } Self::System(action) => write!(f, "Action::System({:?})", action), Self::Widget(_action) => write!(f, "Action::Widget"), + Self::PlatformSpecific(action) => { + write!(f, "Action::PlatformSpecific({:?})", action) + } } } } diff --git a/native/src/command/platform_specific/mod.rs b/native/src/command/platform_specific/mod.rs new file mode 100644 index 0000000000..2bc43bea5f --- /dev/null +++ b/native/src/command/platform_specific/mod.rs @@ -0,0 +1,46 @@ +use std::{fmt, marker::PhantomData}; + +use iced_futures::MaybeSend; + +/// wayland platform specific actions +#[cfg(feature = "wayland")] +pub mod wayland; + +/// Platform specific actions defined for wayland +pub enum Action { + /// LayerSurface Actions + #[cfg(feature = "wayland")] + Wayland(wayland::Action), + /// phantom data variant in case the platform has not specific actions implemented + Phantom(PhantomData), +} + +impl Action { + /// Maps the output of an [`Action`] using the given function. + pub fn map( + self, + f: impl Fn(T) -> A + 'static + MaybeSend + Sync, + ) -> Action + where + T: 'static, + A: 'static, + { + match self { + #[cfg(feature = "wayland")] + Action::Wayland(a) => Action::Wayland(a.map(f)), + Action::Phantom(_) => unimplemented!(), + } + } +} + +impl fmt::Debug for Action { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + #[cfg(feature = "wayland")] + Self::Wayland(arg0) => { + f.debug_tuple("LayerSurface").field(arg0).finish() + } + Action::Phantom(_) => unimplemented!(), + } + } +} diff --git a/native/src/command/platform_specific/wayland/layer_surface.rs b/native/src/command/platform_specific/wayland/layer_surface.rs new file mode 100644 index 0000000000..c8078ae3e8 --- /dev/null +++ b/native/src/command/platform_specific/wayland/layer_surface.rs @@ -0,0 +1,232 @@ +use std::hash::{Hash, Hasher}; +use std::marker::PhantomData; +use std::{collections::hash_map::DefaultHasher, fmt}; + +use iced_futures::MaybeSend; +use sctk::{ + reexports::client::protocol::wl_output::WlOutput, + shell::layer::{Anchor, KeyboardInteractivity, Layer}, +}; + +use crate::layout::Limits; +use crate::window; + +/// 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: window::Id, + /// layer + pub layer: Layer, + /// interactivity + pub keyboard_interactivity: KeyboardInteractivity, + /// 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: window::Id::new(0), + layer: Layer::Top, + keyboard_interactivity: Default::default(), + 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).min_width(1).max_width(1920).max_height(1080), + } + } +} + +#[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: window::Id, + /// The new logical width of the window + width: Option, + /// The new logical height of the window + height: Option, + }, + /// Destroy the layer surface + Destroy(window::Id), + /// The edges which the layer surface is anchored to + Anchor { + /// id of the layer surface + id: window::Id, + /// anchor of the layer surface + anchor: Anchor, + }, + /// exclusive zone of the layer surface + ExclusiveZone { + /// id of the layer surface + id: window::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: window::Id, + /// margins of the layer surface + margin: IcedMargin, + }, + /// keyboard interactivity of the layer surface + KeyboardInteractivity { + /// id of the layer surface + id: window::Id, + /// keyboard interactivity of the layer surface + keyboard_interactivity: KeyboardInteractivity, + }, + /// layer of the layer surface + Layer { + /// id of the layer surface + id: window::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 + ), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +/// TODO(derezzedex) +pub struct Id(u64); + +impl Id { + /// TODO(derezzedex) + pub fn new(id: impl Hash) -> Id { + let mut hasher = DefaultHasher::new(); + id.hash(&mut hasher); + + Id(hasher.finish()) + } +} diff --git a/native/src/command/platform_specific/wayland/mod.rs b/native/src/command/platform_specific/wayland/mod.rs new file mode 100644 index 0000000000..68af3644c6 --- /dev/null +++ b/native/src/command/platform_specific/wayland/mod.rs @@ -0,0 +1,51 @@ +use std::fmt::Debug; + +use iced_futures::MaybeSend; + +/// layer surface actions +pub mod layer_surface; +/// popup actions +pub mod popup; +/// window actions +pub mod window; + +#[derive(Clone)] +/// Platform specific actions defined for wayland +pub enum Action { + /// LayerSurface Actions + LayerSurface(layer_surface::Action), + /// Window Actions + Window(window::Action), + /// popup + Popup(popup::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)), + } + } +} + +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(), + } + } +} diff --git a/native/src/command/platform_specific/wayland/popup.rs b/native/src/command/platform_specific/wayland/popup.rs new file mode 100644 index 0000000000..ec45f181fa --- /dev/null +++ b/native/src/command/platform_specific/wayland/popup.rs @@ -0,0 +1,193 @@ +use std::hash::{Hash, Hasher}; +use std::marker::PhantomData; +use std::{collections::hash_map::DefaultHasher, fmt}; + +use iced_core::Rectangle; +use iced_futures::MaybeSend; +use sctk::reexports::protocols::xdg::shell::client::xdg_positioner::{ + Anchor, Gravity, +}; + +use crate::layout::Limits; +use crate::window; +/// Popup creation details +#[derive(Debug, Clone)] +pub struct SctkPopupSettings { + /// XXX must be unique, id of the parent + pub parent: window::Id, + /// XXX must be unique, id of the popup + pub id: window::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) + .min_width(1) + .max_width(300) + .max_height(1080), + 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: window::Id, + }, + /// request that the popup make an explicit grab + Grab { + /// id of the popup + id: window::Id, + }, + /// set the size of the popup + Size { + /// id of the popup + id: window::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 + ), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +/// TODO(derezzedex) +pub struct Id(u64); + +impl Id { + /// TODO(derezzedex) + pub fn new(id: impl Hash) -> Id { + let mut hasher = DefaultHasher::new(); + id.hash(&mut hasher); + + Id(hasher.finish()) + } +} diff --git a/native/src/command/platform_specific/wayland/window.rs b/native/src/command/platform_specific/wayland/window.rs new file mode 100644 index 0000000000..47a6d86fd7 --- /dev/null +++ b/native/src/command/platform_specific/wayland/window.rs @@ -0,0 +1,303 @@ +use std::hash::{Hash, Hasher}; +use std::marker::PhantomData; +use std::{collections::hash_map::DefaultHasher, fmt}; + +use iced_futures::MaybeSend; +use sctk::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge; + +use crate::layout::Limits; +use crate::window; + +/// window settings +#[derive(Debug, Clone)] +pub struct SctkWindowSettings { + /// vanilla window settings + pub iced_settings: window::Settings, + /// window id + pub window_id: window::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 popup size + pub size_limits: Limits, +} + +impl Default for SctkWindowSettings { + fn default() -> Self { + Self { + iced_settings: Default::default(), + window_id: window::Id::new(0), + app_id: Default::default(), + title: Default::default(), + parent: Default::default(), + autosize: Default::default(), + size_limits: Limits::NONE.min_height(1).min_width(1).max_width(1920).max_height(1080), + } + } +} + +#[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(window::Id), + /// Set size of the window. + Size { + /// id of the window + id: window::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: window::Id, + /// optional size + size: Option<(u32, u32)>, + }, + /// Set max size of the window. + MaxSize { + /// id of the window + id: window::Id, + /// optional size + size: Option<(u32, u32)>, + }, + /// Set title of the window. + Title { + /// id of the window + id: window::Id, + /// The new logical width of the window + title: String, + }, + /// Minimize the window. + Minimize { + /// id of the window + id: window::Id, + }, + /// Toggle maximization of the window. + ToggleMaximized { + /// id of the window + id: window::Id, + }, + /// Maximize the window. + Maximize { + /// id of the window + id: window::Id, + }, + /// UnsetMaximize the window. + UnsetMaximize { + /// id of the window + id: window::Id, + }, + /// Toggle fullscreen of the window. + ToggleFullscreen { + /// id of the window + id: window::Id, + }, + /// Fullscreen the window. + Fullscreen { + /// id of the window + id: window::Id, + }, + /// UnsetFullscreen the window. + UnsetFullscreen { + /// id of the window + id: window::Id, + }, + /// Start an interactive move of the window. + InteractiveResize { + /// id of the window + id: window::Id, + /// edge being resized + edge: ResizeEdge, + }, + /// Start an interactive move of the window. + InteractiveMove { + /// id of the window + id: window::Id, + }, + /// Show the window context menu + ShowWindowMenu { + /// id of the window + id: window::Id, + /// x location of popup + x: i32, + /// y location of popup + y: i32, + }, + /// Set the mode of the window + Mode(window::Id, window::Mode), + /// Set the app id of the window + AppId { + /// id of the window + id: window::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 + ), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +/// TODO(derezzedex) +pub struct Id(u64); + +impl Id { + /// TODO(derezzedex) + pub fn new(id: impl Hash) -> Id { + let mut hasher = DefaultHasher::new(); + id.hash(&mut hasher); + + Id(hasher.finish()) + } +} diff --git a/native/src/element.rs b/native/src/element.rs index 2f1adeff59..72cd06d8de 100644 --- a/native/src/element.rs +++ b/native/src/element.rs @@ -405,7 +405,7 @@ where } fn overlay<'b>( - &'b mut self, + &'b self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, @@ -560,7 +560,7 @@ where } fn overlay<'b>( - &'b mut self, + &'b self, state: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, diff --git a/native/src/event.rs b/native/src/event.rs index bcfaf891bc..a7d04bbda6 100644 --- a/native/src/event.rs +++ b/native/src/event.rs @@ -4,6 +4,9 @@ use crate::mouse; use crate::touch; use crate::window; +#[cfg(feature = "wayland")] +/// platform specific wayland events +pub mod wayland; /// A user interface event. /// /// _**Note:** This type is largely incomplete! If you need to track @@ -19,7 +22,7 @@ pub enum Event { Mouse(mouse::Event), /// A window event - Window(window::Event), + Window(window::Id, window::Event), /// A touch event Touch(touch::Event), @@ -31,6 +34,9 @@ pub enum Event { /// A platform specific event #[derive(Debug, Clone, PartialEq, Eq)] pub enum PlatformSpecific { + /// A Wayland specific event + #[cfg(feature = "wayland")] + Wayland(wayland::Event), /// A MacOS specific event MacOS(MacOS), } diff --git a/native/src/event/wayland/layer.rs b/native/src/event/wayland/layer.rs new file mode 100644 index 0000000000..c1928ad36e --- /dev/null +++ b/native/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/native/src/event/wayland/mod.rs b/native/src/event/wayland/mod.rs new file mode 100644 index 0000000000..e514e1dde1 --- /dev/null +++ b/native/src/event/wayland/mod.rs @@ -0,0 +1,31 @@ +mod layer; +mod output; +mod popup; +mod seat; +mod window; + +use crate::window::Id; +use sctk::reexports::client::protocol::{ + wl_output::WlOutput, wl_seat::WlSeat, wl_surface::WlSurface, +}; + +pub use layer::*; +pub use output::*; +pub use popup::*; +pub use seat::*; +pub use window::*; + +/// wayland events +#[derive(Debug, Clone, PartialEq, Eq)] +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), +} diff --git a/native/src/event/wayland/output.rs b/native/src/event/wayland/output.rs new file mode 100644 index 0000000000..3c5280dc6c --- /dev/null +++ b/native/src/event/wayland/output.rs @@ -0,0 +1,30 @@ +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), + } + } +} \ No newline at end of file diff --git a/native/src/event/wayland/popup.rs b/native/src/event/wayland/popup.rs new file mode 100644 index 0000000000..ff925870b2 --- /dev/null +++ b/native/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/native/src/event/wayland/seat.rs b/native/src/event/wayland/seat.rs new file mode 100644 index 0000000000..3da4374e71 --- /dev/null +++ b/native/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/native/src/event/wayland/window.rs b/native/src/event/wayland/window.rs new file mode 100644 index 0000000000..3db32f2ad7 --- /dev/null +++ b/native/src/event/wayland/window.rs @@ -0,0 +1,22 @@ +#![allow(missing_docs)] + +/// window events +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum WindowEvent { + /// window manager capabilities + WmCapabilities(Vec), + /// window state + State(WindowState), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +/// the state of the window +pub enum WindowState { + Maximized, + Fullscreen, + Activated, + TiledLeft, + TiledRight, + TiledTop, + TiledBottom, +} diff --git a/native/src/overlay.rs b/native/src/overlay.rs index 0b05b058e1..c4a80a99c8 100644 --- a/native/src/overlay.rs +++ b/native/src/overlay.rs @@ -93,7 +93,7 @@ where /// This method will generally only be used by advanced users that are /// implementing the [`Widget`](crate::Widget) trait. pub fn from_children<'a, Message, Renderer>( - children: &'a mut [crate::Element<'_, Message, Renderer>], + children: &'a [crate::Element<'_, Message, Renderer>], tree: &'a mut Tree, layout: Layout<'_>, renderer: &Renderer, @@ -102,11 +102,11 @@ where Renderer: crate::Renderer, { children - .iter_mut() + .iter() .zip(&mut tree.children) .zip(layout.children()) .filter_map(|((child, state), layout)| { - child.as_widget_mut().overlay(state, layout, renderer) + child.as_widget().overlay(state, layout, renderer) }) .next() } diff --git a/native/src/renderer.rs b/native/src/renderer.rs index 5e776be6cd..c1060dbd59 100644 --- a/native/src/renderer.rs +++ b/native/src/renderer.rs @@ -4,6 +4,8 @@ mod null; #[cfg(debug_assertions)] pub use null::Null; +pub use iced_core::BorderRadius; + use crate::layout; use crate::{Background, Color, Element, Rectangle, Vector}; @@ -59,29 +61,6 @@ pub struct Quad { pub border_color: Color, } -/// The border radi for the corners of a graphics primitive in the order: -/// top-left, top-right, bottom-right, bottom-left. -#[derive(Debug, Clone, Copy, PartialEq, Default)] -pub struct BorderRadius([f32; 4]); - -impl From for BorderRadius { - fn from(w: f32) -> Self { - Self([w; 4]) - } -} - -impl From<[f32; 4]> for BorderRadius { - fn from(radi: [f32; 4]) -> Self { - Self(radi) - } -} - -impl From for [f32; 4] { - fn from(radi: BorderRadius) -> Self { - radi.0 - } -} - /// The styling attributes of a [`Renderer`]. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { diff --git a/native/src/user_interface.rs b/native/src/user_interface.rs index 376ce568f8..a48d498393 100644 --- a/native/src/user_interface.rs +++ b/native/src/user_interface.rs @@ -190,7 +190,7 @@ where let mut state = State::Updated; let mut manual_overlay = - ManuallyDrop::new(self.root.as_widget_mut().overlay( + ManuallyDrop::new(self.root.as_widget().overlay( &mut self.state, Layout::new(&self.base), renderer, @@ -226,7 +226,7 @@ where ); manual_overlay = - ManuallyDrop::new(self.root.as_widget_mut().overlay( + ManuallyDrop::new(self.root.as_widget().overlay( &mut self.state, Layout::new(&self.base), renderer, @@ -395,11 +395,11 @@ where let viewport = Rectangle::with_size(self.bounds); - let base_cursor = if let Some(overlay) = self - .root - .as_widget_mut() - .overlay(&mut self.state, Layout::new(&self.base), renderer) - { + let base_cursor = if let Some(overlay) = self.root.as_widget().overlay( + &mut self.state, + Layout::new(&self.base), + renderer, + ) { let overlay_layout = self .overlay .take() @@ -452,7 +452,7 @@ where overlay .as_ref() .and_then(|layout| { - root.as_widget_mut() + root.as_widget() .overlay(&mut self.state, Layout::new(base), renderer) .map(|overlay| { let overlay_interaction = overlay.mouse_interaction( @@ -496,7 +496,7 @@ where operation, ); - if let Some(mut overlay) = self.root.as_widget_mut().overlay( + if let Some(mut overlay) = self.root.as_widget().overlay( &mut self.state, Layout::new(&self.base), renderer, diff --git a/native/src/widget.rs b/native/src/widget.rs index efe26fc78e..be7de3565d 100644 --- a/native/src/widget.rs +++ b/native/src/widget.rs @@ -17,6 +17,7 @@ pub mod column; pub mod container; pub mod helpers; pub mod image; +pub mod mouse_listener; pub mod operation; pub mod pane_grid; pub mod pick_list; @@ -51,6 +52,8 @@ pub use helpers::*; #[doc(no_inline)] pub use image::Image; #[doc(no_inline)] +pub use mouse_listener::MouseListener; +#[doc(no_inline)] pub use pane_grid::PaneGrid; #[doc(no_inline)] pub use pick_list::PickList; @@ -211,7 +214,7 @@ where /// Returns the overlay of the [`Widget`], if there is any. fn overlay<'a>( - &'a mut self, + &'a self, _state: &'a mut Tree, _layout: Layout<'_>, _renderer: &Renderer, diff --git a/native/src/widget/button.rs b/native/src/widget/button.rs index bbd9451ce3..3c0d6e453d 100644 --- a/native/src/widget/button.rs +++ b/native/src/widget/button.rs @@ -2,16 +2,18 @@ //! //! A [`Button`] has some local [`State`]. use crate::event::{self, Event}; +use crate::keyboard; use crate::layout; use crate::mouse; use crate::overlay; use crate::renderer; use crate::touch; +use crate::widget; +use crate::widget::operation::{self, Operation}; use crate::widget::tree::{self, Tree}; -use crate::widget::Operation; use crate::{ - Background, Clipboard, Color, Element, Layout, Length, Padding, Point, - Rectangle, Shell, Vector, Widget, + Background, Clipboard, Color, Command, Element, Layout, Length, Padding, + Point, Rectangle, Shell, Vector, Widget, }; pub use iced_style::button::{Appearance, StyleSheet}; @@ -56,6 +58,7 @@ where Renderer: crate::Renderer, Renderer::Theme: StyleSheet, { + id: Option, content: Element<'a, Message, Renderer>, on_press: Option, width: Length, @@ -72,6 +75,7 @@ where /// Creates a new [`Button`] with the given content. pub fn new(content: impl Into>) -> Self { Button { + id: None, content: content.into(), on_press: None, width: Length::Shrink, @@ -107,6 +111,12 @@ where self } + /// Sets the [`Id`] of the [`Button`]. + pub fn id(mut self, id: Id) -> Self { + self.id = Some(id); + self + } + /// Sets the style variant of this [`Button`]. pub fn style( mut self, @@ -178,6 +188,9 @@ where operation, ); }); + + let state = tree.state.downcast_mut::(); + operation.focusable(state, self.id.as_ref().map(|id| &id.0)); } fn on_event( @@ -260,12 +273,12 @@ where } fn overlay<'b>( - &'b mut self, + &'b self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option> { - self.content.as_widget_mut().overlay( + self.content.as_widget().overlay( &mut tree.children[0], layout.children().next().unwrap(), renderer, @@ -289,6 +302,7 @@ where #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct State { is_pressed: bool, + is_focused: bool, } impl State { @@ -296,6 +310,21 @@ impl State { pub fn new() -> State { State::default() } + + /// Returns whether the [`Button`] is currently focused or not. + pub fn is_focused(&self) -> bool { + self.is_focused + } + + /// Focuses the [`Button`]. + pub fn focus(&mut self) { + self.is_focused = true; + } + + /// Unfocuses the [`Button`]. + pub fn unfocus(&mut self) { + self.is_focused = false; + } } /// Processes the given [`Event`] and updates the [`State`] of a [`Button`] @@ -341,6 +370,17 @@ pub fn update<'a, Message: Clone>( } } } + + Event::Keyboard(keyboard::Event::KeyPressed { key_code, .. }) => { + if let Some(on_press) = on_press.clone() { + let state = state(); + if state.is_focused && key_code == keyboard::KeyCode::Enter { + state.is_pressed = true; + shell.publish(on_press); + return event::Status::Captured; + } + } + } Event::Touch(touch::Event::FingerLost { .. }) => { let state = state(); @@ -368,17 +408,18 @@ where Renderer::Theme: StyleSheet, { let is_mouse_over = bounds.contains(cursor_position); + let state = state(); let styling = if !is_enabled { style_sheet.disabled(style) } else if is_mouse_over { - let state = state(); - if state.is_pressed { style_sheet.pressed(style) } else { style_sheet.hovered(style) } + } else if state.is_focused { + style_sheet.focused(style) } else { style_sheet.active(style) }; @@ -393,7 +434,7 @@ where y: bounds.y + styling.shadow_offset.y, ..bounds }, - border_radius: styling.border_radius.into(), + border_radius: styling.border_radius, border_width: 0.0, border_color: Color::TRANSPARENT, }, @@ -404,7 +445,7 @@ where renderer.fill_quad( renderer::Quad { bounds, - border_radius: styling.border_radius.into(), + border_radius: styling.border_radius, border_width: styling.border_width, border_color: styling.border_color, }, @@ -451,3 +492,46 @@ pub fn mouse_interaction( mouse::Interaction::default() } } + +/// The identifier of a [`Button`]. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Id(widget::Id); + +impl Id { + /// Creates a custom [`Id`]. + pub fn new(id: impl Into>) -> Self { + Self(widget::Id::new(id)) + } + + /// Creates a unique [`Id`]. + /// + /// This function produces a different [`Id`] every time it is called. + pub fn unique() -> Self { + Self(widget::Id::unique()) + } +} + +impl From for widget::Id { + fn from(id: Id) -> Self { + id.0 + } +} + +/// Produces a [`Command`] that focuses the [`Button`] with the given [`Id`]. +pub fn focus(id: Id) -> Command { + Command::widget(operation::focusable::focus(id.0)) +} + +impl operation::Focusable for State { + fn is_focused(&self) -> bool { + State::is_focused(self) + } + + fn focus(&mut self) { + State::focus(self) + } + + fn unfocus(&mut self) { + State::unfocus(self) + } +} diff --git a/native/src/widget/column.rs b/native/src/widget/column.rs index 8030778bff..a8b0f18307 100644 --- a/native/src/widget/column.rs +++ b/native/src/widget/column.rs @@ -242,12 +242,12 @@ where } fn overlay<'b>( - &'b mut self, + &'b self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option> { - overlay::from_children(&mut self.children, tree, layout, renderer) + overlay::from_children(&self.children, tree, layout, renderer) } } diff --git a/native/src/widget/container.rs b/native/src/widget/container.rs index 16d0cb61bd..06ae1bc9e2 100644 --- a/native/src/widget/container.rs +++ b/native/src/widget/container.rs @@ -248,12 +248,12 @@ where } fn overlay<'b>( - &'b mut self, + &'b self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option> { - self.content.as_widget_mut().overlay( + self.content.as_widget().overlay( &mut tree.children[0], layout.children().next().unwrap(), renderer, diff --git a/native/src/widget/helpers.rs b/native/src/widget/helpers.rs index 8cc1ae8205..72ba093c3d 100644 --- a/native/src/widget/helpers.rs +++ b/native/src/widget/helpers.rs @@ -311,3 +311,13 @@ where { widget::Svg::new(handle) } + +/// A container intercepting mouse events. +pub fn mouse_listener<'a, Message, Renderer>( + widget: impl Into>, +) -> widget::MouseListener<'a, Message, Renderer> +where + Renderer: crate::Renderer, +{ + widget::MouseListener::new(widget) +} diff --git a/native/src/widget/mouse_listener.rs b/native/src/widget/mouse_listener.rs new file mode 100644 index 0000000000..116ef474f8 --- /dev/null +++ b/native/src/widget/mouse_listener.rs @@ -0,0 +1,401 @@ +//! A container for capturing mouse events. + +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::overlay; +use crate::renderer; +use crate::touch; +use crate::widget::{tree, Operation, Tree}; +use crate::{ + Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Size, Widget, +}; + +use std::u32; + +/// Emit messages on mouse events. +#[allow(missing_debug_implementations)] +pub struct MouseListener<'a, Message, Renderer> { + content: Element<'a, Message, Renderer>, + + /// Sets the message to emit on a left mouse button press. + on_press: Option, + + /// Sets the message to emit on a left mouse button release. + on_release: Option, + + /// Sets the message to emit on a right mouse button press. + on_right_press: Option, + + /// Sets the message to emit on a right mouse button release. + on_right_release: Option, + + /// Sets the message to emit on a middle mouse button press. + on_middle_press: Option, + + /// Sets the message to emit on a middle mouse button release. + on_middle_release: Option, + + /// Sets the message to emit when the mouse enters the widget. + on_mouse_enter: Option, + + /// Sets the messsage to emit when the mouse exits the widget. + on_mouse_exit: Option, +} + +impl<'a, Message, Renderer> MouseListener<'a, Message, Renderer> { + /// The message to emit on a left button press. + #[must_use] + pub fn on_press(mut self, message: Message) -> Self { + self.on_press = Some(message); + self + } + + /// The message to emit on a left button release. + #[must_use] + pub fn on_release(mut self, message: Message) -> Self { + self.on_release = Some(message); + self + } + + /// The message to emit on a right button press. + #[must_use] + pub fn on_right_press(mut self, message: Message) -> Self { + self.on_right_press = Some(message); + self + } + + /// The message to emit on a right button release. + #[must_use] + pub fn on_right_release(mut self, message: Message) -> Self { + self.on_right_release = Some(message); + self + } + + /// The message to emit on a middle button press. + #[must_use] + pub fn on_middle_press(mut self, message: Message) -> Self { + self.on_middle_press = Some(message); + self + } + + /// The message to emit on a middle button release. + #[must_use] + pub fn on_middle_release(mut self, message: Message) -> Self { + self.on_middle_release = Some(message); + self + } + + /// The message to emit when the mouse enters the widget. + #[must_use] + pub fn on_mouse_enter(mut self, message: Message) -> Self { + self.on_mouse_enter = Some(message); + self + } + + /// The messsage to emit when the mouse exits the widget. + #[must_use] + pub fn on_mouse_exit(mut self, message: Message) -> Self { + self.on_mouse_exit = Some(message); + self + } +} + +/// Local state of the [`MouseListener`]. +#[derive(Default)] +struct State { + hovered: bool, +} + +impl<'a, Message, Renderer> MouseListener<'a, Message, Renderer> { + /// Creates an empty [`MouseListener`]. + pub fn new(content: impl Into>) -> Self { + MouseListener { + content: content.into(), + on_press: None, + on_release: None, + on_right_press: None, + on_right_release: None, + on_middle_press: None, + on_middle_release: None, + on_mouse_enter: None, + on_mouse_exit: None, + } + } +} + +impl<'a, Message, Renderer> Widget + for MouseListener<'a, Message, Renderer> +where + Renderer: crate::Renderer, + Message: Clone, +{ + fn children(&self) -> Vec { + vec![Tree::new(&self.content)] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(std::slice::from_ref(&self.content)); + } + + fn width(&self) -> Length { + self.content.as_widget().width() + } + + fn height(&self) -> Length { + self.content.as_widget().height() + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout( + renderer, + limits, + Widget::::width(self), + Widget::::height(self), + u32::MAX, + u32::MAX, + |renderer, limits| { + self.content.as_widget().layout(renderer, limits) + }, + ) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + operation: &mut dyn Operation, + ) { + operation.container(None, &mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + operation, + ); + }); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> 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, + ) { + return event::Status::Captured; + } + + update( + self, + &event, + layout, + cursor_position, + shell, + tree.state.downcast_mut::(), + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout.children().next().unwrap(), + cursor_position, + viewport, + renderer, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + renderer_style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + 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 self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option> { + self.content.as_widget().overlay( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + ) + } + + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::default()) + } +} + +impl<'a, Message, Renderer> From> + for Element<'a, Message, Renderer> +where + Message: 'a + Clone, + Renderer: 'a + crate::Renderer, +{ + fn from( + listener: MouseListener<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(listener) + } +} + +/// Processes the given [`Event`] and updates the [`State`] of an [`MouseListener`] +/// accordingly. +fn update( + widget: &mut MouseListener<'_, Message, Renderer>, + event: &Event, + layout: Layout<'_>, + cursor_position: Point, + shell: &mut Shell<'_, Message>, + state: &mut State, +) -> event::Status { + let hovered = state.hovered; + + if !layout.bounds().contains(cursor_position) { + if hovered { + state.hovered = false; + if let Some(message) = widget.on_mouse_exit.clone() { + shell.publish(message); + return event::Status::Captured; + } + } + + return event::Status::Ignored; + } + + state.hovered = true; + + if !hovered { + if let Some(message) = widget.on_mouse_enter.clone() { + shell.publish(message); + return event::Status::Captured; + } + } + + if let Some(message) = widget.on_press.clone() { + if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) = event + { + shell.publish(message); + return event::Status::Captured; + } + } + + if let Some(message) = widget.on_release.clone() { + if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) = event + { + shell.publish(message); + return event::Status::Captured; + } + } + + if let Some(message) = widget.on_right_press.clone() { + if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) = + event + { + shell.publish(message); + return event::Status::Captured; + } + } + + if let Some(message) = widget.on_right_release.clone() { + if let Event::Mouse(mouse::Event::ButtonReleased( + mouse::Button::Right, + )) = event + { + shell.publish(message); + return event::Status::Captured; + } + } + + if let Some(message) = widget.on_middle_press.clone() { + if let Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Middle, + )) = event + { + shell.publish(message); + return event::Status::Captured; + } + } + + if let Some(message) = widget.on_middle_release.clone() { + if let Event::Mouse(mouse::Event::ButtonReleased( + mouse::Button::Middle, + )) = event + { + shell.publish(message); + return event::Status::Captured; + } + } + + event::Status::Ignored +} + +/// Computes the layout of a [`MouseListener`]. +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) + .max_width(max_width) + .width(width) + .height(height); + + let content = layout_content(renderer, &limits); + let size = limits.resolve(content.size()); + + layout::Node::with_children(size, vec![content]) +} diff --git a/native/src/widget/pane_grid.rs b/native/src/widget/pane_grid.rs index 5de95c651f..cd941c607b 100644 --- a/native/src/widget/pane_grid.rs +++ b/native/src/widget/pane_grid.rs @@ -444,13 +444,13 @@ where } fn overlay<'b>( - &'b mut self, + &'b self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option> { self.contents - .iter_mut() + .iter() .zip(&mut tree.children) .zip(layout.children()) .filter_map(|(((_, pane), tree), layout)| { diff --git a/native/src/widget/pane_grid/content.rs b/native/src/widget/pane_grid/content.rs index 5f269d1f60..5e843cff74 100644 --- a/native/src/widget/pane_grid/content.rs +++ b/native/src/widget/pane_grid/content.rs @@ -305,12 +305,12 @@ where } pub(crate) fn overlay<'b>( - &'b mut self, + &'b self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option> { - if let Some(title_bar) = self.title_bar.as_mut() { + if let Some(title_bar) = self.title_bar.as_ref() { let mut children = layout.children(); let title_bar_layout = children.next()?; @@ -321,14 +321,14 @@ where match title_bar.overlay(title_bar_state, title_bar_layout, renderer) { Some(overlay) => Some(overlay), - None => self.body.as_widget_mut().overlay( + None => self.body.as_widget().overlay( body_state, children.next()?, renderer, ), } } else { - self.body.as_widget_mut().overlay( + self.body.as_widget().overlay( &mut tree.children[0], layout, renderer, diff --git a/native/src/widget/pane_grid/title_bar.rs b/native/src/widget/pane_grid/title_bar.rs index 28e4670f4f..115f6270da 100644 --- a/native/src/widget/pane_grid/title_bar.rs +++ b/native/src/widget/pane_grid/title_bar.rs @@ -395,7 +395,7 @@ where } pub(crate) fn overlay<'b>( - &'b mut self, + &'b self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, @@ -415,13 +415,13 @@ where let controls_state = states.next().unwrap(); content - .as_widget_mut() + .as_widget() .overlay(title_state, title_layout, renderer) .or_else(move || { - controls.as_mut().and_then(|controls| { + controls.as_ref().and_then(|controls| { let controls_layout = children.next()?; - controls.as_widget_mut().overlay( + controls.as_widget().overlay( controls_state, controls_layout, renderer, diff --git a/native/src/widget/pick_list.rs b/native/src/widget/pick_list.rs index 52cb1ad189..04a554371e 100644 --- a/native/src/widget/pick_list.rs +++ b/native/src/widget/pick_list.rs @@ -219,7 +219,7 @@ where } fn overlay<'b>( - &'b mut self, + &'b self, tree: &'b mut Tree, layout: Layout<'_>, _renderer: &Renderer, diff --git a/native/src/widget/row.rs b/native/src/widget/row.rs index c689ac13f2..eda7c2d355 100644 --- a/native/src/widget/row.rs +++ b/native/src/widget/row.rs @@ -229,12 +229,12 @@ where } fn overlay<'b>( - &'b mut self, + &'b self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option> { - overlay::from_children(&mut self.children, tree, layout, renderer) + overlay::from_children(&self.children, tree, layout, renderer) } } diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index a5e0e0e30c..ce29c184ed 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -276,13 +276,13 @@ where } fn overlay<'b>( - &'b mut self, + &'b self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option> { self.content - .as_widget_mut() + .as_widget() .overlay( &mut tree.children[0], layout.children().next().unwrap(), diff --git a/native/src/widget/tooltip.rs b/native/src/widget/tooltip.rs index 084dc269ee..9347a8868e 100644 --- a/native/src/widget/tooltip.rs +++ b/native/src/widget/tooltip.rs @@ -221,12 +221,12 @@ where } fn overlay<'b>( - &'b mut self, + &'b self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option> { - self.content.as_widget_mut().overlay( + self.content.as_widget().overlay( &mut tree.children[0], layout, renderer, diff --git a/native/src/window.rs b/native/src/window.rs index f910b8f21b..56fc5349ce 100644 --- a/native/src/window.rs +++ b/native/src/window.rs @@ -1,8 +1,16 @@ //! Build window-based GUI applications. mod action; mod event; +mod icon; +mod id; mod mode; +mod position; +mod settings; pub use action::Action; pub use event::Event; +pub use icon::Icon; +pub use id::Id; pub use mode::Mode; +pub use position::Position; +pub use settings::Settings; diff --git a/native/src/window/action.rs b/native/src/window/action.rs index da307e97c6..7b3cc2bf52 100644 --- a/native/src/window/action.rs +++ b/native/src/window/action.rs @@ -1,4 +1,4 @@ -use crate::window::Mode; +use crate::window::{Mode, Settings}; use iced_futures::MaybeSend; use std::fmt; @@ -13,6 +13,11 @@ pub enum Action { /// There’s no guarantee that this will work unless the left mouse /// button was pressed immediately before this function is called. Drag, + /// TODO(derezzedex) + Spawn { + /// TODO(derezzedex) + settings: Settings, + }, /// Resize the window. Resize { /// The new logical width of the window @@ -58,6 +63,7 @@ impl Action { match self { Self::Close => Action::Close, Self::Drag => Action::Drag, + Self::Spawn { settings } => Action::Spawn { settings }, Self::Resize { width, height } => Action::Resize { width, height }, Self::Maximize(bool) => Action::Maximize(bool), Self::Minimize(bool) => Action::Minimize(bool), @@ -75,9 +81,12 @@ impl fmt::Debug for Action { match self { Self::Close => write!(f, "Action::Close"), Self::Drag => write!(f, "Action::Drag"), + Self::Spawn { settings } => { + write!(f, "Action::Spawn {{ settings: {:?} }}", settings) + } Self::Resize { width, height } => write!( f, - "Action::Resize {{ widget: {}, height: {} }}", + "Action::Resize {{ width: {}, height: {} }}", width, height ), Self::Maximize(value) => write!(f, "Action::Maximize({})", value), diff --git a/native/src/window/icon.rs b/native/src/window/icon.rs new file mode 100644 index 0000000000..e89baf03eb --- /dev/null +++ b/native/src/window/icon.rs @@ -0,0 +1,12 @@ +//! Attach an icon to the window of your application. + +/// The icon of a window. +#[derive(Debug, Clone)] +pub struct Icon { + /// TODO(derezzedex) + pub rgba: Vec, + /// TODO(derezzedex) + pub width: u32, + /// TODO(derezzedex) + pub height: u32, +} diff --git a/native/src/window/id.rs b/native/src/window/id.rs new file mode 100644 index 0000000000..5060e162af --- /dev/null +++ b/native/src/window/id.rs @@ -0,0 +1,19 @@ +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +/// TODO(derezzedex) +pub struct Id(u64); + +impl Id { + /// TODO(derezzedex): maybe change `u64` to an enum `Type::{Single, Multi(u64)}` + pub const MAIN: Self = Id(0); + + /// TODO(derezzedex) + pub fn new(id: impl Hash) -> Id { + let mut hasher = DefaultHasher::new(); + id.hash(&mut hasher); + + Id(hasher.finish()) + } +} diff --git a/native/src/window/position.rs b/native/src/window/position.rs new file mode 100644 index 0000000000..c260c29eb1 --- /dev/null +++ b/native/src/window/position.rs @@ -0,0 +1,22 @@ +/// The position of a window in a given screen. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Position { + /// The platform-specific default position for a new window. + Default, + /// The window is completely centered on the screen. + Centered, + /// The window is positioned with specific coordinates: `(X, Y)`. + /// + /// When the decorations of the window are enabled, Windows 10 will add some + /// invisible padding to the window. This padding gets included in the + /// position. So if you have decorations enabled and want the window to be + /// at (0, 0) you would have to set the position to + /// `(PADDING_X, PADDING_Y)`. + Specific(i32, i32), +} + +impl Default for Position { + fn default() -> Self { + Self::Default + } +} diff --git a/native/src/window/settings.rs b/native/src/window/settings.rs new file mode 100644 index 0000000000..67798fbe8a --- /dev/null +++ b/native/src/window/settings.rs @@ -0,0 +1,52 @@ +use crate::window::{Icon, Position}; + +/// The window settings of an application. +#[derive(Debug, Clone)] +pub struct Settings { + /// The initial size of the window. + pub size: (u32, u32), + + /// The initial position of the window. + pub position: Position, + + /// The minimum size of the window. + pub min_size: Option<(u32, u32)>, + + /// The maximum size of the window. + pub max_size: Option<(u32, u32)>, + + /// Whether the window should be visible or not. + pub visible: bool, + + /// Whether the window should be resizable or not. + pub resizable: bool, + + /// Whether the window should have a border, a title bar, etc. or not. + pub decorations: bool, + + /// Whether the window should be transparent. + pub transparent: bool, + + /// Whether the window will always be on top of other windows. + pub always_on_top: bool, + + /// The icon of the window. + pub icon: Option, +} + +impl Default for Settings { + fn default() -> Settings { + Settings { + size: (1024, 768), + position: Position::default(), + min_size: None, + max_size: None, + visible: true, + resizable: true, + decorations: true, + transparent: false, + always_on_top: false, + icon: None, + } + } +} diff --git a/sctk/Cargo.toml b/sctk/Cargo.toml new file mode 100644 index 0000000000..ffcae66b1f --- /dev/null +++ b/sctk/Cargo.toml @@ -0,0 +1,37 @@ +[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_native/debug"] +system = ["sysinfo"] +application = [] +multi_window = [] + +[dependencies] +log = "0.4" +thiserror = "1.0" +sctk = { package = "smithay-client-toolkit", git = "https://github.com/Smithay/client-toolkit", rev = "3776d4a" } +raw-window-handle = "0.5.0" +enum-repr = "0.2.6" +futures = "0.3" +wayland-backend = {version = "0.1.0", features = ["client_system"]} +float-cmp = "0.9" + +[dependencies.iced_native] +features = ["wayland"] +path = "../native" + +[dependencies.iced_graphics] +features = ["opengl"] +path = "../graphics" + +[dependencies.iced_futures] +path = "../futures" + +[dependencies.sysinfo] +version = "0.26" +optional = true 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/application.rs b/sctk/src/application.rs new file mode 100644 index 0000000000..2f1b3cb6e4 --- /dev/null +++ b/sctk/src/application.rs @@ -0,0 +1,1418 @@ +use crate::{ + error::{self, Error}, + event_loop::{ + self, + control_flow::ControlFlow, + proxy, + state::{SctkState, SctkWindow}, + SctkEventLoop, + }, + sctk_event::{ + IcedSctkEvent, KeyboardEventVariant, LayerSurfaceEventVariant, + PopupEventVariant, SctkEvent, + }, + settings, Command, Debug, Executor, Runtime, Size, Subscription, commands::{layer_surface::get_layer_surface, window::get_window}, +}; +use float_cmp::approx_eq; +use futures::{channel::mpsc, task, Future, FutureExt, StreamExt}; +use iced_native::{ + application::{self, StyleSheet}, + clipboard::{self, Null}, + command::platform_specific::{self, wayland::popup}, + event::Status, + layout::Limits, + mouse::{self, Interaction}, + widget::{operation, Tree}, + Element, Renderer, Widget, +}; + +use sctk::{ + reexports::client::{Proxy, protocol::wl_surface::WlSurface}, + seat::{keyboard::Modifiers, pointer::PointerEventKind}, +}; +use std::{ + collections::HashMap, fmt, marker::PhantomData, hash::Hash, +}; +use wayland_backend::client::ObjectId; + +use iced_graphics::{compositor, renderer, window::{self, Compositor}, Color, Point, Viewport}; +use iced_native::user_interface::{self, UserInterface}; +use iced_native::window::Id as SurfaceId; +use std::mem::ManuallyDrop; +use raw_window_handle::{ + HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle, + WaylandDisplayHandle, WaylandWindowHandle +}; +#[derive(Debug)] +pub enum Event { + /// A normal sctk event + SctkEvent(IcedSctkEvent), + /// TODO + // Create a wrapper variant of `window::Event` type instead + // (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), + /// request sctk to set the cursor of the active pointer + SetCursor(Interaction), +} + +pub struct IcedSctkState; + +/// An interactive, native cross-platform application. +/// +/// This trait is the main entrypoint of 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: Sized +where + ::Theme: StyleSheet, +{ + /// The data needed to initialize your [`Application`]. + type Flags; + + /// The graphics backend to use to draw the [`Program`]. + type Renderer: Renderer; + + /// The type of __messages__ your [`Program`] will produce. + type Message: std::fmt::Debug + Send; + + /// Handles a __message__ and updates the state of the [`Program`]. + /// + /// 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 by shells. + fn update(&mut self, message: Self::Message) -> Command; + + /// Returns the widgets to display in the [`Application`]. + /// + /// These widgets can produce __messages__ based on user interaction. + fn view( + &self, + id: SurfaceIdWrapper, + ) -> Element<'_, Self::Message, Self::Renderer>; + + /// 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) -> String; + + /// Returns the current [`Theme`] of the [`Application`]. + fn theme(&self) -> ::Theme; + + /// Returns the [`Style`] variation of the [`Theme`]. + fn style( + &self, + ) -> <::Theme as StyleSheet>::Style { + Default::default() + } + + /// Returns the event `Subscription` for the current state of the + /// application. + /// + /// The messages produced by the `Subscription` will be handled by + /// [`update`](#tymethod.update). + /// + /// A `Subscription` will be kept alive as long as you keep returning it! + /// + /// By default, it returns an empty subscription. + fn subscription(&self) -> Subscription { + Subscription::none() + } + + /// Returns the scale factor of the [`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) -> f64 { + 1.0 + } + + /// Returns whether the [`Application`] should be terminated. + /// + /// By default, it returns `false`. + fn should_exit(&self) -> bool { + false + } + + /// TODO + fn close_requested(&self, id: SurfaceIdWrapper) -> Self::Message; +} + +pub struct SurfaceDisplayWrapper { + comp_surface: Option<::Surface>, + backend: wayland_backend::client::Backend, + wl_surface: WlSurface, +} + +unsafe impl HasRawDisplayHandle for SurfaceDisplayWrapper { + fn raw_display_handle(&self) -> RawDisplayHandle { + let mut display_handle = WaylandDisplayHandle::empty(); + display_handle.display = self.backend.display_ptr() as *mut _; + RawDisplayHandle::Wayland(display_handle) + } +} + +unsafe impl HasRawWindowHandle for SurfaceDisplayWrapper { + fn raw_window_handle(&self) -> RawWindowHandle { + let mut window_handle = WaylandWindowHandle::empty(); + window_handle.surface = self.wl_surface.id().as_ptr() as *mut _; + RawWindowHandle::Wayland(window_handle) + } +} + +/// 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: window::Compositor + 'static, + ::Theme: StyleSheet, + A::Flags: Clone, +{ + let mut debug = Debug::new(); + debug.startup_started(); + + let flags = settings.flags.clone(); + 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 = 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.state.connection.backend(); + let wrapper = SurfaceDisplayWrapper:: { comp_surface: None, backend: backend.clone(), wl_surface }; + + + #[allow(unsafe_code)] + let (compositor, renderer) = C::new(compositor_settings, Some(&wrapper)).unwrap(); + + let auto_size_surfaces = HashMap::new(); + + let surface_ids = Default::default(); + + let (mut sender, receiver) = mpsc::unbounded::>(); + + let compositor_surfaces = HashMap::new(); + let mut instance = Box::pin(run_instance::( + application, + compositor, + renderer, + runtime, + ev_proxy, + debug, + receiver, + compositor_surfaces, + surface_ids, + auto_size_surfaces, + // display, + // context, + // config, + backend, + init_command, + exit_on_close_request, + )); + + 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("Send event"); + + let poll = instance.as_mut().poll(&mut context); + + *control_flow = match poll { + task::Poll::Pending => ControlFlow::Wait, + task::Poll::Ready(_) => ControlFlow::ExitWithCode(1), + }; + }); + + Ok(()) +} + +fn subscription_map(e: A::Message) -> Event +where + A: Application + 'static, + E: Executor + 'static, + C: window::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 compositor_surfaces: HashMap< + SurfaceId, + SurfaceDisplayWrapper, + >, + mut surface_ids: HashMap, + mut auto_size_surfaces: HashMap, + backend: wayland_backend::client::Backend, + init_command: Command, + _exit_on_close_request: bool, // TODO Ashley +) -> Result<(), Error> +where + A: Application + 'static, + E: Executor + 'static, + C: window::Compositor + 'static, + ::Theme: StyleSheet, +{ + let mut cache = user_interface::Cache::default(); + + let mut states: HashMap> = HashMap::new(); + let mut interfaces = ManuallyDrop::new(HashMap::new()); + + { + run_command( + &application, + &mut cache, + None, + &mut renderer, + init_command, + &mut runtime, + &mut ev_proxy, + &mut debug, + || compositor.fetch_information(), + &mut auto_size_surfaces, + ); + } + runtime.track(application.subscription().map(subscription_map::)); + + let _mouse_interaction = mouse::Interaction::default(); + let mut events: Vec = Vec::new(); + let mut messages: Vec = Vec::new(); + debug.startup_finished(); + + // let mut current_context_window = init_id_inner; + + let mut kbd_surface_id: Option = None; + let mut mods = Modifiers::default(); + let mut destroyed_surface_ids: HashMap = + Default::default(); + + 'main: while let Some(event) = receiver.next().await { + match event { + IcedSctkEvent::NewEvents(_) => {} // TODO Ashley: Seems to be ignored in iced_winit so i'll ignore for now + IcedSctkEvent::UserEvent(message) => { + messages.push(message); + } + IcedSctkEvent::SctkEvent(event) => { + 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(Point::new( + variant.position.0 as f32, + variant.position.1 as f32, + )); + } + PointerEventKind::Leave { .. } => { + state.set_cursor_position(Point::new(-1.0, -1.0)); + } + PointerEventKind::Motion { .. } => { + state.set_cursor_position(Point::new( + variant.position.0 as f32, + variant.position.1 as f32, + )); + } + PointerEventKind::Press { .. } + | PointerEventKind::Release { .. } + | PointerEventKind::Axis { .. } => {} + } + } + SctkEvent::KeyboardEvent { variant, .. } => match variant { + KeyboardEventVariant::Leave(_) => { + kbd_surface_id.take(); + } + KeyboardEventVariant::Enter(object_id) => { + kbd_surface_id.replace(object_id.id()); + } + KeyboardEventVariant::Press(_) + | KeyboardEventVariant::Release(_) + | KeyboardEventVariant::Repeat(_) => {} + KeyboardEventVariant::Modifiers(mods) => { + if let Some(state) = kbd_surface_id + .as_ref() + .and_then(|id| surface_ids.get(&id)) + .and_then(|id| states.get_mut(&id.inner())) + { + state.modifiers = mods; + } + } + }, + SctkEvent::WindowEvent { variant, id } => match variant { + crate::sctk_event::WindowEventVariant::Created(id, native_id) => { + surface_ids.insert(id, SurfaceIdWrapper::Window(native_id)); + } + crate::sctk_event::WindowEventVariant::Close => { + if let Some(surface_id) = surface_ids.remove(&id.id()) { + // drop(compositor_surfaces.remove(&surface_id.inner())); + interfaces.remove(&surface_id.inner()); + states.remove(&surface_id.inner()); + messages.push(application.close_requested(surface_id)); + destroyed_surface_ids.insert(id.id(), surface_id); + // if exit_on_close_request && surface_id == init_id { + // break 'main; + // } + } + } + crate::sctk_event::WindowEventVariant::WmCapabilities(_) + | crate::sctk_event::WindowEventVariant::ConfigureBounds { .. } => {} + crate::sctk_event::WindowEventVariant::Configure( + configure, + wl_surface, + first, + ) => { + if let Some(id) = surface_ids.get(&id.id()) { + let new_size = configure.new_size.unwrap(); + + if !compositor_surfaces.contains_key(&id.inner()) { + let mut wrapper = SurfaceDisplayWrapper { + comp_surface: None, + backend: backend.clone(), + wl_surface + }; + let c_surface = compositor.create_surface(&wrapper); + wrapper.comp_surface.replace(c_surface); + compositor_surfaces.insert(id.inner(), wrapper); + } + if first { + let state = State::new(&application, *id); + + let user_interface = build_user_interface( + &application, + user_interface::Cache::default(), + &mut renderer, + state.logical_size(), + &mut debug, + *id, + &mut auto_size_surfaces, + &mut ev_proxy + ); + states.insert(id.inner(), state); + interfaces.insert(id.inner(), user_interface); + } + if let Some(state) = states.get_mut(&id.inner()) { + state.set_logical_size(new_size.0 as f64, new_size.1 as f64); + } + } + } + }, + SctkEvent::LayerSurfaceEvent { variant, id } => match variant { + LayerSurfaceEventVariant::Created(id, native_id) => { + surface_ids.insert(id, SurfaceIdWrapper::LayerSurface(native_id)); + } + LayerSurfaceEventVariant::Done => { + if let Some(surface_id) = surface_ids.remove(&id.id()) { + drop(compositor_surfaces.remove(&surface_id.inner())); + interfaces.remove(&surface_id.inner()); + states.remove(&surface_id.inner()); + messages.push(application.close_requested(surface_id)); + destroyed_surface_ids.insert(id.id(), surface_id); + // if exit_on_close_request && surface_id == init_id { + // break 'main; + // } + } + } + LayerSurfaceEventVariant::Configure(configure, wl_surface, first) => { + if let Some(id) = surface_ids.get(&id.id()) { + if !compositor_surfaces.contains_key(&id.inner()) { + let mut wrapper = SurfaceDisplayWrapper { + comp_surface: None, + backend: backend.clone(), + wl_surface + }; + let mut c_surface = compositor.create_surface(&wrapper); + compositor.configure_surface(&mut c_surface, configure.new_size.0, configure.new_size.1); + wrapper.comp_surface.replace(c_surface); + compositor_surfaces.insert(id.inner(), wrapper); + } + if first { + let state = State::new(&application, *id); + + let user_interface = build_user_interface( + &application, + user_interface::Cache::default(), + &mut renderer, + state.logical_size(), + &mut debug, + *id, + &mut auto_size_surfaces, + &mut ev_proxy + ); + states.insert(id.inner(), state); + interfaces.insert(id.inner(), user_interface); + } + 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, + ); + } + } + } + }, + SctkEvent::PopupEvent { + variant, + toplevel_id: _, + parent_id: _, + id, + } => match variant { + PopupEventVariant::Created(_, native_id) => { + surface_ids.insert(id.id(), SurfaceIdWrapper::Popup(native_id)); + } + PopupEventVariant::Done => { + if let Some(surface_id) = surface_ids.remove(&id.id()) { + drop(compositor_surfaces.remove(&surface_id.inner())); + interfaces.remove(&surface_id.inner()); + states.remove(&surface_id.inner()); + messages.push(application.close_requested(surface_id)); + destroyed_surface_ids.insert(id.id(), surface_id); + } + } + PopupEventVariant::WmCapabilities(_) => {} + PopupEventVariant::Configure(configure, wl_surface, first) => { + if let Some(id) = surface_ids.get(&id.id()) { + if !compositor_surfaces.contains_key(&id.inner()) { + let mut wrapper = SurfaceDisplayWrapper { + comp_surface: None, + backend: backend.clone(), + wl_surface + }; + let c_surface = compositor.create_surface(&wrapper); + wrapper.comp_surface.replace(c_surface); + compositor_surfaces.insert(id.inner(), wrapper); + } + if first { + let state = State::new(&application, *id); + + let user_interface = build_user_interface( + &application, + user_interface::Cache::default(), + &mut renderer, + state.logical_size(), + &mut debug, + *id, + &mut auto_size_surfaces, + &mut ev_proxy + ); + states.insert(id.inner(), state); + interfaces.insert(id.inner(), user_interface); + } + if let Some(state) = states.get_mut(&id.inner()) { + state.set_logical_size( + configure.width as f64, + configure.height as f64, + ); + } + } + } + PopupEventVariant::RepositionionedPopup { .. } => {} + PopupEventVariant::Size(width, height) => { + if let Some(id) = surface_ids.get(&id.id()) { + if let Some(state) = states.get_mut(&id.inner()) { + state.set_logical_size( + width as f64, + height as f64, + ); + } + } + }, + }, + // TODO forward these events to an application which requests them? + SctkEvent::NewOutput { .. } => { + } + SctkEvent::UpdateOutput { .. } => { + } + SctkEvent::RemovedOutput( ..) => { + } + SctkEvent::Frame(_) => { + // TODO if animations are running, request redraw here? + }, + SctkEvent::ScaleFactorChanged { + factor, + id, + inner_size: _, + } => { + if let Some(state) = surface_ids + .get(&id.id()) + .and_then(|id| states.get_mut(&id.inner())) + { + state.set_scale_factor(factor); + } + } + } + } + IcedSctkEvent::MainEventsCleared => { + let mut i = 0; + while i < events.len() { + let remove = match &events[i] { + SctkEvent::NewOutput { .. } + | SctkEvent::UpdateOutput { .. } + | SctkEvent::RemovedOutput(_) => true, + _ => false, + }; + if remove { + let event = 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, + || compositor.fetch_information(), + &mut auto_size_surfaces, + ); + + interfaces = ManuallyDrop::new(build_user_interfaces( + &application, + &mut renderer, + &mut debug, + &states, + pure_states, + &mut auto_size_surfaces, + &mut ev_proxy + )); + + if application.should_exit() { + break 'main; + } + } else { + let mut needs_redraw = false; + for (object_id, surface_id) in &surface_ids { + let mut filtered = Vec::with_capacity(events.len()); + + let mut i = 0; + while i < events.len() { + let has_kbd_focus = + kbd_surface_id.as_ref() == Some(object_id); + if event_is_for_surface( + &events[i], + object_id, + has_kbd_focus, + ) { + filtered.push(events.remove(i)); + } else { + i += 1; + } + } + let has_events = !filtered.is_empty(); + + let cursor_position = + match states.get(&surface_id.inner()) { + Some(s) => s.cursor_position(), + None => continue, + }; + debug.event_processing_started(); + let native_events: Vec<_> = filtered + .into_iter() + .flat_map(|e| { + e.to_native( + &mut mods, + &surface_ids, + &destroyed_surface_ids, + ) + }) + .collect(); + let (interface_state, statuses) = { + let user_interface = interfaces + .get_mut(&surface_id.inner()) + .unwrap(); + user_interface.update( + native_events.as_slice(), + cursor_position, + &mut renderer, + &mut Null, + &mut messages, + ) + }; + debug.event_processing_finished(); + for event in + native_events.into_iter().zip(statuses.into_iter()) + { + runtime.broadcast(event); + } + + if let Some((w, h, limits, dirty)) = auto_size_surfaces.remove(&surface_id) { + if dirty { + let state = + match states.get_mut(&surface_id.inner()) { + Some(s) => s, + None => continue, + }; + state.set_logical_size(w as f64, h as f64); + } + auto_size_surfaces.insert(surface_id.clone(), (w, h, limits, false)); + } + + // TODO ASHLEY if event is a configure which isn't a new size and has no other changes, don't redraw + if has_events || !messages.is_empty() + || matches!( + interface_state, + user_interface::State::Outdated + ) + { + needs_redraw = true; + ev_proxy.send_event(Event::SctkEvent( + IcedSctkEvent::RedrawRequested( + object_id.clone(), + ), + )); + } + } + if needs_redraw { + let mut pure_states: HashMap<_, _> = + ManuallyDrop::into_inner(interfaces) + .drain() + .map(|(id, interface)| { + (id, interface.into_cache()) + }) + .collect(); + + for (_object_id, surface_id) in &surface_ids { + let state = + match states.get_mut(&surface_id.inner()) { + Some(s) => 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, + || compositor.fetch_information(), + &mut auto_size_surfaces, + ); + + pure_states.insert(surface_id.inner(), cache); + + // Update state + state.synchronize(&application); + + if application.should_exit() { + break 'main; + } + } + interfaces = ManuallyDrop::new(build_user_interfaces( + &application, + &mut renderer, + &mut debug, + &states, + pure_states, + &mut auto_size_surfaces, + &mut ev_proxy + )); + } + } + events.clear(); + // clear the destroyed surfaces after they have been handled + destroyed_surface_ids.clear(); + } + IcedSctkEvent::RedrawRequested(object_id) => { + if let Some(( + native_id, + Some(wrapper), + Some(mut user_interface), + Some(state), + )) = surface_ids.get(&object_id).map(|id| { + let surface = compositor_surfaces.get_mut(&id.inner()); + let interface = interfaces.remove(&id.inner()); + let state = states.get_mut(&id.inner()); + (*id, surface, interface, state) + }) { + + debug.render_started(); + let comp_surface = match wrapper.comp_surface.as_mut() { + Some(s) => s, + None => continue, + }; + + if state.viewport_changed() { + let physical_size = state.physical_size(); + let logical_size = state.logical_size(); + compositor.configure_surface(comp_surface, physical_size.width, physical_size.height); + + debug.layout_started(); + user_interface = user_interface.relayout(logical_size, &mut renderer); + debug.layout_finished(); + + debug.draw_started(); + let new_mouse_interaction = user_interface.draw( + &mut renderer, + state.theme(), + &renderer::Style { + text_color: state.text_color(), + }, + state.cursor_position(), + ); + debug.draw_finished(); + ev_proxy.send_event(Event::SetCursor( + new_mouse_interaction, + )); + + let _ = interfaces + .insert(native_id.inner(), user_interface); + + state.viewport_changed = false; + } else { + debug.draw_started(); + let new_mouse_interaction = user_interface.draw( + &mut renderer, + state.theme(), + &renderer::Style { + text_color: state.text_color(), + }, + state.cursor_position(), + ); + debug.draw_finished(); + ev_proxy.send_event(Event::SetCursor( + new_mouse_interaction, + )); + interfaces.insert(native_id.inner(), user_interface); + } + + let _ = compositor.present( + &mut renderer, + comp_surface, + state.viewport(), + state.background_color(), + &debug.overlay(), + ); + + debug.render_finished(); + } + } + IcedSctkEvent::RedrawEventsCleared => { + // TODO + } + IcedSctkEvent::LoopDestroyed => todo!(), + } + } + + Ok(()) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum SurfaceIdWrapper { + LayerSurface(SurfaceId), + Window(SurfaceId), + Popup(SurfaceId), +} + +impl SurfaceIdWrapper { + pub fn inner(&self) -> SurfaceId { + match self { + SurfaceIdWrapper::LayerSurface(id) => *id, + SurfaceIdWrapper::Window(id) => *id, + SurfaceIdWrapper::Popup(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, + debug: &mut Debug, + id: SurfaceIdWrapper, + auto_size_surfaces: &mut HashMap, + ev_proxy: &mut proxy::Proxy>, +) -> UserInterface<'a, A::Message, A::Renderer> +where + ::Theme: StyleSheet, +{ + debug.view_started(); + let view = application.view(id); + debug.view_finished(); + + let size = if let Some((prev_w, prev_h, limits, dirty)) = auto_size_surfaces.remove(&id) { + let view = view.as_widget(); + let _state = view.state(); + // TODO would it be ok to diff against the current cache? + let _ = view.diff(&mut Tree::empty()); + let bounds = view.layout(renderer, &limits).bounds().size(); + let (w, h) = (bounds.width.ceil() as u32, bounds.height.ceil() as u32); + let dirty = dirty || w != prev_w || h != prev_h; + auto_size_surfaces.insert(id, (w, h, limits, dirty)); + if dirty { + match id { + SurfaceIdWrapper::LayerSurface(inner) => { + ev_proxy.send_event( + Event::LayerSurface( + iced_native::command::platform_specific::wayland::layer_surface::Action::Size { id: inner.clone(), width: Some(w), height: Some(h) }, + ) + ); + }, + SurfaceIdWrapper::Window(inner) => { + ev_proxy.send_event( + Event::Window( + iced_native::command::platform_specific::wayland::window::Action::Size { id: inner.clone(), width: w, height: h }, + ) + ); + }, + SurfaceIdWrapper::Popup(inner) => { + ev_proxy.send_event( + Event::Popup( + iced_native::command::platform_specific::wayland::popup::Action::Size { id: inner.clone(), width: w, height: h }, + ) + ); + }, + }; + } + + 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, + scale_factor: f64, + pub(crate) viewport: Viewport, + viewport_changed: bool, + cursor_position: Point, + modifiers: Modifiers, + theme: ::Theme, + appearance: application::Appearance, + application: PhantomData, +} + +impl State +where + ::Theme: application::StyleSheet, +{ + /// Creates a new [`State`] for the provided [`Application`] + pub fn new(application: &A, id: SurfaceIdWrapper) -> Self { + let title = application.title(); + let scale_factor = application.scale_factor(); + let theme = application.theme(); + let appearance = theme.appearance(&application.style()); + let viewport = Viewport::with_physical_size(Size::new(1, 1), 1.0); + + Self { + id, + title, + scale_factor, + viewport, + viewport_changed: true, + // TODO: Encode cursor availability in the type-system + cursor_position: Point::new(-1.0, -1.0), + modifiers: Modifiers::default(), + theme, + appearance, + application: PhantomData, + } + } + + /// Returns the current [`Viewport`] of the [`State`]. + pub fn viewport(&self) -> &Viewport { + &self.viewport + } + + /// 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, ulps = 2) || !approx_eq!(f32, h as f32, old_size.height, ulps = 2) { + self.viewport_changed = true; + self.viewport = Viewport::with_physical_size( + Size { + width: (w * self.scale_factor) as u32, + height: (h * self.scale_factor) as u32, + }, + self.scale_factor, + ); + } + } + + /// 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.scale_factor , ulps = 2) { + self.viewport_changed = true; + let logical_size = self.viewport.logical_size(); + self.viewport = Viewport::with_physical_size( + Size { + width: (logical_size.width as f64 * scale_factor) as u32, + height: (logical_size.height as f64 * scale_factor) as u32, + }, + self.scale_factor, + ); + } + } + + /// Returns the current cursor position of the [`State`]. + pub fn cursor_position(&self) -> Point { + self.cursor_position + } + + /// 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: Point) { + self.cursor_position = p; + } + + fn synchronize(&mut self, application: &A) { + // Update theme and appearance + self.theme = application.theme(); + 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: &mut Runtime< + E, + proxy::Proxy>, + Event, + >, + proxy: &mut proxy::Proxy>, + debug: &mut Debug, + messages: &mut Vec, + graphics_info: impl FnOnce() -> compositor::Information + Copy, + auto_size_surfaces: &mut HashMap, +) where + A: Application + 'static, + E: Executor + 'static, + C: window::Compositor + 'static, + ::Theme: StyleSheet, +{ + 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, + ); + } + + runtime.track(application.subscription().map(subscription_map::)); +} + +/// 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: &mut Runtime< + E, + proxy::Proxy>, + Event, + >, + proxy: &mut proxy::Proxy>, + debug: &mut Debug, + _graphics_info: impl FnOnce() -> compositor::Information + Copy, + auto_size_surfaces: &mut HashMap, + +) where + A: Application, + E: Executor, + ::Theme: StyleSheet, +{ + use iced_native::command; + use iced_native::system; + + for action in command.actions() { + 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(..) => { + todo!(); + } + clipboard::Action::Write(..) => { + todo!(); + } + }, + 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::Application(message)) + .expect("Send message to event loop") + }); + } + } + }, + command::Action::Widget(action) => { + let state = match state { + Some(s) => s, + None => continue, + }; + let id = &state.id; + + let mut current_cache = std::mem::take(cache); + let mut current_operation = Some(action.into_operation()); + + let mut user_interface = build_user_interface( + application, + current_cache, + renderer, + state.logical_size(), + debug, + id.clone(), // TODO: run the operation on every widget tree ? + auto_size_surfaces, + proxy + ); + + 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) => { + proxy.send_event(Event::SctkEvent( + IcedSctkEvent::UserEvent(message), + )); + } + operation::Outcome::Chain(next) => { + current_operation = Some(next); + } + } + } + + current_cache = user_interface.into_cache(); + *cache = current_cache; + } + 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(SurfaceIdWrapper::LayerSurface(builder.id)); + let _state = Widget::state(e.as_widget()); + e.as_widget().diff(&mut Tree::empty()); + let node = Widget::layout(e.as_widget(), renderer, &builder.size_limits); + let bounds = node.bounds(); + let w = bounds.width.ceil() as u32; + let h = bounds.height.ceil() 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(SurfaceIdWrapper::Window(builder.window_id)); + let _state = Widget::state(e.as_widget()); + e.as_widget().diff(&mut Tree::empty()); + let node = Widget::layout(e.as_widget(), renderer, &builder.size_limits); + let bounds = node.bounds(); + let w = bounds.width.ceil() as u32; + let h = bounds.height.ceil() as u32; + auto_size_surfaces.insert(SurfaceIdWrapper::Window(builder.window_id), (w, h, builder.size_limits, false)); + builder.iced_settings.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(SurfaceIdWrapper::Popup(popup.id)); + let _state = Widget::state(e.as_widget()); + e.as_widget().diff(&mut Tree::empty()); + let node = Widget::layout(e.as_widget(), renderer, &popup.positioner.size_limits); + let bounds = node.bounds(); + let w = bounds.width.ceil().ceil() as u32; + let h = bounds.height.ceil().ceil() as u32; + auto_size_surfaces.insert(SurfaceIdWrapper::Popup(popup.id), (w, h, popup.positioner.size_limits, false)); + popup.positioner.size = Some((bounds.width as u32, bounds.height as u32)); + } + proxy.send_event(Event::Popup(popup::Action::Popup{popup, _phantom})); + } else { + proxy.send_event(Event::Popup(popup_action)); + } + } + _ => {} + } + } +} + +pub fn build_user_interfaces<'a, A>( + application: &'a A, + renderer: &mut A::Renderer, + debug: &mut Debug, + states: &HashMap>, + mut pure_states: HashMap, + auto_size_surfaces: &mut HashMap, + ev_proxy: &mut proxy::Proxy>, +) -> HashMap< + SurfaceId, + UserInterface< + 'a, + ::Message, + ::Renderer, + >, +> +where + A: Application + 'static, + ::Theme: StyleSheet, +{ + let mut interfaces = HashMap::new(); + + for (id, pure_state) in pure_states.drain() { + let state = &states.get(&id).unwrap(); + + let user_interface = build_user_interface( + application, + pure_state, + renderer, + state.logical_size(), + 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::Frame(_) + | SctkEvent::NewOutput { .. } + | SctkEvent::UpdateOutput { .. } + | SctkEvent::RemovedOutput(_) => false, + SctkEvent::ScaleFactorChanged { id, .. } => &id.id() == object_id, + } +} diff --git a/sctk/src/commands/data_device.rs b/sctk/src/commands/data_device.rs new file mode 100644 index 0000000000..65199e4b9c --- /dev/null +++ b/sctk/src/commands/data_device.rs @@ -0,0 +1 @@ +//! Interact with the data device objects of your application. diff --git a/sctk/src/commands/layer_surface.rs b/sctk/src/commands/layer_surface.rs new file mode 100644 index 0000000000..9072546ac2 --- /dev/null +++ b/sctk/src/commands/layer_surface.rs @@ -0,0 +1,125 @@ +//! Interact with the window of your application. +use std::marker::PhantomData; + +use iced_native::command::platform_specific::wayland::layer_surface::IcedMargin; +use iced_native::window::Id as SurfaceId; +use iced_native::{ + command::{ + self, + platform_specific::{ + self, + wayland::{self, layer_surface::SctkLayerSurfaceSettings}, + }, + Command, + }, + window, +}; +pub use window::{Event, Mode}; + +pub use sctk::shell::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..3c40a938cd --- /dev/null +++ b/sctk/src/commands/mod.rs @@ -0,0 +1,6 @@ +//! Interact with the wayland objects of your application. + +pub mod data_device; +pub mod layer_surface; +pub mod popup; +pub mod window; diff --git a/sctk/src/commands/popup.rs b/sctk/src/commands/popup.rs new file mode 100644 index 0000000000..cc7a4f02d6 --- /dev/null +++ b/sctk/src/commands/popup.rs @@ -0,0 +1,58 @@ +//! Interact with the popups of your application. +use iced_native::command::{ + self, + platform_specific::{ + self, + wayland::{ + self, + popup::SctkPopupSettings, + }, + }, +}; +use iced_native::window::Id as SurfaceId; +use iced_native::{command::Command, window}; +pub use window::{Event, Mode}; + +/// +/// +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/window.rs b/sctk/src/commands/window.rs new file mode 100644 index 0000000000..9a5b880d63 --- /dev/null +++ b/sctk/src/commands/window.rs @@ -0,0 +1,85 @@ +//! Interact with the window of your application. +use std::marker::PhantomData; + +use crate::command::{self, Command}; +use iced_native::command::platform_specific::{ + self, + wayland::{self, window::SctkWindowSettings}, +}; +use iced_native::window; + +pub use window::Action; +pub use window::{Event, Mode}; + +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..8853d13b94 --- /dev/null +++ b/sctk/src/conversion.rs @@ -0,0 +1,259 @@ +use iced_native::{ + keyboard::{self, KeyCode}, + mouse::{self, ScrollDelta}, +}; +use sctk::{ + reexports::client::protocol::wl_pointer::AxisSource, + seat::{ + keyboard::Modifiers, + pointer::{AxisScroll, 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::Right) + } else { + button.try_into().ok().map(|b| mouse::Button::Other(b)) + } +} + +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: horizontal.discrete as f32, + y: vertical.discrete as f32, + }, + AxisSource::Finger | AxisSource::Continuous | _ => { + ScrollDelta::Pixels { + x: horizontal.absolute as f32, + y: 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: u32) -> Option { + use sctk::seat::keyboard::keysyms; + match keysym { + // Numbers. + keysyms::XKB_KEY_1 => Some(KeyCode::Key1), + keysyms::XKB_KEY_2 => Some(KeyCode::Key2), + keysyms::XKB_KEY_3 => Some(KeyCode::Key3), + keysyms::XKB_KEY_4 => Some(KeyCode::Key4), + keysyms::XKB_KEY_5 => Some(KeyCode::Key5), + keysyms::XKB_KEY_6 => Some(KeyCode::Key6), + keysyms::XKB_KEY_7 => Some(KeyCode::Key7), + keysyms::XKB_KEY_8 => Some(KeyCode::Key8), + keysyms::XKB_KEY_9 => Some(KeyCode::Key9), + keysyms::XKB_KEY_0 => Some(KeyCode::Key0), + // Letters. + keysyms::XKB_KEY_A | keysyms::XKB_KEY_a => Some(KeyCode::A), + keysyms::XKB_KEY_B | keysyms::XKB_KEY_b => Some(KeyCode::B), + keysyms::XKB_KEY_C | keysyms::XKB_KEY_c => Some(KeyCode::C), + keysyms::XKB_KEY_D | keysyms::XKB_KEY_d => Some(KeyCode::D), + keysyms::XKB_KEY_E | keysyms::XKB_KEY_e => Some(KeyCode::E), + keysyms::XKB_KEY_F | keysyms::XKB_KEY_f => Some(KeyCode::F), + keysyms::XKB_KEY_G | keysyms::XKB_KEY_g => Some(KeyCode::G), + keysyms::XKB_KEY_H | keysyms::XKB_KEY_h => Some(KeyCode::H), + keysyms::XKB_KEY_I | keysyms::XKB_KEY_i => Some(KeyCode::I), + keysyms::XKB_KEY_J | keysyms::XKB_KEY_j => Some(KeyCode::J), + keysyms::XKB_KEY_K | keysyms::XKB_KEY_k => Some(KeyCode::K), + keysyms::XKB_KEY_L | keysyms::XKB_KEY_l => Some(KeyCode::L), + keysyms::XKB_KEY_M | keysyms::XKB_KEY_m => Some(KeyCode::M), + keysyms::XKB_KEY_N | keysyms::XKB_KEY_n => Some(KeyCode::N), + keysyms::XKB_KEY_O | keysyms::XKB_KEY_o => Some(KeyCode::O), + keysyms::XKB_KEY_P | keysyms::XKB_KEY_p => Some(KeyCode::P), + keysyms::XKB_KEY_Q | keysyms::XKB_KEY_q => Some(KeyCode::Q), + keysyms::XKB_KEY_R | keysyms::XKB_KEY_r => Some(KeyCode::R), + keysyms::XKB_KEY_S | keysyms::XKB_KEY_s => Some(KeyCode::S), + keysyms::XKB_KEY_T | keysyms::XKB_KEY_t => Some(KeyCode::T), + keysyms::XKB_KEY_U | keysyms::XKB_KEY_u => Some(KeyCode::U), + keysyms::XKB_KEY_V | keysyms::XKB_KEY_v => Some(KeyCode::V), + keysyms::XKB_KEY_W | keysyms::XKB_KEY_w => Some(KeyCode::W), + keysyms::XKB_KEY_X | keysyms::XKB_KEY_x => Some(KeyCode::X), + keysyms::XKB_KEY_Y | keysyms::XKB_KEY_y => Some(KeyCode::Y), + keysyms::XKB_KEY_Z | keysyms::XKB_KEY_z => Some(KeyCode::Z), + // Escape. + keysyms::XKB_KEY_Escape => Some(KeyCode::Escape), + // Function keys. + keysyms::XKB_KEY_F1 => Some(KeyCode::F1), + keysyms::XKB_KEY_F2 => Some(KeyCode::F2), + keysyms::XKB_KEY_F3 => Some(KeyCode::F3), + keysyms::XKB_KEY_F4 => Some(KeyCode::F4), + keysyms::XKB_KEY_F5 => Some(KeyCode::F5), + keysyms::XKB_KEY_F6 => Some(KeyCode::F6), + keysyms::XKB_KEY_F7 => Some(KeyCode::F7), + keysyms::XKB_KEY_F8 => Some(KeyCode::F8), + keysyms::XKB_KEY_F9 => Some(KeyCode::F9), + keysyms::XKB_KEY_F10 => Some(KeyCode::F10), + keysyms::XKB_KEY_F11 => Some(KeyCode::F11), + keysyms::XKB_KEY_F12 => Some(KeyCode::F12), + keysyms::XKB_KEY_F13 => Some(KeyCode::F13), + keysyms::XKB_KEY_F14 => Some(KeyCode::F14), + keysyms::XKB_KEY_F15 => Some(KeyCode::F15), + keysyms::XKB_KEY_F16 => Some(KeyCode::F16), + keysyms::XKB_KEY_F17 => Some(KeyCode::F17), + keysyms::XKB_KEY_F18 => Some(KeyCode::F18), + keysyms::XKB_KEY_F19 => Some(KeyCode::F19), + keysyms::XKB_KEY_F20 => Some(KeyCode::F20), + keysyms::XKB_KEY_F21 => Some(KeyCode::F21), + keysyms::XKB_KEY_F22 => Some(KeyCode::F22), + keysyms::XKB_KEY_F23 => Some(KeyCode::F23), + keysyms::XKB_KEY_F24 => Some(KeyCode::F24), + // Flow control. + keysyms::XKB_KEY_Print => Some(KeyCode::Snapshot), + keysyms::XKB_KEY_Scroll_Lock => Some(KeyCode::Scroll), + keysyms::XKB_KEY_Pause => Some(KeyCode::Pause), + keysyms::XKB_KEY_Insert => Some(KeyCode::Insert), + keysyms::XKB_KEY_Home => Some(KeyCode::Home), + keysyms::XKB_KEY_Delete => Some(KeyCode::Delete), + keysyms::XKB_KEY_End => Some(KeyCode::End), + keysyms::XKB_KEY_Page_Down => Some(KeyCode::PageDown), + keysyms::XKB_KEY_Page_Up => Some(KeyCode::PageUp), + // Arrows. + keysyms::XKB_KEY_Left => Some(KeyCode::Left), + keysyms::XKB_KEY_Up => Some(KeyCode::Up), + keysyms::XKB_KEY_Right => Some(KeyCode::Right), + keysyms::XKB_KEY_Down => Some(KeyCode::Down), + + keysyms::XKB_KEY_BackSpace => Some(KeyCode::Backspace), + keysyms::XKB_KEY_Return => Some(KeyCode::Enter), + keysyms::XKB_KEY_space => Some(KeyCode::Space), + + keysyms::XKB_KEY_Multi_key => Some(KeyCode::Compose), + keysyms::XKB_KEY_caret => Some(KeyCode::Caret), + + // Keypad. + keysyms::XKB_KEY_Num_Lock => Some(KeyCode::Numlock), + keysyms::XKB_KEY_KP_0 => Some(KeyCode::Numpad0), + keysyms::XKB_KEY_KP_1 => Some(KeyCode::Numpad1), + keysyms::XKB_KEY_KP_2 => Some(KeyCode::Numpad2), + keysyms::XKB_KEY_KP_3 => Some(KeyCode::Numpad3), + keysyms::XKB_KEY_KP_4 => Some(KeyCode::Numpad4), + keysyms::XKB_KEY_KP_5 => Some(KeyCode::Numpad5), + keysyms::XKB_KEY_KP_6 => Some(KeyCode::Numpad6), + keysyms::XKB_KEY_KP_7 => Some(KeyCode::Numpad7), + keysyms::XKB_KEY_KP_8 => Some(KeyCode::Numpad8), + keysyms::XKB_KEY_KP_9 => Some(KeyCode::Numpad9), + // Misc. + // => Some(KeyCode::AbntC1), + // => Some(KeyCode::AbntC2), + keysyms::XKB_KEY_plus => Some(KeyCode::Plus), + keysyms::XKB_KEY_apostrophe => Some(KeyCode::Apostrophe), + // => Some(KeyCode::Apps), + keysyms::XKB_KEY_at => Some(KeyCode::At), + // => Some(KeyCode::Ax), + keysyms::XKB_KEY_backslash => Some(KeyCode::Backslash), + keysyms::XKB_KEY_XF86Calculator => Some(KeyCode::Calculator), + // => Some(KeyCode::Capital), + keysyms::XKB_KEY_colon => Some(KeyCode::Colon), + keysyms::XKB_KEY_comma => Some(KeyCode::Comma), + // => Some(KeyCode::Convert), + keysyms::XKB_KEY_equal => Some(KeyCode::Equals), + keysyms::XKB_KEY_grave => Some(KeyCode::Grave), + // => Some(KeyCode::Kana), + keysyms::XKB_KEY_Kanji => Some(KeyCode::Kanji), + keysyms::XKB_KEY_Alt_L => Some(KeyCode::LAlt), + keysyms::XKB_KEY_bracketleft => Some(KeyCode::LBracket), + keysyms::XKB_KEY_Control_L => Some(KeyCode::LControl), + keysyms::XKB_KEY_Shift_L => Some(KeyCode::LShift), + keysyms::XKB_KEY_Super_L => Some(KeyCode::LWin), + keysyms::XKB_KEY_XF86Mail => Some(KeyCode::Mail), + // => Some(KeyCode::MediaSelect), + // => Some(KeyCode::MediaStop), + keysyms::XKB_KEY_minus => Some(KeyCode::Minus), + keysyms::XKB_KEY_asterisk => Some(KeyCode::Asterisk), + keysyms::XKB_KEY_XF86AudioMute => Some(KeyCode::Mute), + // => Some(KeyCode::MyComputer), + keysyms::XKB_KEY_XF86AudioNext => Some(KeyCode::NextTrack), + // => Some(KeyCode::NoConvert), + keysyms::XKB_KEY_KP_Separator => Some(KeyCode::NumpadComma), + keysyms::XKB_KEY_KP_Enter => Some(KeyCode::NumpadEnter), + keysyms::XKB_KEY_KP_Equal => Some(KeyCode::NumpadEquals), + keysyms::XKB_KEY_KP_Add => Some(KeyCode::NumpadAdd), + keysyms::XKB_KEY_KP_Subtract => Some(KeyCode::NumpadSubtract), + keysyms::XKB_KEY_KP_Multiply => Some(KeyCode::NumpadMultiply), + keysyms::XKB_KEY_KP_Divide => Some(KeyCode::NumpadDivide), + keysyms::XKB_KEY_KP_Decimal => Some(KeyCode::NumpadDecimal), + keysyms::XKB_KEY_KP_Page_Up => Some(KeyCode::PageUp), + keysyms::XKB_KEY_KP_Page_Down => Some(KeyCode::PageDown), + keysyms::XKB_KEY_KP_Home => Some(KeyCode::Home), + keysyms::XKB_KEY_KP_End => Some(KeyCode::End), + keysyms::XKB_KEY_KP_Left => Some(KeyCode::Left), + keysyms::XKB_KEY_KP_Up => Some(KeyCode::Up), + keysyms::XKB_KEY_KP_Right => Some(KeyCode::Right), + keysyms::XKB_KEY_KP_Down => Some(KeyCode::Down), + // => Some(KeyCode::OEM102), + keysyms::XKB_KEY_period => Some(KeyCode::Period), + // => Some(KeyCode::Playpause), + keysyms::XKB_KEY_XF86PowerOff => Some(KeyCode::Power), + keysyms::XKB_KEY_XF86AudioPrev => Some(KeyCode::PrevTrack), + keysyms::XKB_KEY_Alt_R => Some(KeyCode::RAlt), + keysyms::XKB_KEY_bracketright => Some(KeyCode::RBracket), + keysyms::XKB_KEY_Control_R => Some(KeyCode::RControl), + keysyms::XKB_KEY_Shift_R => Some(KeyCode::RShift), + keysyms::XKB_KEY_Super_R => Some(KeyCode::RWin), + keysyms::XKB_KEY_semicolon => Some(KeyCode::Semicolon), + keysyms::XKB_KEY_slash => Some(KeyCode::Slash), + keysyms::XKB_KEY_XF86Sleep => Some(KeyCode::Sleep), + // => Some(KeyCode::Stop), + // => Some(KeyCode::Sysrq), + keysyms::XKB_KEY_Tab => Some(KeyCode::Tab), + keysyms::XKB_KEY_ISO_Left_Tab => Some(KeyCode::Tab), + keysyms::XKB_KEY_underscore => Some(KeyCode::Underline), + // => Some(KeyCode::Unlabeled), + keysyms::XKB_KEY_XF86AudioLowerVolume => Some(KeyCode::VolumeDown), + keysyms::XKB_KEY_XF86AudioRaiseVolume => Some(KeyCode::VolumeUp), + // => Some(KeyCode::Wake), + // => Some(KeyCode::Webback), + // => Some(KeyCode::WebFavorites), + // => Some(KeyCode::WebForward), + // => Some(KeyCode::WebHome), + // => Some(KeyCode::WebRefresh), + // => Some(KeyCode::WebSearch), + // => Some(KeyCode::WebStop), + keysyms::XKB_KEY_yen => Some(KeyCode::Yen), + keysyms::XKB_KEY_XF86Copy => Some(KeyCode::Copy), + keysyms::XKB_KEY_XF86Paste => Some(KeyCode::Paste), + keysyms::XKB_KEY_XF86Cut => Some(KeyCode::Cut), + // Fallback. + _ => None, + } +} 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/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..dd74aa10d1 --- /dev/null +++ b/sctk/src/event_loop/mod.rs @@ -0,0 +1,862 @@ +pub mod control_flow; +pub mod proxy; +pub mod state; + +use std::{ + collections::HashMap, + fmt::Debug, + time::{Duration, Instant}, +}; + +use crate::{ + application::Event, + sctk_event::{ + IcedSctkEvent, LayerSurfaceEventVariant, PopupEventVariant, SctkEvent, + StartCause, WindowEventVariant, + }, + settings, +}; + +use iced_native::command::platform_specific::{ + self, + wayland::{ + layer_surface::SctkLayerSurfaceSettings, window::SctkWindowSettings, + }, +}; +use sctk::{ + compositor::CompositorState, + event_loop::WaylandSource, + output::OutputState, + reexports::{ + calloop::{self, EventLoop}, + client::{ + globals::registry_queue_init, + protocol::wl_surface::WlSurface, ConnectError, Connection, + DispatchError, Proxy, + }, + }, + registry::RegistryState, + seat::SeatState, + shell::{ + layer::LayerShell, + xdg::{window::XdgWindowState, XdgShellState}, + }, + shm::ShmState, +}; +use wayland_backend::client::WaylandError; + +use self::{ + control_flow::ControlFlow, + state::{LayerSurfaceCreationError, SctkState}, +}; + +// impl SctkSurface { +// pub fn hash(&self) -> u64 { +// let hasher = DefaultHasher::new(); +// match self { +// SctkSurface::LayerSurface(s) => s.wl_surface().id().hash(.hash(&mut hasher)), +// SctkSurface::Window(s) => s.wl_surface().id().hash(.hash(&mut hasher)), +// SctkSurface::Popup(s) => s.wl_surface().id().hash(.hash(&mut hasher)), +// }; +// hasher.finish() +// } +// } + +#[derive(Debug, Default, Clone, Copy)] +pub struct Features { + // TODO +} + +#[derive(Debug)] +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, +} + +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); + todo!() + }) + .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(event_queue).unwrap(); + + 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(); + + 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: ShmState::bind(&globals, &qh) + .expect("wl_shm is not available"), + xdg_shell_state: XdgShellState::bind(&globals, &qh) + .expect("xdg shell is not available"), + xdg_window_state: XdgWindowState::bind(&globals, &qh), + layer_shell: LayerShell::bind(&globals, &qh).ok(), + + // data_device_manager_state: DataDeviceManagerState::new(), + queue_handle: qh, + loop_handle: loop_handle, + + cursor_surface: None, + multipool: None, + outputs: Vec::new(), + seats: Vec::new(), + windows: Vec::new(), + layer_surfaces: Vec::new(), + popups: Vec::new(), + kbd_focus: None, + window_compositor_updates: HashMap::new(), + sctk_events: Vec::new(), + popup_compositor_updates: Default::default(), + layer_surface_compositor_updates: Default::default(), + pending_user_events: Vec::new(), + token_ctr: 0, + }, + features: Default::default(), + event_loop_awakener: ping, + user_events_sender, + }) + } + + 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_native::window::Id, WlSurface), LayerSurfaceCreationError> + { + self.state.get_layer_surface(layer_surface) + } + + pub fn get_window( + &mut self, + settings: SctkWindowSettings, + ) -> (iced_native::window::Id, WlSurface) { + self.state.get_window(settings) + } + + 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 event_sink_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.state.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, + ) + } + } + } + + // 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 event_sink_back_buffer, + &mut self.state.sctk_events, + ); + + // Handle pending sctk events. + for event in event_sink_back_buffer.drain(..) { + match event { + SctkEvent::Frame(id) => sticky_exit_callback( + IcedSctkEvent::SctkEvent(SctkEvent::Frame(id)), + &self.state, + &mut control_flow, + &mut callback, + ), + 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::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(); + 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, + ); + } + } + 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.requested_size = (width, height); + layer_surface.surface.set_size(width.unwrap_or_default(), height.unwrap_or_default()); + + pending_redraws.push(layer_surface.surface.wl_surface().id()); + } + + }, + 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(_) => { + // TODO set cursor after cursor theming PR is merged + // https://github.com/Smithay/client-toolkit/pull/306 + } + Event::Window(action) => match action { + platform_specific::wayland::window::Action::Window { builder, _phantom } => { + 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, + ); + }, + platform_specific::wayland::window::Action::Size { id, width, height } => { + if let Some(window) = self.state.windows.iter_mut().find(|w| w.id == id) { + window.requested_size = Some((width, height)); + window.window.xdg_surface().set_window_geometry(0, 0, width.max(1) as i32, height.max(1) as i32); + // 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() { + prev_configure.new_size = Some((width, 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_mimimized(); + 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 { + iced_native::window::Mode::Windowed => { + window.window.unset_fullscreen(); + }, + iced_native::window::Mode::Fullscreen => { + window.window.set_fullscreen(None); + }, + iced_native::window::Mode::Hidden => { + window.window.set_mimimized(); + }, + } + 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, + ); + } + }, + // 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() + .find(|s| s.data.id == id) + { + // update geometry + sctk_popup.popup.xdg_surface().set_window_geometry(0, 0, width as i32, height as i32); + // update positioner + self.state.token_ctr += 1; + sctk_popup.data.positioner.set_size(width as i32, height as i32); + sctk_popup.popup.reposition(&sctk_popup.data.positioner, 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 { id } => {}, + }, + } + } + + // 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..be2e469fdc --- /dev/null +++ b/sctk/src/event_loop/proxy.rs @@ -0,0 +1,66 @@ +use iced_native::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..697a450a90 --- /dev/null +++ b/sctk/src/event_loop/state.rs @@ -0,0 +1,553 @@ +use std::{collections::HashMap, fmt::Debug}; + +use crate::{ + application::Event, + dpi::LogicalSize, + sctk_event::{SctkEvent, SurfaceCompositorUpdate, SurfaceUserRequest}, +}; + +use iced_native::{ + command::platform_specific::{ + self, + wayland::{ + layer_surface::{IcedMargin, IcedOutput, SctkLayerSurfaceSettings}, + popup::SctkPopupSettings, + window::SctkWindowSettings, + }, + }, + keyboard::Modifiers, + layout::Limits, + window, +}; +use sctk::{ + compositor::CompositorState, + error::GlobalError, + output::OutputState, + reexports::{ + calloop::LoopHandle, + client::{ + backend::ObjectId, + protocol::{ + wl_data_device::WlDataDevice, + wl_keyboard::WlKeyboard, + wl_output::WlOutput, + wl_pointer::WlPointer, + wl_seat::WlSeat, + wl_surface::{self, WlSurface}, + wl_touch::WlTouch, + }, + Connection, Proxy, QueueHandle, + }, + }, + registry::RegistryState, + seat::{keyboard::KeyEvent, SeatState}, + shell::{ + layer::{ + Anchor, KeyboardInteractivity, Layer, LayerShell, LayerSurface, + LayerSurfaceConfigure, + }, + xdg::{ + popup::{Popup, PopupConfigure}, + window::{ + Window, WindowConfigure, WindowDecorations, XdgWindowState, + }, + XdgPositioner, XdgShellState, XdgShellSurface, + }, + }, + shm::{multi::MultiPool, ShmState}, +}; + +#[derive(Debug, Clone)] +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) data_device: Option, + pub(crate) modifiers: Modifiers, +} + +#[derive(Debug, Clone)] +pub struct SctkWindow { + pub(crate) id: iced_native::window::Id, + pub(crate) window: Window, + pub(crate) requested_size: Option<(u32, u32)>, + pub(crate) current_size: Option<(u32, u32)>, + pub(crate) last_configure: Option, + /// Requests that SCTK window should perform. + pub(crate) pending_requests: + Vec>, +} + +#[derive(Debug, Clone)] +pub struct SctkLayerSurface { + pub(crate) id: iced_native::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>, +} + +#[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, +} + +#[derive(Debug)] +pub struct SctkPopupData { + pub(crate) id: iced_native::window::Id, + pub(crate) parent: SctkSurface, + pub(crate) toplevel: WlSurface, + pub(crate) positioner: XdgPositioner, +} + +/// Wrapper to carry sctk state. +#[derive(Debug)] +pub struct SctkState { + /// 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) 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 popup_compositor_updates: HashMap, + /// Window updates, which are coming from SCTK or the compositor, which require + /// calling back to the sctk's downstream. They are handled right in the event loop, + /// unlike the ones coming from buffers on the `WindowHandle`'s. + pub window_compositor_updates: HashMap, + /// Layer Surface 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 layer_surface_compositor_updates: + HashMap, + + /// A sink for window and device events that is being filled during dispatching + /// event loop and forwarded downstream afterwards. + pub(crate) sctk_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 + pub(crate) registry_state: RegistryState, + pub(crate) seat_state: SeatState, + pub(crate) output_state: OutputState, + pub(crate) compositor_state: CompositorState, + pub(crate) shm_state: ShmState, + pub(crate) xdg_shell_state: XdgShellState, + pub(crate) xdg_window_state: XdgWindowState, + pub(crate) layer_shell: Option, + + pub(crate) connection: Connection, + 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), +} + +impl SctkState +where + T: 'static + Debug, +{ + pub fn get_popup( + &mut self, + settings: SctkPopupSettings, + ) -> Result<(window::Id, WlSurface, WlSurface, WlSurface), PopupCreationError> + { + let limits = settings.positioner.size_limits; + + 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(|e| PopupCreationError::PositionerCreationFailed(e))?; + 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(|e| PopupCreationError::PopupCreationFailed(e))?; + 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(|e| PopupCreationError::PopupCreationFailed(e))?, + ) + } + 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(|e| PopupCreationError::PopupCreationFailed(e))?, + ) + } + }; + 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(); + 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(), + }); + + 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 { + iced_settings: + window::Settings { + size, + min_size, + max_size, + decorations, + transparent, + icon, + .. + }, + window_id, + app_id, + title, + parent, + .. + } = 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 mut builder = if let Some(app_id) = app_id { + Window::builder().app_id(app_id) + } else { + Window::builder() + }; + builder = if let Some(min_size) = min_size { + builder.min_size(min_size) + } else { + builder + }; + builder = if let Some(max_size) = max_size { + builder.max_size(max_size) + } else { + builder + }; + builder = if let Some(title) = title { + builder.title(title) + } else { + builder + }; + + // builder = if let Some(parent) = parent.and_then(|p| self.windows.iter().find(|w| w.window.wl_surface().id() == p)) { + // builder.parent(&parent.window) + // } else { + // builder + // }; + let window = builder + .decorations(if decorations { + WindowDecorations::RequestServer + } else { + WindowDecorations::RequestClient + }) + .map( + &self.queue_handle, + &self.xdg_shell_state, + &mut self.xdg_window_state, + wl_surface.clone(), + ) + .expect("failed to create window"); + window.xdg_surface().set_window_geometry( + 0, + 0, + size.0 as i32, + size.1 as i32, + ); + window.wl_surface().commit(); + self.windows.push(SctkWindow { + id: window_id, + window, + requested_size: Some(size), + current_size: Some((1, 1)), + last_configure: None, + pending_requests: Vec::new(), + }); + (window_id, wl_surface) + } + + pub fn get_layer_surface( + &mut self, + SctkLayerSurfaceSettings { + id, + layer, + keyboard_interactivity, + anchor, + output, + namespace, + margin, + size, + exclusive_zone, + .. + }: SctkLayerSurfaceSettings, + ) -> Result<(iced_native::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 mut builder = LayerSurface::builder() + .anchor(anchor) + .keyboard_interactivity(keyboard_interactivity) + .margin(margin.top, margin.right, margin.bottom, margin.left) + .size((size.0.unwrap_or_default(), size.1.unwrap_or_default())) + .namespace(namespace) + .exclusive_zone(exclusive_zone); + if let Some(wl_output) = wl_output { + builder = builder.output(&wl_output); + } + let layer_surface = builder + .map(&self.queue_handle, layer_shell, wl_surface.clone(), layer) + .map_err(|g_err| { + LayerSurfaceCreationError::LayerSurfaceCreationFailed(g_err) + })?; + 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(), + }); + Ok((id, wl_surface)) + } +} diff --git a/sctk/src/handlers/compositor.rs b/sctk/src/handlers/compositor.rs new file mode 100644 index 0000000000..f6938d9e01 --- /dev/null +++ b/sctk/src/handlers/compositor.rs @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MPL-2.0-only +use sctk::{ + compositor::CompositorHandler, + delegate_compositor, + reexports::client::{protocol::wl_surface, Connection, Proxy, QueueHandle}, +}; +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, + ) { + if let Some(w) = self + .windows + .iter() + .find(|w| w.window.wl_surface().id() == surface.id()) + { + if let Some(e) = + self.window_compositor_updates.get_mut(&surface.id()) + { + e.scale_factor = Some(new_factor) + } + } + if let Some(w) = self + .layer_surfaces + .iter() + .find(|w| w.surface.wl_surface().id() == surface.id()) + { + if let Some(e) = + self.layer_surface_compositor_updates.get_mut(&surface.id()) + { + e.scale_factor = Some(new_factor) + } + } + if let Some(w) = self + .popups + .iter() + .find(|w| w.popup.wl_surface().id() == surface.id()) + { + if let Some(e) = + self.popup_compositor_updates.get_mut(&surface.id()) + { + e.scale_factor = Some(new_factor) + } + } + } + + fn frame( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + surface: &wl_surface::WlSurface, + _time: u32, + ) { + self.sctk_events.push(SctkEvent::Frame(surface.clone())); + } +} + +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..e69de29bb2 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..e69de29bb2 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..e69de29bb2 diff --git a/sctk/src/handlers/data_device/mod.rs b/sctk/src/handlers/data_device/mod.rs new file mode 100644 index 0000000000..d7de6bb99a --- /dev/null +++ b/sctk/src/handlers/data_device/mod.rs @@ -0,0 +1 @@ +// TODO after merge diff --git a/sctk/src/handlers/mod.rs b/sctk/src/handlers/mod.rs new file mode 100644 index 0000000000..6356483934 --- /dev/null +++ b/sctk/src/handlers/mod.rs @@ -0,0 +1,37 @@ +// handlers +pub mod compositor; +pub mod data_device; +pub mod output; +pub mod seat; +pub mod shell; + +use sctk::{ + delegate_registry, delegate_shm, + output::OutputState, + registry::{ProvidesRegistryState, RegistryState}, + registry_handlers, + seat::SeatState, + shm::{ShmHandler, ShmState}, +}; +use std::fmt::Debug; + +use crate::event_loop::state::SctkState; + +impl ShmHandler for SctkState { + fn shm_state(&mut self) -> &mut ShmState { + &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..75069c22fb --- /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, reexports::client::Proxy}; +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..974065be7f --- /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, reexports::client::Proxy, + seat::keyboard::KeyboardHandler, +}; +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: &[u32], + ) { + 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.clone(), + }) + } + } + + 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..b5900e7b1e --- /dev/null +++ b/sctk/src/handlers/seat/pointer.rs @@ -0,0 +1,58 @@ +use crate::{event_loop::state::SctkState, sctk_event::SctkEvent}; +use sctk::{ + delegate_pointer, + seat::pointer::{PointerEventKind, PointerHandler}, +}; +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() == 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 { + 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(); + } + 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..9221b0199d --- /dev/null +++ b/sctk/src/handlers/seat/seat.rs @@ -0,0 +1,172 @@ +use crate::{ + event_loop::{state::SctkSeat, state::SctkState}, + sctk_event::{KeyboardEventVariant, SctkEvent, SeatEventVariant}, +}; +use iced_native::keyboard::Modifiers; +use sctk::{delegate_seat, reexports::client::Proxy, seat::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(), + }); + self.seats.push(SctkSeat { + seat, + kbd: None, + ptr: None, + touch: None, + data_device: None, + modifiers: Modifiers::default(), + kbd_focus: None, + ptr_focus: None, + last_ptr_press: None, + last_kbd_press: 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: None, + modifiers: Modifiers::default(), + kbd_focus: None, + ptr_focus: None, + last_ptr_press: None, + last_kbd_press: None, + }); + self.seats.last_mut().unwrap() + } + }; + // TODO data device + match capability { + sctk::seat::Capability::Keyboard => { + if let Ok((kbd, source)) = + self.seat_state.get_keyboard_with_repeat(qh, &seat, None) + { + self.sctk_events.push(SctkEvent::SeatEvent { + variant: SeatEventVariant::NewCapability( + capability, + kbd.id(), + ), + id: seat.clone(), + }); + let kbd_clone = kbd.clone(); + self.loop_handle + .insert_source(source, move |e, _, state| { + state.sctk_events.push(SctkEvent::KeyboardEvent { + variant: KeyboardEventVariant::Repeat(e), + kbd_id: kbd_clone.clone(), + seat_id: seat.clone(), + }); + }) + .expect("Failed to insert the repeating keyboard into the event loop"); + my_seat.kbd.replace(kbd); + } + } + sctk::seat::Capability::Pointer => { + if let Ok(ptr) = self.seat_state.get_pointer(qh, &seat) { + self.sctk_events.push(SctkEvent::SeatEvent { + variant: SeatEventVariant::NewCapability( + capability, + ptr.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.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/shell/layer.rs b/sctk/src/handlers/shell/layer.rs new file mode 100644 index 0000000000..6a5b0eff9d --- /dev/null +++ b/sctk/src/handlers/shell/layer.rs @@ -0,0 +1,114 @@ +use crate::{ + dpi::LogicalSize, + event_loop::state::SctkState, + sctk_event::{LayerSurfaceEventVariant, SctkEvent}, +}; +use sctk::{ + delegate_layer, + reexports::client::Proxy, + shell::layer::{Anchor, KeyboardInteractivity, LayerShellHandler}, +}; +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::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::layer::LayerSurface, + mut configure: sctk::shell::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.current_size.replace(LogicalSize::new( + 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.sctk_events + .push(SctkEvent::Frame(layer.surface.wl_surface().clone())); + } +} + +delegate_layer!(@ SctkState); + +/// 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..1bf4f0b009 --- /dev/null +++ b/sctk/src/handlers/shell/xdg_popup.rs @@ -0,0 +1,91 @@ +use crate::{ + commands::popup, + event_loop::state::{self, SctkState, SctkSurface}, + sctk_event::{PopupEventVariant, SctkEvent}, +}; +use sctk::{ + delegate_xdg_popup, reexports::client::Proxy, + 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.sctk_events + .push(SctkEvent::Frame(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..34b2dc0735 --- /dev/null +++ b/sctk/src/handlers/shell/xdg_window.rs @@ -0,0 +1,72 @@ +use crate::{ + event_loop::state::SctkState, + sctk_event::{SctkEvent, WindowEventVariant}, +}; +use sctk::{ + delegate_xdg_shell, delegate_xdg_window, shell::xdg::window::WindowHandler, +}; +use std::fmt::Debug; + +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 configure.new_size.is_none() { + configure.new_size = + Some(window.requested_size.unwrap_or((300, 500))); + }; + 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.sctk_events.push(SctkEvent::Frame(wl_surface.clone())); + } +} + +delegate_xdg_window!(@ SctkState); +delegate_xdg_shell!(@ SctkState); diff --git a/sctk/src/lib.rs b/sctk/src/lib.rs new file mode 100644 index 0000000000..2fab080ecb --- /dev/null +++ b/sctk/src/lib.rs @@ -0,0 +1,23 @@ +pub use iced_native::*; + +pub mod application; +pub mod commands; +pub mod conversion; +pub mod dpi; +pub mod error; +pub mod event_loop; +mod handlers; +pub mod result; +pub mod sctk_event; +pub mod settings; +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 settings::Settings; + +pub use iced_graphics::Viewport; +pub use iced_native::window::Position; 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 100644 index 0000000000..557445d82e --- /dev/null +++ b/sctk/src/sctk_event.rs @@ -0,0 +1,645 @@ +use std::{collections::HashMap, time::Instant}; + +use crate::{ + application::SurfaceIdWrapper, + conversion::{ + keysym_to_vkey, modifiers_to_native, pointer_axis_to_native, + pointer_button_to_native, + }, + dpi::{LogicalSize, PhysicalSize}, +}; +use iced_graphics::Point; +use iced_native::{ + event::{ + wayland::{self, LayerEvent, PopupEvent}, + PlatformSpecific, + }, + keyboard::{self, KeyCode}, + mouse, + window::{self, Id as SurfaceId}, +}; +use sctk::{ + output::OutputInfo, + reexports::client::{ + backend::ObjectId, + protocol::{ + wl_keyboard::WlKeyboard, + wl_output::WlOutput, + wl_pointer::WlPointer, + wl_seat::{self, WlSeat}, + wl_surface::WlSurface, + }, + Proxy, + }, + seat::{ + keyboard::{KeyEvent, Modifiers}, + pointer::{PointerEvent, PointerEventKind}, + Capability, + }, + shell::{ + layer::LayerSurfaceConfigure, + xdg::{popup::PopupConfigure, window::WindowConfigure}, + }, +}; + +#[derive(Debug, Clone)] +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), + + /// 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, +} + +#[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 + // + Frame(WlSurface), + ScaleFactorChanged { + factor: f64, + id: WlOutput, + inner_size: PhysicalSize, + }, +} + +#[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(Vec), + /// + ConfigureBounds { + width: u32, + height: u32, + }, + /// + Configure(WindowConfigure, WlSurface, bool), +} + +#[derive(Debug, Clone)] +pub enum PopupEventVariant { + /// Popup Created + Created(ObjectId, SurfaceId), + /// + Done, + /// + WmCapabilities(Vec), + /// + Configure(PopupConfigure, WlSurface, bool), + /// + RepositionionedPopup { token: u32 }, + /// size + Size(u32, u32), +} + +#[derive(Debug, Clone)] +pub enum LayerSurfaceEventVariant { + /// sent after creation of the layer surface + Created(ObjectId, SurfaceId), + /// + Done, + /// + Configure(LayerSurfaceConfigure, WlSurface, bool), +} + +/// 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, + + /// first + pub first: bool, + + /// New scale factor. + pub scale_factor: Option, + + /// Close the window. + pub close_window: bool, +} + +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_native::Event::Mouse(mouse::Event::CursorEntered)] + } + PointerEventKind::Leave { .. } => { + vec![iced_native::Event::Mouse(mouse::Event::CursorLeft)] + } + PointerEventKind::Motion { .. } => { + vec![iced_native::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_native::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_native::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_native::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()) + .map(|id| match id { + SurfaceIdWrapper::LayerSurface(_id) => { + iced_native::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::Layer( + LayerEvent::Unfocused, + surface, + id.inner(), + ), + ), + ) + } + SurfaceIdWrapper::Window(id) => { + iced_native::Event::Window( + *id, + window::Event::Unfocused, + ) + } + SurfaceIdWrapper::Popup(_id) => { + iced_native::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::Popup( + PopupEvent::Unfocused, + surface, + id.inner(), + ), + ), + ) + } + }) + .into_iter() + .chain([iced_native::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::Seat( + wayland::SeatEvent::Leave, + seat_id, + )), + )]) + .collect(), + KeyboardEventVariant::Enter(surface) => surface_ids + .get(&surface.id()) + .map(|id| match id { + SurfaceIdWrapper::LayerSurface(_id) => { + iced_native::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::Layer( + LayerEvent::Focused, + surface, + id.inner(), + ), + ), + ) + } + SurfaceIdWrapper::Window(id) => { + iced_native::Event::Window( + *id, + window::Event::Focused, + ) + } + SurfaceIdWrapper::Popup(_id) => { + iced_native::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::Popup( + PopupEvent::Focused, + surface, + id.inner(), + ), + ), + ) + } + }) + .into_iter() + .chain([iced_native::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::Seat( + wayland::SeatEvent::Enter, + seat_id, + )), + )]) + .collect(), + KeyboardEventVariant::Press(ke) => { + let mut skip_char = false; + + let mut events: Vec<_> = keysym_to_vkey(ke.keysym) + .map(|k| { + if k == KeyCode::Backspace { + skip_char = true; + } + iced_native::Event::Keyboard( + keyboard::Event::KeyPressed { + key_code: k, + modifiers: modifiers_to_native(*modifiers), + }, + ) + }) + .into_iter() + .collect(); + if !skip_char { + if let Some(s) = ke.utf8 { + let mut chars = s + .chars() + .map(|c| { + iced_native::Event::Keyboard( + keyboard::Event::CharacterReceived(c), + ) + }) + .collect(); + events.append(&mut chars); + } + } + events + } + KeyboardEventVariant::Repeat(ke) => { + let mut skip_char = false; + + let mut events: Vec<_> = keysym_to_vkey(ke.keysym) + .map(|k| { + if k == KeyCode::Backspace { + skip_char = true; + } + iced_native::Event::Keyboard( + keyboard::Event::KeyPressed { + key_code: k, + modifiers: modifiers_to_native(*modifiers), + }, + ) + }) + .into_iter() + .collect(); + if !skip_char { + if let Some(s) = ke.utf8 { + let mut chars = s + .chars() + .map(|c| { + iced_native::Event::Keyboard( + keyboard::Event::CharacterReceived(c), + ) + }) + .collect(); + events.append(&mut chars); + } + } + events + } + KeyboardEventVariant::Release(k) => keysym_to_vkey(k.keysym) + .map(|k| { + iced_native::Event::Keyboard( + keyboard::Event::KeyReleased { + key_code: k, + modifiers: modifiers_to_native(*modifiers), + }, + ) + }) + .into_iter() + .collect(), + KeyboardEventVariant::Modifiers(new_mods) => { + *modifiers = new_mods; + vec![iced_native::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_native::Event::Window( + id.inner(), + window::Event::CloseRequested, + ) + }) + .into_iter() + .collect(), + WindowEventVariant::WmCapabilities(_) => Default::default(), + WindowEventVariant::ConfigureBounds { .. } => { + Default::default() + } + WindowEventVariant::Configure(configure, surface, _) => { + if configure.is_resizing() { + let new_size = configure.new_size.unwrap(); + surface_ids + .get(&surface.id()) + .map(|id| { + iced_native::Event::Window( + id.inner(), + window::Event::Resized { + width: new_size.0, + height: new_size.1, + }, + ) + }) + .into_iter() + .collect() + } else { + Default::default() + } + } + }, + SctkEvent::LayerSurfaceEvent { + variant, + id: surface, + } => match variant { + LayerSurfaceEventVariant::Done => destroyed_surface_ids + .get(&surface.id()) + .map(|id| { + iced_native::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_native::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::Popup( + PopupEvent::Done, + surface, + id.inner(), + ), + ), + ) + }) + .into_iter() + .collect(), + PopupEventVariant::Created(_, _) => Default::default(), // TODO + PopupEventVariant::WmCapabilities(_) => Default::default(), // TODO + PopupEventVariant::Configure(_, _, _) => Default::default(), // TODO + PopupEventVariant::RepositionionedPopup { token } => { + Default::default() + } + PopupEventVariant::Size(_, _) => Default::default(), // TODO + } + } + SctkEvent::NewOutput { id, info } => { + Some(iced_native::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::Output( + wayland::OutputEvent::Created(info), + id, + )), + )) + .into_iter() + .collect() + } + SctkEvent::UpdateOutput { id, info } => { + vec![iced_native::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::Output( + wayland::OutputEvent::InfoUpdate(info), + id.clone(), + )), + )] + } + SctkEvent::RemovedOutput(id) => { + Some(iced_native::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::Output( + wayland::OutputEvent::Removed, + id.clone(), + )), + )) + .into_iter() + .collect() + } + SctkEvent::Frame(_) => Default::default(), + SctkEvent::ScaleFactorChanged { + factor, + id, + inner_size, + } => Default::default(), + } + } +} diff --git a/sctk/src/settings.rs b/sctk/src/settings.rs new file mode 100644 index 0000000000..0311d13904 --- /dev/null +++ b/sctk/src/settings.rs @@ -0,0 +1,32 @@ +use iced_native::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/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/softbuffer/Cargo.toml b/softbuffer/Cargo.toml new file mode 100644 index 0000000000..ed5cc01be9 --- /dev/null +++ b/softbuffer/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "iced_softbuffer" +version = "0.1.0" +authors = ["Jeremy Soller "] +edition = "2021" +description = "A softbuffer renderer for Iced" +license = "MIT AND OFL-1.1" +repository = "https://github.com/iced-rs/iced" + +[dependencies] +cosmic-text = "0.6" +lazy_static = "1.4" +log = "0.4" +raw-window-handle = "0.5" +raqote = { version = "0.8", default-features = false } +softbuffer = { git = "https://github.com/pop-os/softbuffer", rev = "8dcb6438b" } +[dependencies.iced_native] +path = "../native" + +[dependencies.iced_graphics] +path = "../graphics" +features = ["font-fallback", "font-icons"] + +[features] +default = [] +svg = ["iced_graphics/svg"] +image = ["iced_graphics/image"] + +[package.metadata.docs.rs] +rustdoc-args = ["--cfg", "docsrs"] +all-features = true diff --git a/softbuffer/README.md b/softbuffer/README.md new file mode 100644 index 0000000000..e656fd2e4c --- /dev/null +++ b/softbuffer/README.md @@ -0,0 +1,3 @@ +# `iced_softbuffer` + +Software rendering for Iced \ No newline at end of file diff --git a/softbuffer/src/backend.rs b/softbuffer/src/backend.rs new file mode 100644 index 0000000000..9d3e1684ec --- /dev/null +++ b/softbuffer/src/backend.rs @@ -0,0 +1,294 @@ +use cosmic_text::{ + Attrs, AttrsList, BufferLine, FontSystem, Metrics, SwashCache, Weight, +}; +#[cfg(feature = "image")] +use iced_graphics::image::raster; +use iced_graphics::image::storage; +#[cfg(feature = "svg")] +use iced_graphics::image::vector; +#[cfg(feature = "image")] +use iced_native::image; +#[cfg(feature = "svg")] +use iced_native::svg; +use iced_native::text; +use iced_native::{Font, Point, Size}; +use std::cell::RefCell; +use std::fmt; + +lazy_static::lazy_static! { + pub(crate) static ref FONT_SYSTEM: FontSystem = FontSystem::new(); +} + +/// An entry in some [`Storage`], +pub(crate) struct CpuEntry { + pub(crate) size: Size, + pub(crate) data: Vec, +} + +impl fmt::Debug for CpuEntry { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("CpuEntry") + .field("size", &self.size) + .finish() + } +} + +impl storage::Entry for CpuEntry { + /// The [`Size`] of the [`Entry`]. + fn size(&self) -> Size { + self.size + } +} + +/// Stores cached image data for use in rendering +#[derive(Debug)] +pub(crate) struct CpuStorage; + +impl storage::Storage for CpuStorage { + /// The type of an [`Entry`] in the [`Storage`]. + type Entry = CpuEntry; + + /// State provided to upload or remove a [`Self::Entry`]. + type State<'a> = (); + + /// Upload the image data of a [`Self::Entry`]. + fn upload( + &mut self, + width: u32, + height: u32, + data_u8: &[u8], + state: &mut Self::State<'_>, + ) -> Option { + let mut data = Vec::with_capacity(data_u8.len() / 4); + for chunk in data_u8.chunks_exact(4) { + data.push( + raqote::SolidSource::from_unpremultiplied_argb( + chunk[3], chunk[0], chunk[1], chunk[2], + ) + .to_u32(), + ); + } + Some(Self::Entry { + size: Size::new(width, height), + data, + }) + } + + /// Romve a [`Self::Entry`] from the [`Storage`]. + fn remove(&mut self, entry: &Self::Entry, state: &mut Self::State<'_>) { + // no-op + } +} + +pub struct Backend { + pub(crate) swash_cache: SwashCache<'static>, + #[cfg(feature = "image")] + pub(crate) raster_cache: RefCell>, + #[cfg(feature = "svg")] + pub(crate) vector_cache: RefCell>, +} + +impl Backend { + pub fn new() -> Self { + Self { + swash_cache: SwashCache::new(&FONT_SYSTEM), + #[cfg(feature = "image")] + raster_cache: RefCell::new(raster::Cache::default()), + #[cfg(feature = "svg")] + vector_cache: RefCell::new(vector::Cache::default()), + } + } + + pub(crate) fn cosmic_metrics_attrs( + &self, + size: f32, + font: &Font, + ) -> (Metrics, Attrs) { + //TODO: why is this conversion necessary? + let font_size = (size * 5.0 / 6.0) as i32; + + //TODO: how to properly calculate line height? + let line_height = size as i32; + + let attrs = match font { + Font::Default => Attrs::new().weight(Weight::NORMAL), + //TODO: support using the bytes field. Right now this is just a hack for libcosmic + Font::External { name, bytes } => match *name { + "Fira Sans Regular" => Attrs::new().weight(Weight::NORMAL), + "Fira Sans Light" => Attrs::new().weight(Weight::LIGHT), + "Fira Sans SemiBold" => Attrs::new().weight(Weight::SEMIBOLD), + _ => { + log::warn!("Unsupported font name {:?}", name); + Attrs::new() + } + }, + }; + + (Metrics::new(font_size, line_height), attrs) + } +} + +impl iced_graphics::backend::Backend for Backend { + fn trim_measurements(&mut self) { + // no-op + } +} + +impl iced_graphics::backend::Text for Backend { + const ICON_FONT: Font = Font::Default; + const CHECKMARK_ICON: char = '✓'; + const ARROW_DOWN_ICON: char = '⌄'; + + fn default_size(&self) -> u16 { + //TODO: get from settings + 16 + } + + fn measure( + &self, + content: &str, + size: f32, + font: Font, + bounds: Size, + ) -> (f32, f32) { + let (metrics, attrs) = self.cosmic_metrics_attrs(size, &font); + + //TODO: improve implementation + let mut buffer_line = BufferLine::new(content, AttrsList::new(attrs)); + let layout = buffer_line.layout( + &FONT_SYSTEM, + metrics.font_size, + bounds.width as i32, + ); + + let mut width = 0.0; + let mut height = 0.0; + for layout_line in layout.iter() { + for glyph in layout_line.glyphs.iter() { + let max_x = if glyph.level.is_rtl() { + glyph.x - glyph.w + } else { + glyph.x + glyph.w + }; + if max_x + 1.0 > width { + width = max_x + 1.0; + } + } + + height += metrics.line_height as f32; + } + (width, height) + } + + fn hit_test( + &self, + content: &str, + size: f32, + font: Font, + bounds: Size, + point: Point, + nearest_only: bool, + ) -> Option { + let (metrics, attrs) = self.cosmic_metrics_attrs(size, &font); + + //TODO: improve implementation + let mut buffer_line = BufferLine::new(content, AttrsList::new(attrs)); + let layout = buffer_line.layout( + &FONT_SYSTEM, + metrics.font_size, + bounds.width as i32, + ); + + // Find exact hit + if !nearest_only { + let mut line_y = 0.0; + for layout_line in layout.iter() { + if point.y > line_y + && point.y < line_y + metrics.line_height as f32 + { + for glyph in layout_line.glyphs.iter() { + let (min_x, max_x) = if glyph.level.is_rtl() { + (glyph.x - glyph.w, glyph.x) + } else { + (glyph.x, glyph.x + glyph.w) + }; + + if point.x > min_x && point.x < max_x { + return Some(text::Hit::CharOffset(glyph.start)); + } + } + } + + line_y += metrics.line_height as f32; + } + } + + // Find nearest + let mut nearest_opt = None; + let mut line_y = 0.0; + for layout_line in layout.iter() { + let center_y = line_y + metrics.line_height as f32 / 2.0; + + for glyph in layout_line.glyphs.iter() { + let (min_x, max_x) = if glyph.level.is_rtl() { + (glyph.x - glyph.w, glyph.x) + } else { + (glyph.x, glyph.x + glyph.w) + }; + + let center_x = (min_x + max_x) / 2.0; + let center = Point::new(center_x, center_y); + + let distance = center.distance(point); + let vector = point - center; + nearest_opt = match nearest_opt { + Some(( + nearest_offset, + nearest_vector, + nearest_distance, + )) => { + if distance < nearest_distance { + Some((glyph.start, vector, distance)) + } else { + Some(( + nearest_offset, + nearest_vector, + nearest_distance, + )) + } + } + None => Some((glyph.start, vector, distance)), + }; + } + + line_y += metrics.line_height as f32; + } + + match nearest_opt { + Some((offset, vector, _)) => { + Some(text::Hit::NearestCharOffset(offset, vector)) + } + None => None, + } + } +} + +#[cfg(feature = "image")] +impl iced_graphics::backend::Image for Backend { + fn dimensions(&self, handle: &image::Handle) -> Size { + let mut cache = self.raster_cache.borrow_mut(); + let memory = cache.load(handle); + + memory.dimensions() + } +} + +#[cfg(feature = "svg")] +impl iced_graphics::backend::Svg for Backend { + fn viewport_dimensions(&self, handle: &svg::Handle) -> Size { + let mut cache = self.vector_cache.borrow_mut(); + let svg = cache.load(handle); + + svg.viewport_dimensions() + } +} diff --git a/softbuffer/src/lib.rs b/softbuffer/src/lib.rs new file mode 100644 index 0000000000..d84250353a --- /dev/null +++ b/softbuffer/src/lib.rs @@ -0,0 +1,23 @@ +//! A [`softbuffer`] renderer for [`iced_native`]. +#![doc( + html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" +)] +#![cfg_attr(docsrs, feature(doc_cfg))] + +mod backend; +pub use self::backend::Backend; + +pub mod settings; +pub use self::settings::Settings; + +pub(crate) mod surface; + +pub mod native { + pub use crate::surface::draw_primitive; + pub use raqote; +} + +pub mod window; + +pub type Renderer = + iced_graphics::Renderer; diff --git a/softbuffer/src/settings.rs b/softbuffer/src/settings.rs new file mode 100644 index 0000000000..ac4a09c7e1 --- /dev/null +++ b/softbuffer/src/settings.rs @@ -0,0 +1,47 @@ +//! Configure a renderer. +pub use iced_graphics::Antialiasing; + +/// The settings of a [`Backend`]. +/// +/// [`Backend`]: crate::Backend +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Settings { + /// The bytes of the font that will be used by default. + /// + /// If `None` is provided, a default system font will be chosen. + pub default_font: Option<&'static [u8]>, + + /// The default size of text. + /// + /// By default, it will be set to 20. + pub default_text_size: u16, + + /// If enabled, spread text workload in multiple threads when multiple cores + /// are available. + /// + /// By default, it is disabled. + pub text_multithreading: bool, + + /// The antialiasing strategy that will be used for triangle primitives. + /// + /// By default, it is `None`. + pub antialiasing: Option, +} + +impl Settings { + /// Creates new [`Settings`] using environment configuration. + pub fn from_env() -> Self { + Settings::default() + } +} + +impl Default for Settings { + fn default() -> Settings { + Settings { + default_font: None, + default_text_size: 20, + text_multithreading: false, + antialiasing: None, + } + } +} diff --git a/softbuffer/src/surface.rs b/softbuffer/src/surface.rs new file mode 100644 index 0000000000..cddf3322b2 --- /dev/null +++ b/softbuffer/src/surface.rs @@ -0,0 +1,749 @@ +use crate::backend::{Backend, CpuStorage, FONT_SYSTEM}; + +use cosmic_text::{AttrsList, BufferLine, Metrics, SwashContent}; +use iced_graphics::alignment::{Horizontal, Vertical}; +#[cfg(feature = "svg")] +use iced_graphics::image::vector; +use iced_graphics::{Background, Gradient, Point, Primitive, Rectangle, Size}; +use raqote::{ + DrawOptions, DrawTarget, Image, IntPoint, IntRect, PathBuilder, + SolidSource, Source, StrokeStyle, Transform, Vector, +}; +use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; +use softbuffer::GraphicsContext; +use std::cmp; + +// A software rendering surface +pub struct Surface { + context: GraphicsContext, + width: u32, + height: u32, + buffer: Vec, +} + +impl Surface { + pub(crate) fn new( + window: &W, + ) -> Self { + let context = match unsafe { GraphicsContext::new(window, window) } { + Ok(ok) => ok, + Err(err) => panic!("failed to create softbuffer context: {}", err), + }; + Surface { + context, + width: 0, + height: 0, + buffer: Vec::new(), + } + } + + pub(crate) fn configure(&mut self, width: u32, height: u32) { + self.width = width; + self.height = height; + self.buffer = vec![0; self.width as usize * self.height as usize]; + } + + pub(crate) fn present( + &mut self, + renderer: &mut crate::Renderer, + scale_factor: f32, + background: iced_graphics::Color, + ) { + { + let mut draw_target = DrawTarget::from_backing( + self.width as i32, + self.height as i32, + self.buffer.as_mut_slice(), + ); + + draw_target.clear({ + let rgba = background.into_rgba8(); + SolidSource::from_unpremultiplied_argb( + rgba[3], rgba[0], rgba[1], rgba[2], + ) + }); + + let draw_options = DrawOptions { + // Default to antialiasing off, enable it when necessary + antialias: raqote::AntialiasMode::None, + ..Default::default() + }; + + // Having at least one clip fixes some font rendering issues + draw_target.push_clip_rect(IntRect::new( + IntPoint::new(0, 0), + IntPoint::new(self.width as i32, self.height as i32), + )); + + renderer.with_primitives(|backend, primitives| { + for primitive in primitives.iter() { + draw_primitive( + &mut draw_target, + &draw_options, + backend, + scale_factor, + primitive, + ); + } + }); + + draw_target.pop_clip(); + } + + self.context.set_buffer( + &self.buffer, + self.width as u16, + self.height as u16, + ); + } +} + +pub fn draw_primitive( + draw_target: &mut DrawTarget<&mut [u32]>, + draw_options: &DrawOptions, + backend: &mut Backend, + scale_factor: f32, + primitive: &Primitive, +) { + let scale_size = |size: f32, align: bool| -> f32 { + if align { + (size * scale_factor).round() + } else { + size * scale_factor + } + }; + let scale_rect = |rect: &Rectangle, align: bool| -> Rectangle { + Rectangle::new( + Point::new(scale_size(rect.x, align), scale_size(rect.y, align)), + Size::new( + scale_size(rect.width, align), + scale_size(rect.height, align), + ), + ) + }; + + match primitive { + Primitive::None => (), + Primitive::Group { primitives } => { + for child in primitives.iter() { + draw_primitive( + draw_target, + draw_options, + backend, + scale_factor, + child, + ); + } + } + Primitive::Text { + content, + bounds, + color, + size, + font, + horizontal_alignment, + vertical_alignment, + } => { + // Apply scaling + //TODO: align to integers? + let bounds = scale_rect(bounds, false); + + let cosmic_color = { + let rgba8 = color.into_rgba8(); + cosmic_text::Color::rgba(rgba8[0], rgba8[1], rgba8[2], rgba8[3]) + }; + + let (metrics_unscaled, attrs) = + backend.cosmic_metrics_attrs(*size, &font); + // Scale metrics separately to avoid errors + //TODO: fix this by knowing correct scale when measuring text and doing hit test + let metrics = Metrics::new( + ((metrics_unscaled.font_size as f32) * scale_factor) as i32, + ((metrics_unscaled.line_height as f32) * scale_factor) as i32, + ); + + /* + // Debug bounds in green + let mut pb = PathBuilder::new(); + pb.move_to(bounds.x, bounds.y); + pb.line_to(bounds.x + bounds.width, bounds.y); + pb.line_to(bounds.x + bounds.width, bounds.y + bounds.height); + pb.line_to(bounds.x, bounds.y + bounds.height); + pb.close(); + let path = pb.finish(); + draw_target.stroke( + &path, + &Source::Solid(SolidSource::from_unpremultiplied_argb(0xFF, 0, 0xFF, 0)), + &StrokeStyle::default(), + draw_options + ); + */ + + //TODO: improve implementation + let mut buffer_line = + BufferLine::new(content, AttrsList::new(attrs)); + let layout = buffer_line.layout( + &FONT_SYSTEM, + metrics.font_size, + bounds.width as i32, + ); + + let mut line_y = match vertical_alignment { + Vertical::Top => bounds.y as i32 + metrics.font_size, + Vertical::Center => { + //TODO: why is this so weird? + bounds.y as i32 + metrics.font_size + - metrics.line_height * layout.len() as i32 / 2 + } + Vertical::Bottom => { + //TODO: why is this so weird? + bounds.y as i32 + metrics.font_size + - metrics.line_height * layout.len() as i32 + } + }; + + let mut line_width = 0.0; + for layout_line in layout.iter() { + for glyph in layout_line.glyphs.iter() { + let max_x = if glyph.level.is_rtl() { + glyph.x - glyph.w + } else { + glyph.x + glyph.w + }; + if max_x + 1.0 > line_width { + line_width = max_x + 1.0; + } + } + } + + let line_x = match horizontal_alignment { + Horizontal::Left => bounds.x as i32, + Horizontal::Center => { + //TODO: why is this so weird? + bounds.x as i32 - (line_width / 2.0) as i32 + } + Horizontal::Right => { + //TODO: why is this so weird? + bounds.x as i32 - line_width as i32 + } + }; + + /* + eprintln!( + "{:?}: {}, {}, {}, {} in {:?} from font size {}, {:?}, {:?}", + content, + line_x, line_y, + line_width, metrics.line_height, + bounds, + *size, + horizontal_alignment, + vertical_alignment + ); + */ + + for layout_line in layout.iter() { + /* + // Debug line placement in blue + let mut pb = PathBuilder::new(); + pb.move_to(line_x as f32, line_y as f32 - metrics.font_size as f32); + pb.line_to(line_x as f32 + line_width, line_y as f32 - metrics.font_size as f32); + pb.line_to(line_x as f32 + line_width, line_y as f32 + metrics.line_height as f32 - metrics.font_size as f32); + pb.line_to(line_x as f32, line_y as f32 + metrics.line_height as f32 - metrics.font_size as f32); + pb.close(); + let path = pb.finish(); + draw_target.stroke( + &path, + &Source::Solid(SolidSource::from_unpremultiplied_argb(0xFF, 0, 0, 0xFF)), + &StrokeStyle::default(), + draw_options + ); + */ + + //TODO: also clip y, it does not seem to work though because + // bounds.height < metrics.line_height * layout_lines.len() + draw_target.push_clip_rect(IntRect::new( + IntPoint::new(line_x, 0), + IntPoint::new( + line_x + .checked_add(bounds.width as i32) + .unwrap_or_else(i32::max_value), + i32::max_value(), + ), + )); + + for glyph in layout_line.glyphs.iter() { + let (cache_key, x_int, y_int) = + (glyph.cache_key, glyph.x_int, glyph.y_int); + + let glyph_color = match glyph.color_opt { + Some(some) => some, + None => cosmic_color, + }; + + if let Some(image) = + backend.swash_cache.get_image(cache_key) + { + let x = line_x + x_int + image.placement.left; + let y = line_y + y_int + -image.placement.top; + + /* + // Debug glyph placement in red + let mut pb = PathBuilder::new(); + pb.move_to(x as f32, y as f32); + pb.line_to(x as f32 + image.placement.width as f32, y as f32); + pb.line_to(x as f32 + image.placement.width as f32, y as f32 + image.placement.height as f32); + pb.line_to(x as f32, y as f32 + image.placement.height as f32); + pb.close(); + let path = pb.finish(); + draw_target.stroke( + &path, + &Source::Solid(SolidSource::from_unpremultiplied_argb(0xFF, 0xFF, 0, 0)), + &StrokeStyle::default(), + draw_options + ); + */ + + let mut image_data = Vec::with_capacity( + image.placement.height as usize + * image.placement.width as usize, + ); + match image.content { + SwashContent::Mask => { + let mut i = 0; + for _off_y in 0..image.placement.height as i32 { + for _off_x in + 0..image.placement.width as i32 + { + //TODO: blend base alpha? + image_data.push( + SolidSource::from_unpremultiplied_argb( + image.data[i], + glyph_color.r(), + glyph_color.g(), + glyph_color.b(), + ).to_u32() + ); + i += 1; + } + } + } + SwashContent::Color => { + let mut i = 0; + for _off_y in 0..image.placement.height as i32 { + for _off_x in + 0..image.placement.width as i32 + { + //TODO: blend base alpha? + image_data.push( + SolidSource::from_unpremultiplied_argb( + image.data[i + 3], + image.data[i + 0], + image.data[i + 1], + image.data[i + 2], + ).to_u32() + ); + i += 4; + } + } + } + SwashContent::SubpixelMask => { + eprintln!("Content::SubpixelMask"); + } + } + + if !image_data.is_empty() { + draw_target.draw_image_at( + x as f32, + y as f32, + &Image { + width: image.placement.width as i32, + height: image.placement.height as i32, + data: &image_data, + }, + &draw_options, + ); + } + } + } + + draw_target.pop_clip(); + + line_y += metrics.line_height; + } + } + Primitive::Quad { + bounds, + background, + border_radius, + border_width, + border_color, + } => { + // Apply scaling + //TODO: align to integers? + let bounds = scale_rect(bounds, false); + let border_radius = [ + scale_size(border_radius[0], false), + scale_size(border_radius[1], false), + scale_size(border_radius[2], false), + scale_size(border_radius[3], false), + ]; + let border_width = scale_size(*border_width, false); + + // Ensure radius is not too large + let clamp_radius = |radius: f32| -> f32 { + if radius > bounds.width / 2.0 { + return bounds.width / 2.0; + } + + if radius > bounds.height / 2.0 { + return bounds.height / 2.0; + } + + radius + }; + + let mut pb = PathBuilder::new(); + + let top_left = clamp_radius(border_radius[0]); + let top_right = clamp_radius(border_radius[1]); + let bottom_right = clamp_radius(border_radius[2]); + let bottom_left = clamp_radius(border_radius[3]); + + // Move to top left corner at start of clockwise arc + pb.move_to(bounds.x, bounds.y + top_left); + pb.arc( + bounds.x + top_left, + bounds.y + top_left, + top_left, + 180.0f32.to_radians(), + 90.0f32.to_radians(), + ); + + // Move to top right corner at start of clockwise arc + pb.line_to(bounds.x + bounds.width - top_right, bounds.y); + pb.arc( + bounds.x + bounds.width - top_right, + bounds.y + top_right, + top_right, + 270.0f32.to_radians(), + 90.0f32.to_radians(), + ); + + // Move to bottom right corner at start of clockwise arc + pb.line_to( + bounds.x + bounds.width, + bounds.y + bounds.height - bottom_right, + ); + pb.arc( + bounds.x + bounds.width - bottom_right, + bounds.y + bounds.height - bottom_right, + bottom_right, + 0.0f32.to_radians(), + 90.0f32.to_radians(), + ); + + // Move to bottom left corner at start of clockwise arc + pb.line_to(bounds.x + bottom_left, bounds.y + bounds.height); + pb.arc( + bounds.x + bottom_left, + bounds.y + bounds.height - bottom_left, + bottom_left, + 90.0f32.to_radians(), + 90.0f32.to_radians(), + ); + + // Close and finish path + pb.close(); + let path = pb.finish(); + + let background_source = match background { + Background::Color(color) => { + let rgba = color.into_rgba8(); + Source::Solid(SolidSource::from_unpremultiplied_argb( + rgba[3], rgba[0], rgba[1], rgba[2], + )) + } + }; + + draw_target.fill( + &path, + &background_source, + &DrawOptions { + // Anti-alias rounded rectangles + antialias: raqote::AntialiasMode::Gray, + ..*draw_options + }, + ); + + let border_source = { + let rgba = border_color.into_rgba8(); + Source::Solid(SolidSource::from_unpremultiplied_argb( + rgba[3], rgba[0], rgba[1], rgba[2], + )) + }; + + let style = StrokeStyle { + width: border_width, + ..Default::default() + }; + + draw_target.stroke( + &path, + &border_source, + &style, + &DrawOptions { + // Anti-alias rounded rectangles + antialias: raqote::AntialiasMode::Gray, + ..*draw_options + }, + ); + } + Primitive::Image { handle, bounds } => { + // Apply scaling + //TODO: align to integers? + let bounds = scale_rect(bounds, false); + + #[cfg(feature = "image")] + match backend.raster_cache.borrow_mut().upload( + handle, + &mut (), + &mut CpuStorage, + ) { + Some(entry) => { + draw_target.draw_image_with_size_at( + bounds.width, + bounds.height, + bounds.x, + bounds.y, + &Image { + width: entry.size.width as i32, + height: entry.size.height as i32, + data: &entry.data, + }, + draw_options, + ); + } + None => (), + } + } + Primitive::Svg { + handle, + bounds, + color, + } => { + // Apply scaling + //TODO: align to integers? + let bounds = scale_rect(bounds, false); + + #[cfg(feature = "svg")] + match backend.vector_cache.borrow_mut().upload( + handle, + color.clone(), + [bounds.width, bounds.height], + 1.0, /*TODO: what should scale be?*/ + &mut (), + &mut CpuStorage, + ) { + Some(entry) => { + draw_target.draw_image_with_size_at( + bounds.width, + bounds.height, + bounds.x, + bounds.y, + &Image { + width: entry.size.width as i32, + height: entry.size.height as i32, + data: &entry.data, + }, + draw_options, + ); + } + None => (), + } + } + Primitive::Clip { bounds, content } => { + // Apply scaling + //TODO: align to integers? + let bounds = scale_rect(bounds, false); + + draw_target.push_clip_rect(IntRect::new( + IntPoint::new(bounds.x as i32, bounds.y as i32), + IntPoint::new( + (bounds.x + bounds.width) as i32, + (bounds.y + bounds.height) as i32, + ), + )); + draw_primitive( + draw_target, + draw_options, + backend, + scale_factor, + &content, + ); + draw_target.pop_clip(); + } + Primitive::Translate { + translation, + content, + } => { + // Apply scaling + //TODO: align to integers? + let translation = Vector::new( + scale_size(translation.x, false), + scale_size(translation.y, false), + ); + + let transform = draw_target.get_transform().clone(); + draw_target.set_transform(&transform.then_translate(translation)); + + draw_primitive( + draw_target, + draw_options, + backend, + scale_factor, + &content, + ); + + draw_target.set_transform(&transform); + } + Primitive::GradientMesh { + buffers, + size, + gradient, + } => { + let source = match gradient { + Gradient::Linear(linear) => { + let mut stops = Vec::new(); + for stop in linear.color_stops.iter() { + let rgba8 = stop.color.into_rgba8(); + stops.push(raqote::GradientStop { + position: stop.offset, + color: raqote::Color::new( + rgba8[3], rgba8[0], rgba8[1], rgba8[2], + ), + }); + } + Source::new_linear_gradient( + raqote::Gradient { stops }, + raqote::Point::new(linear.start.x, linear.start.y), + raqote::Point::new(linear.end.x, linear.end.y), + raqote::Spread::Pad, /*TODO: which spread?*/ + ) + } + }; + + /* + draw_target.push_clip_rect(IntRect::new( + IntPoint::new(0, 0), + IntPoint::new(size.width as i32, size.height as i32), + )); + */ + + let mut pb = PathBuilder::new(); + + for indices in buffers.indices.chunks_exact(3) { + let a = &buffers.vertices[indices[0] as usize]; + let b = &buffers.vertices[indices[1] as usize]; + let c = &buffers.vertices[indices[2] as usize]; + + // Scaling is applied here + //TODO: align to integers? + pb.move_to( + scale_size(a.position[0], false), + scale_size(a.position[1], false), + ); + pb.line_to( + scale_size(b.position[0], false), + scale_size(b.position[1], false), + ); + pb.line_to( + scale_size(c.position[0], false), + scale_size(c.position[1], false), + ); + pb.close(); + } + + let path = pb.finish(); + draw_target.fill(&path, &source, draw_options); + + /* + draw_target.pop_clip(); + */ + } + Primitive::SolidMesh { buffers, size } => { + fn undo_linear_component(linear: f32) -> f32 { + if linear < 0.0031308 { + linear * 12.92 + } else { + 1.055 * linear.powf(1.0 / 2.4) - 0.055 + } + } + + fn linear_to_rgba8(color: &[f32; 4]) -> [u8; 4] { + let r = undo_linear_component(color[0]) * 255.0; + let g = undo_linear_component(color[1]) * 255.0; + let b = undo_linear_component(color[2]) * 255.0; + let a = color[3] * 255.0; + [ + cmp::max(0, cmp::min(255, r.round() as i32)) as u8, + cmp::max(0, cmp::min(255, g.round() as i32)) as u8, + cmp::max(0, cmp::min(255, b.round() as i32)) as u8, + cmp::max(0, cmp::min(255, a.round() as i32)) as u8, + ] + } + + /* + draw_target.push_clip_rect(IntRect::new( + IntPoint::new(0, 0), + IntPoint::new(size.width as i32, size.height as i32), + )); + */ + + for indices in buffers.indices.chunks_exact(3) { + let a = &buffers.vertices[indices[0] as usize]; + let b = &buffers.vertices[indices[1] as usize]; + let c = &buffers.vertices[indices[2] as usize]; + + // Scaling is applied here + //TODO: align to integers? + let mut pb = PathBuilder::new(); + pb.move_to( + scale_size(a.position[0], false), + scale_size(a.position[1], false), + ); + pb.line_to( + scale_size(b.position[0], false), + scale_size(b.position[1], false), + ); + pb.line_to( + scale_size(c.position[0], false), + scale_size(c.position[1], false), + ); + pb.close(); + + // TODO: Each vertice has its own separate color. + let rgba8 = linear_to_rgba8(&a.color); + let source = + Source::Solid(SolidSource::from_unpremultiplied_argb( + rgba8[3], rgba8[0], rgba8[1], rgba8[2], + )); + + let path = pb.finish(); + draw_target.fill(&path, &source, draw_options); + } + + /* + draw_target.pop_clip(); + */ + } + Primitive::Cached { cache } => { + draw_primitive( + draw_target, + draw_options, + backend, + scale_factor, + &cache, + ); + } + } +} diff --git a/softbuffer/src/window.rs b/softbuffer/src/window.rs new file mode 100644 index 0000000000..aac5fb9ed8 --- /dev/null +++ b/softbuffer/src/window.rs @@ -0,0 +1,4 @@ +//! Display rendering results on windows. +mod compositor; + +pub use compositor::Compositor; diff --git a/softbuffer/src/window/compositor.rs b/softbuffer/src/window/compositor.rs new file mode 100644 index 0000000000..2b0ce9db65 --- /dev/null +++ b/softbuffer/src/window/compositor.rs @@ -0,0 +1,80 @@ +use crate::{surface::Surface, Backend}; + +use iced_graphics::{ + compositor::{self, Information, SurfaceError}, + Color, Error, Viewport, +}; +use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; +use std::marker::PhantomData; + +/// A window graphics backend for iced powered by `glow`. +pub struct Compositor { + theme: PhantomData, +} + +/// A graphics compositor that can draw to windows. +impl compositor::Compositor for Compositor { + /// The settings of the backend. + type Settings = crate::Settings; + + /// The iced renderer of the backend. + type Renderer = crate::Renderer; + + /// The surface of the backend. + type Surface = Surface; + + /// Creates a new [`Compositor`]. + fn new( + settings: Self::Settings, + compatible_window: Option<&W>, + ) -> Result<(Self, Self::Renderer), Error> { + let compositor = Self { theme: PhantomData }; + + let renderer = Self::Renderer::new(Backend::new()); + + Ok((compositor, renderer)) + } + + /// Crates a new [`Surface`] for the given window. + /// + /// [`Surface`]: Self::Surface + fn create_surface( + &mut self, + window: &W, + ) -> Self::Surface { + Self::Surface::new(window) + } + + /// Configures a new [`Surface`] with the given dimensions. + /// + /// [`Surface`]: Self::Surface + fn configure_surface( + &mut self, + surface: &mut Self::Surface, + width: u32, + height: u32, + ) { + surface.configure(width, height); + } + + /// Returns [`Information`] used by this [`Compositor`]. + fn fetch_information(&self) -> Information { + todo!("Compositor::fetch_information"); + } + + /// Presents the [`Renderer`] primitives to the next frame of the given [`Surface`]. + /// + /// [`Renderer`]: Self::Renderer + /// [`Surface`]: Self::Surface + fn present>( + &mut self, + renderer: &mut Self::Renderer, + surface: &mut Self::Surface, + viewport: &Viewport, + background: Color, + overlay: &[T], + ) -> Result<(), SurfaceError> { + surface.present(renderer, viewport.scale_factor() as f32, background); + Ok(()) + } +} diff --git a/src/clipboard.rs b/src/clipboard.rs index dde170514b..27ac211040 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -1,3 +1,4 @@ //! Access the clipboard. -#[cfg(not(target_arch = "wasm32"))] +#[cfg(any(feature = "winit"))] +// TODO support in wayland pub use crate::runtime::clipboard::{read, write}; diff --git a/src/element.rs b/src/element.rs index 2eb1bb4db3..0081bae6d2 100644 --- a/src/element.rs +++ b/src/element.rs @@ -1,5 +1,11 @@ /// A generic widget. /// /// This is an alias of an `iced_native` element with a default `Renderer`. +#[cfg(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" +))] pub type Element<'a, Message, Renderer = crate::Renderer> = - crate::runtime::Element<'a, Message, Renderer>; + iced_native::Element<'a, Message, Renderer>; diff --git a/src/error.rs b/src/error.rs index 0bfa3ff1c4..c6d617b2c9 100644 --- a/src/error.rs +++ b/src/error.rs @@ -16,6 +16,24 @@ pub enum Error { GraphicsCreationFailed(iced_graphics::Error), } +#[cfg(feature = "wayland")] +impl From for Error { + fn from(error: iced_sctk::Error) -> Self { + match error { + iced_sctk::Error::ExecutorCreationFailed(error) => { + Error::ExecutorCreationFailed(error) + } + iced_sctk::Error::WindowCreationFailed(error) => { + Error::WindowCreationFailed(error) + } + iced_sctk::Error::GraphicsCreationFailed(error) => { + Error::GraphicsCreationFailed(error) + } + } + } +} + +#[cfg(feature = "winit")] impl From for Error { fn from(error: iced_winit::Error) -> Error { match error { diff --git a/src/executor.rs b/src/executor.rs index 36ae274ec0..e9853391cc 100644 --- a/src/executor.rs +++ b/src/executor.rs @@ -1,4 +1,5 @@ //! Choose your preferred executor to power your application. +#[cfg(any(feature = "winit", feature = "wayland"))] pub use crate::runtime::Executor; /// A default cross-platform executor. diff --git a/src/keyboard.rs b/src/keyboard.rs index 2134a66bc3..a1e45851f8 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -1,2 +1,2 @@ //! Listen and react to keyboard events. -pub use crate::runtime::keyboard::{Event, KeyCode, Modifiers}; +pub use iced_native::keyboard::{Event, KeyCode, Modifiers}; diff --git a/src/lib.rs b/src/lib.rs index 001768272b..dfaf53e2b4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -164,12 +164,27 @@ #![allow(clippy::inherent_to_string, clippy::type_complexity)] #![cfg_attr(docsrs, feature(doc_cfg))] +#[cfg(feature = "winit")] +pub mod application; + mod element; mod error; mod result; + +#[cfg(feature = "winit")] mod sandbox; -pub mod application; +#[cfg(feature = "winit")] +pub use application::Application; + +/// wayland application +#[cfg(feature = "wayland")] +pub mod wayland; +#[cfg(feature = "wayland")] +pub use wayland::sandbox; +#[cfg(feature = "wayland")] +pub use wayland::Application; + pub mod clipboard; pub mod executor; pub mod keyboard; @@ -181,39 +196,73 @@ pub mod touch; pub mod widget; pub mod window; -#[cfg(all(not(feature = "glow"), feature = "wgpu"))] +#[cfg(all( + not(feature = "glow"), + any(feature = "wgpu", feature = "softbuffer", feature = "dyrend",), + not(feature = "wayland"), + feature = "multi_window" +))] +pub mod multi_window; + +#[cfg(feature = "wayland")] +use iced_sctk as runtime; + +#[cfg(feature = "winit")] use iced_winit as runtime; -#[cfg(feature = "glow")] +#[cfg(feature = "glutin")] use iced_glutin as runtime; -#[cfg(all(not(feature = "glow"), feature = "wgpu"))] +#[cfg(feature = "wgpu")] use iced_wgpu as renderer; -#[cfg(feature = "glow")] +#[cfg(any(feature = "glow", feature = "glutin"))] use iced_glow as renderer; +#[cfg(feature = "softbuffer")] +use iced_softbuffer as renderer; + +#[cfg(feature = "dyrend")] +use iced_dyrend as renderer; + pub use iced_native::theme; -pub use runtime::event; -pub use runtime::subscription; -pub use application::Application; +#[cfg(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" +))] pub use element::Element; pub use error::Error; pub use event::Event; +#[cfg(any(feature = "winit", feature = "wayland"))] pub use executor::Executor; +#[cfg(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" +))] pub use renderer::Renderer; pub use result::Result; +#[cfg(any(feature = "winit", feature = "wayland"))] pub use sandbox::Sandbox; pub use settings::Settings; pub use subscription::Subscription; pub use theme::Theme; -pub use runtime::alignment; -pub use runtime::futures; +#[cfg(not(any(feature = "winit", feature = "wayland")))] +pub use iced_native::{ + alignment, color, event, futures, subscription, Alignment, Background, + Color, Command, ContentFit, Font, Length, Padding, Point, Rectangle, Size, + Vector, +}; +#[cfg(any(feature = "winit", feature = "wayland"))] pub use runtime::{ - color, Alignment, Background, Color, Command, ContentFit, Font, Length, - Padding, Point, Rectangle, Size, Vector, + alignment, color, event, futures, subscription, Alignment, Background, + Color, Command, ContentFit, Font, Length, Padding, Point, Rectangle, Size, + Vector, }; #[cfg(feature = "system")] diff --git a/src/mouse.rs b/src/mouse.rs index d61ed09a06..9b2d368dc5 100644 --- a/src/mouse.rs +++ b/src/mouse.rs @@ -1,2 +1,2 @@ //! Listen and react to mouse events. -pub use crate::runtime::mouse::{Button, Event, Interaction, ScrollDelta}; +pub use iced_native::mouse::{Button, Event, Interaction, ScrollDelta}; diff --git a/src/overlay.rs b/src/overlay.rs index c0f4c49202..cab951186e 100644 --- a/src/overlay.rs +++ b/src/overlay.rs @@ -5,14 +5,40 @@ /// This is an alias of an `iced_native` element with a default `Renderer`. /// /// [`Overlay`]: iced_native::Overlay +#[cfg(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" +))] pub type Element<'a, Message, Renderer = crate::Renderer> = iced_native::overlay::Element<'a, Message, Renderer>; +#[cfg(not(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" +)))] +pub use iced_native::overlay::Element; pub mod menu { //! Build and show dropdown menus. pub use iced_native::overlay::menu::{Appearance, State, StyleSheet}; /// A widget that produces a message when clicked. + #[cfg(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" + ))] pub type Menu<'a, Message, Renderer = crate::Renderer> = iced_native::overlay::Menu<'a, Message, Renderer>; + #[cfg(not(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" + )))] + pub use iced_native::overlay::Menu; } diff --git a/src/settings.rs b/src/settings.rs index d31448fbd7..82c8831ef9 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,4 +1,5 @@ //! Configure your application. +#[cfg(not(feature = "wayland"))] use crate::window; /// The settings of an application. @@ -13,8 +14,13 @@ pub struct Settings { /// The window settings. /// /// They will be ignored on the Web. + #[cfg(not(feature = "wayland"))] pub window: window::Settings, + /// the initial surface to be created + #[cfg(feature = "wayland")] + pub initial_surface: iced_sctk::settings::InitialSurface, + /// The data needed to initialize the [`Application`]. /// /// [`Application`]: crate::Application @@ -76,7 +82,10 @@ impl Settings { Self { flags, id: default_settings.id, + #[cfg(not(feature = "wayland"))] window: default_settings.window, + #[cfg(feature = "wayland")] + initial_surface: default_settings.initial_surface, default_font: default_settings.default_font, default_text_size: default_settings.default_text_size, text_multithreading: default_settings.text_multithreading, @@ -94,7 +103,10 @@ where fn default() -> Self { Self { id: None, + #[cfg(not(feature = "wayland"))] window: Default::default(), + #[cfg(feature = "wayland")] + initial_surface: Default::default(), flags: Default::default(), default_font: Default::default(), default_text_size: 20, @@ -106,6 +118,7 @@ where } } +#[cfg(all(not(target_arch = "wasm32"), feature = "winit"))] impl From> for iced_winit::Settings { fn from(settings: Settings) -> iced_winit::Settings { iced_winit::Settings { @@ -117,3 +130,15 @@ impl From> for iced_winit::Settings { } } } +#[cfg(feature = "wayland")] +impl From> for iced_sctk::Settings { + fn from(settings: Settings) -> Self { + Self { + flags: settings.flags, + kbd_repeat: Default::default(), + ptr_theme: None, + surface: settings.initial_surface, + exit_on_close_request: settings.exit_on_close_request, + } + } +} diff --git a/src/touch.rs b/src/touch.rs index 0b77c386ee..e8f21af87a 100644 --- a/src/touch.rs +++ b/src/touch.rs @@ -1,2 +1,2 @@ //! Listen and react to touch events. -pub use crate::runtime::touch::{Event, Finger}; +pub use iced_native::touch::{Event, Finger}; diff --git a/src/wayland/mod.rs b/src/wayland/mod.rs new file mode 100644 index 0000000000..fd9c404a7d --- /dev/null +++ b/src/wayland/mod.rs @@ -0,0 +1,224 @@ +use crate::{Command, Element, Executor, Settings, Subscription}; + +/// wayland sandbox +pub mod sandbox; +pub use iced_native::application::{Appearance, StyleSheet}; +pub use iced_native::command::platform_specific::wayland as actions; +pub use iced_sctk::{ + application::SurfaceIdWrapper, command::*, 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) -> 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) -> Self::Theme { + Self::Theme::default() + } + + /// Returns the current [`Style`] of the [`Theme`]. + /// + /// [`Style`]: ::Style + /// [`Theme`]: Self::Theme + fn style(&self) -> ::Style { + ::Style::default() + } + + /// Returns the event [`Subscription`] for the current state of the + /// application. + /// + /// A [`Subscription`] will be kept alive as long as you keep returning it, + /// and the __messages__ produced will be handled by + /// [`update`](#tymethod.update). + /// + /// By default, this method returns an empty [`Subscription`]. + fn subscription(&self) -> Subscription { + Subscription::none() + } + + /// Returns the widgets to display in the [`Application`]. + /// + /// These widgets can produce __messages__ based on user interaction. + fn view( + &self, + id: SurfaceIdWrapper, + ) -> Element<'_, Self::Message, crate::Renderer>; + + /// Returns the scale factor of the [`Application`]. + /// + /// It can be used to dynamically control the size of the UI at runtime + /// (i.e. zooming). + /// + /// For instance, a scale factor of `2.0` will make widgets twice as big, + /// while a scale factor of `0.5` will shrink them to half their size. + /// + /// By default, it returns `1.0`. + fn scale_factor(&self) -> f64 { + 1.0 + } + + /// Returns whether the [`Application`] should be terminated. + /// + /// By default, it returns `false`. + fn should_exit(&self) -> bool { + false + } + + /// window was requested to close + fn close_requested(&self, id: SurfaceIdWrapper) -> Self::Message; + + /// 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, + text_multithreading: settings.text_multithreading, + antialiasing: if settings.antialiasing { + Some(crate::renderer::settings::Antialiasing::MSAAx4) + } else { + None + }, + ..crate::renderer::Settings::from_env() + }; + + #[cfg(feature = "glow")] + { + let renderer_settings = iced_glutin::Settings { + gl_settings: renderer_settings, + try_opengles_first: settings.try_opengles_first + }; + Ok(crate::runtime::run::< + Instance, + Self::Executor, + iced_glutin::Compositor>, + >(settings.into(), renderer_settings)?) + } + #[cfg(not(feature = "glow"))] + { + Ok(crate::runtime::run::< + Instance, + Self::Executor, + crate::renderer::window::Compositor, + >(settings.into(), renderer_settings)?) + } + } +} + +struct Instance(A); + +impl crate::runtime::Application for Instance +where + A: Application, +{ + type Flags = A::Flags; + type Renderer = crate::Renderer; + type Message = A::Message; + + fn new(flags: Self::Flags) -> (Self, Command) { + let (app, command) = A::new(flags); + + (Instance(app), command) + } + + fn title(&self) -> String { + self.0.title() + } + + fn update(&mut self, message: Self::Message) -> Command { + self.0.update(message) + } + + fn view( + &self, + id: SurfaceIdWrapper, + ) -> Element<'_, Self::Message, Self::Renderer> { + self.0.view(id) + } + + fn theme(&self) -> A::Theme { + self.0.theme() + } + + fn style(&self) -> ::Style { + self.0.style() + } + + fn subscription(&self) -> Subscription { + self.0.subscription() + } + + fn scale_factor(&self) -> f64 { + self.0.scale_factor() + } + + fn should_exit(&self) -> bool { + self.0.should_exit() + } + + fn close_requested(&self, id: SurfaceIdWrapper) -> Self::Message { + self.0.close_requested(id) + } +} diff --git a/src/wayland/sandbox.rs b/src/wayland/sandbox.rs new file mode 100644 index 0000000000..416d3826ef --- /dev/null +++ b/src/wayland/sandbox.rs @@ -0,0 +1,229 @@ +use iced_sctk::application::SurfaceIdWrapper; + +use crate::theme::{self, Theme}; +use crate::{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: SurfaceIdWrapper, + ) -> Element<'_, Self::Message>; + + /// window was requested to close + fn close_requested(&self, id: SurfaceIdWrapper) -> Self::Message; + + /// Returns the current [`Theme`] of the [`Sandbox`]. + /// + /// If you want to use your own custom theme type, you will have to use an + /// [`Application`]. + /// + /// By default, it returns [`Theme::default`]. + fn theme(&self) -> Theme { + Theme::default() + } + + /// Returns the current style variant of [`theme::Application`]. + /// + /// By default, it returns [`theme::Application::default`]. + fn style(&self) -> theme::Application { + theme::Application::default() + } + + /// Returns the scale factor of the [`Sandbox`]. + /// + /// It can be used to dynamically control the size of the UI at runtime + /// (i.e. zooming). + /// + /// For instance, a scale factor of `2.0` will make widgets twice as big, + /// while a scale factor of `0.5` will shrink them to half their size. + /// + /// By default, it returns `1.0`. + fn scale_factor(&self) -> f64 { + 1.0 + } + + /// Returns whether the [`Sandbox`] should be terminated. + /// + /// By default, it returns `false`. + fn should_exit(&self) -> bool { + false + } + + /// 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) -> String { + T::title(self) + } + + fn update(&mut self, message: T::Message) -> Command { + T::update(self, message); + + Command::none() + } + + fn theme(&self) -> Self::Theme { + T::theme(self) + } + + fn style(&self) -> theme::Application { + T::style(self) + } + + fn subscription(&self) -> Subscription { + Subscription::none() + } + + fn scale_factor(&self) -> f64 { + T::scale_factor(self) + } + + fn should_exit(&self) -> bool { + T::should_exit(self) + } + + /// Returns the widgets to display in the [`Sandbox`] window. + /// + /// These widgets can produce __messages__ based on user interaction. + fn view( + &self, + id: SurfaceIdWrapper, + ) -> Element<'_, Self::Message> { + T::view(self, id) + } + + /// window was requested to close + fn close_requested(&self, id: SurfaceIdWrapper) -> Self::Message { + T::close_requested(&self, id) + } +} diff --git a/src/widget.rs b/src/widget.rs index 76cea7be6c..a06b8468ac 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -4,29 +4,81 @@ pub use iced_native::widget::helpers::*; pub use iced_native::{column, row}; /// A container that distributes its contents vertically. +#[cfg(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" +))] pub type Column<'a, Message, Renderer = crate::Renderer> = iced_native::widget::Column<'a, Message, Renderer>; +#[cfg(not(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" +)))] +pub use iced_native::widget::Column; /// A container that distributes its contents horizontally. +#[cfg(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" +))] pub type Row<'a, Message, Renderer = crate::Renderer> = iced_native::widget::Row<'a, Message, Renderer>; +#[cfg(not(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" +)))] +pub use iced_native::widget::Row; pub mod text { //! Write some text for your users to read. pub use iced_native::widget::text::{Appearance, StyleSheet}; /// A paragraph of text. + #[cfg(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" + ))] pub type Text<'a, Renderer = crate::Renderer> = iced_native::widget::Text<'a, Renderer>; + #[cfg(not(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" + )))] + pub use iced_native::widget::Text; } pub mod button { //! Allow your users to perform actions by pressing a button. - pub use iced_native::widget::button::{Appearance, StyleSheet}; + pub use iced_native::widget::button::{focus, Appearance, Id, StyleSheet}; /// A widget that produces a message when clicked. + #[cfg(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" + ))] pub type Button<'a, Message, Renderer = crate::Renderer> = iced_native::widget::Button<'a, Message, Renderer>; + #[cfg(not(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" + )))] + pub use iced_native::widget::Button; } pub mod checkbox { @@ -34,8 +86,21 @@ pub mod checkbox { pub use iced_native::widget::checkbox::{Appearance, StyleSheet}; /// A box that can be checked. + #[cfg(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" + ))] pub type Checkbox<'a, Message, Renderer = crate::Renderer> = iced_native::widget::Checkbox<'a, Message, Renderer>; + #[cfg(not(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" + )))] + pub use iced_native::widget::Checkbox; } pub mod container { @@ -43,8 +108,42 @@ pub mod container { pub use iced_native::widget::container::{Appearance, StyleSheet}; /// An element decorating some content. + #[cfg(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" + ))] pub type Container<'a, Message, Renderer = crate::Renderer> = iced_native::widget::Container<'a, Message, Renderer>; + #[cfg(not(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" + )))] + pub use iced_native::widget::Container; +} + +pub mod mouse_listener { + //! Intercept mouse events on a widget. + + /// A container intercepting mouse events. + #[cfg(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" + ))] + pub type MouseListener<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::MouseListener<'a, Message, Renderer>; + #[cfg(not(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" + )))] + pub use iced_native::widget::MouseListener; } pub mod pane_grid { @@ -66,16 +165,55 @@ pub mod pane_grid { /// to completely fill the space available. /// /// [![Pane grid - Iced](https://thumbs.gfycat.com/MixedFlatJellyfish-small.gif)](https://gfycat.com/mixedflatjellyfish) + #[cfg(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" + ))] pub type PaneGrid<'a, Message, Renderer = crate::Renderer> = iced_native::widget::PaneGrid<'a, Message, Renderer>; + #[cfg(not(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" + )))] + pub use iced_native::widget::PaneGrid; /// The content of a [`Pane`]. + #[cfg(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" + ))] pub type Content<'a, Message, Renderer = crate::Renderer> = iced_native::widget::pane_grid::Content<'a, Message, Renderer>; + #[cfg(not(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" + )))] + pub use iced_native::widget::pane_grid::Content; /// The title bar of a [`Pane`]. + #[cfg(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" + ))] pub type TitleBar<'a, Message, Renderer = crate::Renderer> = iced_native::widget::pane_grid::TitleBar<'a, Message, Renderer>; + #[cfg(not(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" + )))] + pub use iced_native::widget::pane_grid::TitleBar; } pub mod pick_list { @@ -83,8 +221,21 @@ pub mod pick_list { pub use iced_native::widget::pick_list::{Appearance, StyleSheet}; /// A widget allowing the selection of a single value from a list of options. + #[cfg(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" + ))] pub type PickList<'a, T, Message, Renderer = crate::Renderer> = iced_native::widget::PickList<'a, T, Message, Renderer>; + #[cfg(not(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" + )))] + pub use iced_native::widget::PickList; } pub mod radio { @@ -92,8 +243,21 @@ pub mod radio { pub use iced_native::widget::radio::{Appearance, StyleSheet}; /// A circular button representing a choice. + #[cfg(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" + ))] pub type Radio = iced_native::widget::Radio; + #[cfg(not(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" + )))] + pub use iced_native::widget::Radio; } pub mod scrollable { @@ -104,8 +268,21 @@ pub mod scrollable { /// A widget that can vertically display an infinite amount of content /// with a scrollbar. + #[cfg(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" + ))] pub type Scrollable<'a, Message, Renderer = crate::Renderer> = iced_native::widget::Scrollable<'a, Message, Renderer>; + #[cfg(not(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" + )))] + pub use iced_native::widget::Scrollable; } pub mod toggler { @@ -113,8 +290,21 @@ pub mod toggler { pub use iced_native::widget::toggler::{Appearance, StyleSheet}; /// A toggler widget. + #[cfg(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" + ))] pub type Toggler<'a, Message, Renderer = crate::Renderer> = iced_native::widget::Toggler<'a, Message, Renderer>; + #[cfg(not(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" + )))] + pub use iced_native::widget::Toggler; } pub mod text_input { @@ -125,8 +315,21 @@ pub mod text_input { }; /// A field that can be filled with text. + #[cfg(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" + ))] pub type TextInput<'a, Message, Renderer = crate::Renderer> = iced_native::widget::TextInput<'a, Message, Renderer>; + #[cfg(not(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" + )))] + pub use iced_native::widget::TextInput; } pub mod tooltip { @@ -134,8 +337,21 @@ pub mod tooltip { pub use iced_native::widget::tooltip::Position; /// A widget allowing the selection of a single value from a list of options. + #[cfg(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" + ))] pub type Tooltip<'a, Message, Renderer = crate::Renderer> = iced_native::widget::Tooltip<'a, Message, Renderer>; + #[cfg(not(any( + feature = "softbuffer", + feature = "glow", + feature = "wgpu", + feature = "dyrend" + )))] + pub use iced_native::widget::Tooltip; } pub use iced_native::widget::progress_bar; @@ -215,10 +431,13 @@ pub use qr_code::QRCode; #[cfg_attr(docsrs, doc(cfg(feature = "svg")))] pub use svg::Svg; +#[cfg(any(feature = "winit", feature = "wayland"))] use crate::Command; +#[cfg(any(feature = "winit", feature = "wayland"))] use iced_native::widget::operation; /// Focuses the previous focusable widget. +#[cfg(any(feature = "winit", feature = "wayland"))] pub fn focus_previous() -> Command where Message: 'static, @@ -227,6 +446,7 @@ where } /// Focuses the next focusable widget. +#[cfg(any(feature = "winit", feature = "wayland"))] pub fn focus_next() -> Command where Message: 'static, diff --git a/src/window.rs b/src/window.rs index 2018053fbb..1fa1b60fe5 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,12 +1,19 @@ //! Configure the window of your application in native platforms. +#[cfg(feature = "winit")] +pub mod icon; mod position; mod settings; - -pub mod icon; - +#[cfg(feature = "winit")] pub use icon::Icon; pub use position::Position; pub use settings::Settings; -#[cfg(not(target_arch = "wasm32"))] -pub use crate::runtime::window::*; +#[cfg(all(not(target_arch = "wasm32"), feature = "winit"))] +pub use crate::runtime::window::move_to; +#[cfg(all( + not(target_arch = "wasm32"), + any(feature = "wayland", feature = "winit") +))] +pub use crate::runtime::window::resize; + +pub use iced_native::window::Id; diff --git a/src/window/position.rs b/src/window/position.rs index 6b9fac417b..539804cbf0 100644 --- a/src/window/position.rs +++ b/src/window/position.rs @@ -21,6 +21,7 @@ impl Default for Position { } } +#[cfg(feature = "winit")] impl From for iced_winit::Position { fn from(position: Position) -> Self { match position { diff --git a/src/window/settings.rs b/src/window/settings.rs index 24d0f4f9ec..bed957ed73 100644 --- a/src/window/settings.rs +++ b/src/window/settings.rs @@ -1,8 +1,13 @@ -use crate::window::{Icon, Position}; +use crate::window::Position; +#[cfg(feature = "winit")] +use crate::window::Icon; /// The window settings of an application. #[derive(Debug, Clone)] pub struct Settings { + /// The size of the resize-enabled border. + pub border_size: u32, + /// The initial size of the window. pub size: (u32, u32), @@ -31,12 +36,14 @@ pub struct Settings { pub always_on_top: bool, /// The icon of the window. + #[cfg(feature = "winit")] pub icon: Option, } impl Default for Settings { fn default() -> Settings { Settings { + border_size: 8, size: (1024, 768), position: Position::default(), min_size: None, @@ -46,14 +53,17 @@ impl Default for Settings { decorations: true, transparent: false, always_on_top: false, + #[cfg(feature = "winit")] icon: None, } } } +#[cfg(feature = "winit")] impl From for iced_winit::settings::Window { fn from(settings: Settings) -> Self { Self { + border_size: settings.border_size, size: settings.size, position: iced_winit::Position::from(settings.position), min_size: settings.min_size, diff --git a/style/src/button.rs b/style/src/button.rs index a564a2b7ea..93e13e6b40 100644 --- a/style/src/button.rs +++ b/style/src/button.rs @@ -1,5 +1,5 @@ //! Change the apperance of a button. -use iced_core::{Background, Color, Vector}; +use iced_core::{Background, BorderRadius, Color, Vector}; /// The appearance of a button. #[derive(Debug, Clone, Copy)] @@ -9,7 +9,7 @@ pub struct Appearance { /// The [`Background`] of the button. pub background: Option, /// The border radius of the button. - pub border_radius: f32, + pub border_radius: BorderRadius, /// The border width of the button. pub border_width: f32, /// The border [`Color`] of the button. @@ -23,7 +23,7 @@ impl std::default::Default for Appearance { Self { shadow_offset: Vector::default(), background: None, - border_radius: 0.0, + border_radius: BorderRadius::from(0.0), border_width: 0.0, border_color: Color::TRANSPARENT, text_color: Color::BLACK, @@ -39,6 +39,16 @@ pub trait StyleSheet { /// Produces the active [`Appearance`] of a button. fn active(&self, style: &Self::Style) -> Appearance; + /// Produces the focused [`Appearance`] of a button. + fn focused(&self, style: &Self::Style) -> Appearance { + let active = self.active(style); + + Appearance { + shadow_offset: active.shadow_offset + Vector::new(0.0, 1.0), + ..active + } + } + /// Produces the hovered [`Appearance`] of a button. fn hovered(&self, style: &Self::Style) -> Appearance { let active = self.active(style); diff --git a/style/src/theme.rs b/style/src/theme.rs index 271d9a29f2..a427904c03 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -21,6 +21,7 @@ use crate::text; use crate::text_input; use crate::toggler; +use iced_core::BorderRadius; use iced_core::{Background, Color, Vector}; use std::rc::Rc; @@ -146,7 +147,7 @@ impl button::StyleSheet for Theme { let palette = self.extended_palette(); let appearance = button::Appearance { - border_radius: 2.0, + border_radius: BorderRadius::from(2.0), ..button::Appearance::default() }; @@ -192,6 +193,25 @@ impl button::StyleSheet for Theme { } } + fn focused(&self, style: &Self::Style) -> button::Appearance { + let palette = self.extended_palette(); + + let background = match style { + Button::Primary => Some(palette.primary.base.color), + Button::Secondary => Some(palette.background.strong.color), + Button::Positive => Some(palette.success.strong.color), + Button::Destructive => Some(palette.danger.strong.color), + Button::Text | Button::Custom(_) => None, + }; + + let active = self.active(style); + + button::Appearance { + background: background.map(Background::from), + ..active + } + } + fn pressed(&self, style: &Self::Style) -> button::Appearance { if let Button::Custom(custom) = style { return custom.pressed(self); @@ -351,6 +371,13 @@ pub enum Container { Custom(Box>), } +impl Container { + /// Creates a custom [`Container`] style. + pub fn custom_fn(f: fn(&Theme) -> container::Appearance) -> Self { + Self::Custom(Box::new(f)) + } +} + impl From container::Appearance> for Container { fn from(f: fn(&Theme) -> container::Appearance) -> Self { Self::Custom(Box::new(f)) @@ -362,7 +389,7 @@ impl container::StyleSheet for Theme { fn appearance(&self, style: &Self::Style) -> container::Appearance { match style { - Container::Transparent => Default::default(), + Container::Transparent => container::Appearance::default(), Container::Box => { let palette = self.extended_palette(); diff --git a/winit/Cargo.toml b/winit/Cargo.toml index ebbadb12dc..5afb75e3cb 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -21,9 +21,8 @@ log = "0.4" thiserror = "1.0" [dependencies.winit] -version = "0.27" -git = "https://github.com/iced-rs/winit.git" -rev = "940457522e9fb9f5dac228b0ecfafe0138b4048c" +git = "https://github.com/pop-os/winit.git" +branch = "iced" [dependencies.iced_native] version = "0.7" diff --git a/winit/src/application.rs b/winit/src/application.rs index 0f9b562e67..42cab0c6b2 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -22,7 +22,9 @@ use iced_native::user_interface::{self, UserInterface}; pub use iced_native::application::{Appearance, StyleSheet}; +use std::collections::HashMap; use std::mem::ManuallyDrop; +use winit::window::{CursorIcon, ResizeDirection}; /// An interactive, native cross-platform application. /// @@ -114,6 +116,9 @@ where let mut debug = Debug::new(); debug.startup_started(); + // Defines width of the window border for the resize handles. + let border_size = settings.window.border_size; + let event_loop = EventLoopBuilder::with_user_event().build(); let proxy = event_loop.create_proxy(); @@ -186,6 +191,7 @@ where init_command, window, settings.exit_on_close_request, + border_size, )); let mut context = task::Context::from_waker(task::noop_waker_ref()); @@ -236,6 +242,7 @@ async fn run_instance( init_command: Command, window: winit::window::Window, exit_on_close_request: bool, + border_size: u32, ) where A: Application + 'static, E: Executor + 'static, @@ -285,6 +292,14 @@ async fn run_instance( &mut debug, )); + let mut cursor_resize_event = cursor_resize_event_func( + &window, + border_size as f64 * window.scale_factor(), + ); + + let window_ids = + HashMap::from([(window.id(), iced_native::window::Id::MAIN)]); + let mut mouse_interaction = mouse::Interaction::default(); let mut events = Vec::new(); let mut messages = Vec::new(); @@ -466,9 +481,16 @@ async fn run_instance( } } event::Event::WindowEvent { + window_id, event: window_event, .. } => { + if let Some(func) = cursor_resize_event.as_mut() { + if func(&window, &window_event) { + continue; + } + } + if requests_exit(&window_event, state.modifiers()) && exit_on_close_request { @@ -478,6 +500,7 @@ async fn run_instance( state.update(&window, &window_event, &mut debug); if let Some(event) = conversion::window_event( + *window_ids.get(&window_id).unwrap(), &window_event, state.scale_factor(), state.modifiers(), @@ -625,7 +648,7 @@ pub fn run_command( clipboard.write(contents); } }, - command::Action::Window(action) => match action { + command::Action::Window(_, action) => match action { window::Action::Close => { *should_exit = true; } @@ -674,6 +697,7 @@ pub fn run_command( .send_event(tag(mode)) .expect("Send message to event loop"); } + window::Action::Spawn { .. } => {} }, command::Action::System(action) => match action { system::Action::QueryInformation(_tag) => { @@ -726,6 +750,7 @@ pub fn run_command( current_cache = user_interface.into_cache(); *cache = current_cache; } + command::Action::PlatformSpecific(_) => {} } } } @@ -769,3 +794,134 @@ mod platform { event_loop.run(event_handler) } } + +/// If supported by winit, returns a closure that implements cursor resize support. +fn cursor_resize_event_func( + window: &winit::window::Window, + border_size: f64, +) -> Option< + impl FnMut(&winit::window::Window, &winit::event::WindowEvent<'_>) -> bool, +> { + if window.drag_resize_window(ResizeDirection::East).is_ok() { + // Keep track of cursor when it is within a resizeable border. + let mut cursor_prev_resize_direction = None; + + Some( + move |window: &winit::window::Window, + window_event: &winit::event::WindowEvent<'_>| + -> bool { + // Keep track of border resize state and set cursor icon when in range + match window_event { + winit::event::WindowEvent::CursorMoved { + position, .. + } => { + if !window.is_decorated() { + let location = cursor_resize_direction( + window.inner_size(), + *position, + border_size, + ); + if location != cursor_prev_resize_direction { + window.set_cursor_icon( + resize_direction_cursor_icon(location), + ); + cursor_prev_resize_direction = location; + return true; + } + } + } + winit::event::WindowEvent::MouseInput { + state: winit::event::ElementState::Pressed, + button: winit::event::MouseButton::Left, + .. + } => { + if let Some(direction) = cursor_prev_resize_direction { + let _res = window.drag_resize_window(direction); + return true; + } + } + _ => (), + } + + false + }, + ) + } else { + None + } +} + +/// Get the cursor icon that corresponds to the resize direction. +fn resize_direction_cursor_icon( + resize_direction: Option, +) -> CursorIcon { + match resize_direction { + Some(resize_direction) => match resize_direction { + ResizeDirection::East => CursorIcon::EResize, + ResizeDirection::North => CursorIcon::NResize, + ResizeDirection::NorthEast => CursorIcon::NeResize, + ResizeDirection::NorthWest => CursorIcon::NwResize, + ResizeDirection::South => CursorIcon::SResize, + ResizeDirection::SouthEast => CursorIcon::SeResize, + ResizeDirection::SouthWest => CursorIcon::SwResize, + ResizeDirection::West => CursorIcon::WResize, + }, + None => CursorIcon::Default, + } +} + +/// Identifies resize direction based on cursor position and window dimensions. +#[allow(clippy::similar_names)] +fn cursor_resize_direction( + win_size: winit::dpi::PhysicalSize, + position: winit::dpi::PhysicalPosition, + border_size: f64, +) -> Option { + enum XDirection { + West, + East, + Default, + } + + enum YDirection { + North, + South, + Default, + } + + let xdir = if position.x < border_size { + XDirection::West + } else if position.x > (win_size.width as f64 - border_size) { + XDirection::East + } else { + XDirection::Default + }; + + let ydir = if position.y < border_size { + YDirection::North + } else if position.y > (win_size.height as f64 - border_size) { + YDirection::South + } else { + YDirection::Default + }; + + Some(match xdir { + XDirection::West => match ydir { + YDirection::North => ResizeDirection::NorthWest, + YDirection::South => ResizeDirection::SouthWest, + YDirection::Default => ResizeDirection::West, + }, + + XDirection::East => match ydir { + YDirection::North => ResizeDirection::NorthEast, + YDirection::South => ResizeDirection::SouthEast, + YDirection::Default => ResizeDirection::East, + }, + + XDirection::Default => match ydir { + YDirection::North => ResizeDirection::North, + YDirection::South => ResizeDirection::South, + YDirection::Default => return None, + }, + }) +} diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index b1076afe7c..013964a02e 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -10,6 +10,7 @@ use crate::{Event, Point, Position}; /// Converts a winit window event into an iced event. pub fn window_event( + id: window::Id, event: &winit::event::WindowEvent<'_>, scale_factor: f64, modifiers: winit::event::ModifiersState, @@ -20,21 +21,27 @@ pub fn window_event( WindowEvent::Resized(new_size) => { let logical_size = new_size.to_logical(scale_factor); - Some(Event::Window(window::Event::Resized { - width: logical_size.width, - height: logical_size.height, - })) + Some(Event::Window( + id, + window::Event::Resized { + width: logical_size.width, + height: logical_size.height, + }, + )) } WindowEvent::ScaleFactorChanged { new_inner_size, .. } => { let logical_size = new_inner_size.to_logical(scale_factor); - Some(Event::Window(window::Event::Resized { - width: logical_size.width, - height: logical_size.height, - })) + Some(Event::Window( + id, + window::Event::Resized { + width: logical_size.width, + height: logical_size.height, + }, + )) } WindowEvent::CloseRequested => { - Some(Event::Window(window::Event::CloseRequested)) + Some(Event::Window(id, window::Event::CloseRequested)) } WindowEvent::CursorMoved { position, .. } => { let position = position.to_logical::(scale_factor); @@ -112,19 +119,22 @@ pub fn window_event( WindowEvent::ModifiersChanged(new_modifiers) => Some(Event::Keyboard( keyboard::Event::ModifiersChanged(self::modifiers(*new_modifiers)), )), - WindowEvent::Focused(focused) => Some(Event::Window(if *focused { - window::Event::Focused - } else { - window::Event::Unfocused - })), + WindowEvent::Focused(focused) => Some(Event::Window( + id, + if *focused { + window::Event::Focused + } else { + window::Event::Unfocused + }, + )), WindowEvent::HoveredFile(path) => { - Some(Event::Window(window::Event::FileHovered(path.clone()))) + Some(Event::Window(id, window::Event::FileHovered(path.clone()))) } WindowEvent::DroppedFile(path) => { - Some(Event::Window(window::Event::FileDropped(path.clone()))) + Some(Event::Window(id, window::Event::FileDropped(path.clone()))) } WindowEvent::HoveredFileCancelled => { - Some(Event::Window(window::Event::FilesHoveredLeft)) + Some(Event::Window(id, window::Event::FilesHoveredLeft)) } WindowEvent::Touch(touch) => { Some(Event::Touch(touch_event(*touch, scale_factor))) @@ -133,7 +143,7 @@ pub fn window_event( let winit::dpi::LogicalPosition { x, y } = position.to_logical(scale_factor); - Some(Event::Window(window::Event::Moved { x, y })) + Some(Event::Window(id, window::Event::Moved { x, y })) } _ => None, } diff --git a/winit/src/settings.rs b/winit/src/settings.rs index 9bbdef5cea..d925b4fba8 100644 --- a/winit/src/settings.rs +++ b/winit/src/settings.rs @@ -61,6 +61,9 @@ pub struct Settings { /// The window settings of an application. #[derive(Debug, Clone)] pub struct Window { + /// The size of the resize-enabled border. + pub border_size: u32, + /// The size of the window. pub size: (u32, u32), @@ -183,6 +186,7 @@ impl Window { impl Default for Window { fn default() -> Window { Window { + border_size: 8, size: (1024, 768), position: Position::default(), min_size: None, diff --git a/winit/src/window.rs b/winit/src/window.rs index f6b43a0f96..6150427b50 100644 --- a/winit/src/window.rs +++ b/winit/src/window.rs @@ -2,56 +2,69 @@ use crate::command::{self, Command}; use iced_native::window; +pub use window::Id; pub use window::{Event, Mode}; /// Closes the current window and exits the application. -pub fn close() -> Command { - Command::single(command::Action::Window(window::Action::Close)) +pub fn close(id: window::Id) -> Command { + Command::single(command::Action::Window(id, window::Action::Close)) } /// Begins dragging the window while the left mouse button is held. -pub fn drag() -> Command { - Command::single(command::Action::Window(window::Action::Drag)) +pub fn drag(id: window::Id) -> Command { + Command::single(command::Action::Window(id, window::Action::Drag)) } /// Resizes the window to the given logical dimensions. -pub fn resize(width: u32, height: u32) -> Command { - Command::single(command::Action::Window(window::Action::Resize { - width, - height, - })) +pub fn resize( + id: window::Id, + width: u32, + height: u32, +) -> Command { + Command::single(command::Action::Window( + id, + window::Action::Resize { width, height }, + )) } /// Sets the window to maximized or back. -pub fn maximize(value: bool) -> Command { - Command::single(command::Action::Window(window::Action::Maximize(value))) +pub fn maximize(id: window::Id, value: bool) -> Command { + Command::single(command::Action::Window( + id, + window::Action::Maximize(value), + )) } /// Set the window to minimized or back. -pub fn minimize(value: bool) -> Command { - Command::single(command::Action::Window(window::Action::Minimize(value))) +pub fn minimize(id: window::Id, value: bool) -> Command { + Command::single(command::Action::Window( + id, + window::Action::Minimize(value), + )) } /// Moves a window to the given logical coordinates. -pub fn move_to(x: i32, y: i32) -> Command { - Command::single(command::Action::Window(window::Action::Move { x, y })) +pub fn move_to(id: window::Id, x: i32, y: i32) -> Command { + Command::single(command::Action::Window(id, window::Action::Move { x, y })) } /// Sets the [`Mode`] of the window. -pub fn set_mode(mode: Mode) -> Command { - Command::single(command::Action::Window(window::Action::SetMode(mode))) +pub fn set_mode(id: window::Id, mode: Mode) -> Command { + Command::single(command::Action::Window(id, window::Action::SetMode(mode))) } /// Sets the window to maximized or back. -pub fn toggle_maximize() -> Command { - Command::single(command::Action::Window(window::Action::ToggleMaximize)) +pub fn toggle_maximize(id: window::Id) -> Command { + Command::single(command::Action::Window(id, window::Action::ToggleMaximize)) } /// Fetches the current [`Mode`] of the window. pub fn fetch_mode( + id: window::Id, f: impl FnOnce(Mode) -> Message + 'static, ) -> Command { - Command::single(command::Action::Window(window::Action::FetchMode( - Box::new(f), - ))) + Command::single(command::Action::Window( + id, + window::Action::FetchMode(Box::new(f)), + )) }