From 5100b5d0a1f654ec1254b7765ceadfb9091d6939 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 24 Feb 2023 23:24:48 +0100 Subject: Introduce `iced_renderer` subcrate featuring runtime renderer fallback --- Cargo.toml | 14 ++-- graphics/src/lib.rs | 2 +- graphics/src/renderer.rs | 7 +- graphics/src/window/gl_compositor.rs | 2 +- renderer/Cargo.toml | 26 +++++++ renderer/src/backend.rs | 94 +++++++++++++++++++++++ renderer/src/lib.rs | 17 +++++ renderer/src/settings.rs | 30 ++++++++ renderer/src/window.rs | 3 + renderer/src/window/compositor.rs | 96 ++++++++++++++++++++++++ src/application.rs | 4 +- src/lib.rs | 2 +- wgpu/src/window.rs | 3 +- wgpu/src/window/compositor.rs | 139 ++++++++++++++++++++++------------- winit/src/application.rs | 2 +- winit/src/system.rs | 2 +- 16 files changed, 371 insertions(+), 72 deletions(-) create mode 100644 renderer/Cargo.toml create mode 100644 renderer/src/backend.rs create mode 100644 renderer/src/lib.rs create mode 100644 renderer/src/settings.rs create mode 100644 renderer/src/window.rs create mode 100644 renderer/src/window/compositor.rs diff --git a/Cargo.toml b/Cargo.toml index 551e12ac..d999febe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,9 +13,9 @@ categories = ["gui"] [features] # Enables the `Image` widget -image = ["iced_wgpu/image", "image_rs"] +image = ["iced_renderer/image", "image_rs"] # Enables the `Svg` widget -svg = ["iced_wgpu/svg"] +svg = ["iced_renderer/svg"] # Enables the `Canvas` widget canvas = ["iced_graphics/canvas"] # Enables the `QRCode` widget @@ -35,7 +35,7 @@ system = ["iced_winit/system"] # Enables chrome traces chrome-trace = [ "iced_winit/chrome-trace", - "iced_wgpu/tracing", + "iced_renderer/tracing", ] [badges] @@ -48,6 +48,7 @@ members = [ "graphics", "lazy", "native", + "renderer", "style", "wgpu", "winit", @@ -59,6 +60,7 @@ iced_core = { version = "0.8", path = "core" } iced_futures = { version = "0.6", path = "futures" } iced_native = { version = "0.9", path = "native" } iced_graphics = { version = "0.7", path = "graphics" } +iced_renderer = { version = "0.1", path = "renderer" } iced_winit = { version = "0.8", path = "winit", features = ["application"] } thiserror = "1.0" @@ -67,12 +69,6 @@ version = "0.24" package = "image" optional = true -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -iced_wgpu = { version = "0.9", path = "wgpu" } - -[target.'cfg(target_arch = "wasm32")'.dependencies] -iced_wgpu = { version = "0.9", path = "wgpu", features = ["webgl"] } - [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] features = ["image", "svg", "canvas", "qr_code"] diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index 41bef2c3..bbbdfa0e 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -46,9 +46,9 @@ pub use primitive::Primitive; pub use renderer::Renderer; pub use transformation::Transformation; pub use viewport::Viewport; -pub use window::compositor; pub use iced_native::alignment; +pub use iced_native::text; pub use iced_native::{ Alignment, Background, Color, Font, Point, Rectangle, Size, Vector, }; diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index b052c094..859ebc04 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -43,8 +43,11 @@ impl Renderer { /// Runs the given closure with the [`Backend`] and the recorded primitives /// of the [`Renderer`]. - pub fn with_primitives(&mut self, f: impl FnOnce(&mut B, &[Primitive])) { - f(&mut self.backend, &self.primitives); + pub fn with_primitives( + &mut self, + f: impl FnOnce(&mut B, &[Primitive]) -> O, + ) -> O { + f(&mut self.backend, &self.primitives) } } diff --git a/graphics/src/window/gl_compositor.rs b/graphics/src/window/gl_compositor.rs index a45a7ca1..3e6dfd9e 100644 --- a/graphics/src/window/gl_compositor.rs +++ b/graphics/src/window/gl_compositor.rs @@ -1,6 +1,6 @@ //! A compositor is responsible for initializing a renderer and managing window //! surfaces. -use crate::compositor::Information; +use crate::window::compositor::Information; use crate::{Color, Error, Size, Viewport}; use core::ffi::c_void; diff --git a/renderer/Cargo.toml b/renderer/Cargo.toml new file mode 100644 index 00000000..2a179f3a --- /dev/null +++ b/renderer/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "iced_renderer" +version = "0.1.0" +edition = "2021" + +[features] +image = ["iced_wgpu/image"] +svg = ["iced_wgpu/svg"] +tracing = ["iced_wgpu/tracing"] + +[dependencies] +raw-window-handle = "0.5" + +[dependencies.iced_native] +version = "0.9" +path = "../native" + +[dependencies.iced_graphics] +version = "0.7" +path = "../graphics" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +iced_wgpu = { version = "0.9", path = "../wgpu" } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +iced_wgpu = { version = "0.9", path = "../wgpu", features = ["webgl"] } diff --git a/renderer/src/backend.rs b/renderer/src/backend.rs new file mode 100644 index 00000000..a9a09593 --- /dev/null +++ b/renderer/src/backend.rs @@ -0,0 +1,94 @@ +use crate::{Font, Point, Size}; + +use iced_graphics::backend; +use iced_graphics::text; + +use std::borrow::Cow; + +pub enum Backend { + Wgpu(iced_wgpu::Backend), +} + +impl iced_graphics::Backend for Backend {} + +impl backend::Text for Backend { + const ICON_FONT: Font = Font::Name("Iced-Icons"); + const CHECKMARK_ICON: char = '\u{f00c}'; + const ARROW_DOWN_ICON: char = '\u{e800}'; + + fn default_font(&self) -> Font { + match self { + Self::Wgpu(backend) => backend.default_font(), + } + } + + fn default_size(&self) -> f32 { + match self { + Self::Wgpu(backend) => backend.default_size(), + } + } + + fn measure( + &self, + contents: &str, + size: f32, + font: Font, + bounds: Size, + ) -> (f32, f32) { + match self { + Self::Wgpu(backend) => { + backend.measure(contents, size, font, bounds) + } + } + } + + fn hit_test( + &self, + contents: &str, + size: f32, + font: Font, + bounds: Size, + position: Point, + nearest_only: bool, + ) -> Option { + match self { + Self::Wgpu(backend) => backend.hit_test( + contents, + size, + font, + bounds, + position, + nearest_only, + ), + } + } + + fn load_font(&mut self, font: Cow<'static, [u8]>) { + match self { + Self::Wgpu(backend) => { + backend.load_font(font); + } + } + } +} + +#[cfg(feature = "image")] +impl backend::Image for Backend { + fn dimensions(&self, handle: &iced_native::image::Handle) -> Size { + match self { + Self::Wgpu(backend) => backend.dimensions(handle), + } + } +} + +#[cfg(feature = "svg")] +impl backend::Svg for Backend { + fn viewport_dimensions( + &self, + handle: &iced_native::svg::Handle, + ) -> Size { + match self { + Self::Wgpu(backend) => backend.viewport_dimensions(handle), + } + } +} diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs new file mode 100644 index 00000000..d0ba6793 --- /dev/null +++ b/renderer/src/lib.rs @@ -0,0 +1,17 @@ +pub mod window; + +mod backend; +mod settings; + +pub use backend::Backend; +pub use settings::Settings; + +pub use iced_graphics::{ + Antialiasing, Color, Error, Font, Point, Size, Viewport, +}; + +/// The default graphics renderer for [`iced`]. +/// +/// [`iced`]: https://github.com/iced-rs/iced +pub type Renderer = + iced_graphics::Renderer; diff --git a/renderer/src/settings.rs b/renderer/src/settings.rs new file mode 100644 index 00000000..c4dc248b --- /dev/null +++ b/renderer/src/settings.rs @@ -0,0 +1,30 @@ +use crate::{Antialiasing, Font}; + +/// The settings of a [`Backend`]. +/// +/// [`Backend`]: crate::Backend +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Settings { + /// The default [`Font`] to use. + pub default_font: Font, + + /// The default size of text. + /// + /// By default, it will be set to `16.0`. + pub default_text_size: f32, + + /// The antialiasing strategy that will be used for triangle primitives. + /// + /// By default, it is `None`. + pub antialiasing: Option, +} + +impl Default for Settings { + fn default() -> Settings { + Settings { + default_font: Font::SansSerif, + default_text_size: 16.0, + antialiasing: None, + } + } +} diff --git a/renderer/src/window.rs b/renderer/src/window.rs new file mode 100644 index 00000000..a7c8911b --- /dev/null +++ b/renderer/src/window.rs @@ -0,0 +1,3 @@ +mod compositor; + +pub use compositor::Compositor; diff --git a/renderer/src/window/compositor.rs b/renderer/src/window/compositor.rs new file mode 100644 index 00000000..ad78d8bf --- /dev/null +++ b/renderer/src/window/compositor.rs @@ -0,0 +1,96 @@ +use crate::{Backend, Color, Error, Renderer, Settings, Viewport}; + +use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; + +pub use iced_graphics::window::compositor::{Information, SurfaceError}; + +pub enum Compositor { + Wgpu(iced_wgpu::window::Compositor), +} + +pub enum Surface { + Wgpu(iced_wgpu::window::Surface), +} + +impl iced_graphics::window::Compositor for Compositor { + type Settings = Settings; + type Renderer = Renderer; + type Surface = Surface; + + fn new( + settings: Self::Settings, + compatible_window: Option<&W>, + ) -> Result<(Self, Self::Renderer), Error> { + let (compositor, backend) = iced_wgpu::window::compositor::new( + iced_wgpu::Settings { + default_font: settings.default_font, + default_text_size: settings.default_text_size, + antialiasing: settings.antialiasing, + ..iced_wgpu::Settings::from_env() + }, + compatible_window, + )?; + + Ok(( + Self::Wgpu(compositor), + Renderer::new(Backend::Wgpu(backend)), + )) + } + + fn create_surface( + &mut self, + window: &W, + ) -> Surface { + match self { + Self::Wgpu(compositor) => { + Surface::Wgpu(compositor.create_surface(window)) + } + } + } + + fn configure_surface( + &mut self, + surface: &mut Surface, + width: u32, + height: u32, + ) { + match (self, surface) { + (Self::Wgpu(compositor), Surface::Wgpu(surface)) => { + compositor.configure_surface(surface, width, height); + } + } + } + + fn fetch_information(&self) -> Information { + match self { + Self::Wgpu(compositor) => compositor.fetch_information(), + } + } + + fn present>( + &mut self, + renderer: &mut Self::Renderer, + surface: &mut Self::Surface, + viewport: &Viewport, + background_color: Color, + overlay: &[T], + ) -> Result<(), SurfaceError> { + renderer.with_primitives(|backend, primitives| { + match (self, backend, surface) { + ( + Self::Wgpu(compositor), + Backend::Wgpu(backend), + Surface::Wgpu(surface), + ) => iced_wgpu::window::compositor::present( + compositor, + backend, + surface, + primitives, + viewport, + background_color, + overlay, + ), + } + }) + } +} diff --git a/src/application.rs b/src/application.rs index 9a1c1855..b9871556 100644 --- a/src/application.rs +++ b/src/application.rs @@ -198,11 +198,11 @@ pub trait Application: Sized { default_font: settings.default_font, default_text_size: settings.default_text_size, antialiasing: if settings.antialiasing { - Some(crate::renderer::settings::Antialiasing::MSAAx4) + Some(crate::renderer::Antialiasing::MSAAx4) } else { None }, - ..crate::renderer::Settings::from_env() + ..crate::renderer::Settings::default() }; Ok(crate::runtime::application::run::< diff --git a/src/lib.rs b/src/lib.rs index 31ec4f48..bb162f2d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -182,7 +182,7 @@ pub mod touch; pub mod widget; pub mod window; -use iced_wgpu as renderer; +use iced_renderer as renderer; use iced_winit as runtime; pub use iced_native::theme; diff --git a/wgpu/src/window.rs b/wgpu/src/window.rs index aac5fb9e..9545a14e 100644 --- a/wgpu/src/window.rs +++ b/wgpu/src/window.rs @@ -1,4 +1,5 @@ //! Display rendering results on windows. -mod compositor; +pub mod compositor; pub use compositor::Compositor; +pub use wgpu::Surface; diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 365cb603..7406bfb8 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -1,8 +1,9 @@ -use crate::{Backend, Color, Error, Renderer, Settings, Viewport}; +//! Connect a window with a renderer. +use crate::{Backend, Color, Error, Primitive, Renderer, Settings, Viewport}; use futures::stream::{self, StreamExt}; -use iced_graphics::compositor; +use iced_graphics::window::compositor; use iced_native::futures; use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; @@ -112,6 +113,77 @@ impl Compositor { } } +/// Creates a [`Compositor`] and its [`Backend`] for the given [`Settings`] and +/// window. +pub fn new( + settings: Settings, + compatible_window: Option<&W>, +) -> Result<(Compositor, Backend), Error> { + let compositor = futures::executor::block_on(Compositor::request( + settings, + compatible_window, + )) + .ok_or(Error::GraphicsAdapterNotFound)?; + + let backend = compositor.create_backend(); + + Ok((compositor, backend)) +} + +/// Presents the given primitives with the given [`Compositor`] and [`Backend`]. +pub fn present>( + compositor: &mut Compositor, + backend: &mut Backend, + surface: &mut wgpu::Surface, + primitives: &[Primitive], + viewport: &Viewport, + background_color: Color, + overlay: &[T], +) -> Result<(), compositor::SurfaceError> { + match surface.get_current_texture() { + Ok(frame) => { + let mut encoder = compositor.device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { + label: Some("iced_wgpu encoder"), + }, + ); + + let view = &frame + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + backend.present( + &compositor.device, + &compositor.queue, + &mut encoder, + Some(background_color), + view, + primitives, + viewport, + overlay, + ); + + // Submit work + let _submission = compositor.queue.submit(Some(encoder.finish())); + frame.present(); + + Ok(()) + } + Err(error) => match error { + wgpu::SurfaceError::Timeout => { + Err(compositor::SurfaceError::Timeout) + } + wgpu::SurfaceError::Outdated => { + Err(compositor::SurfaceError::Outdated) + } + wgpu::SurfaceError::Lost => Err(compositor::SurfaceError::Lost), + wgpu::SurfaceError::OutOfMemory => { + Err(compositor::SurfaceError::OutOfMemory) + } + }, + } +} + impl iced_graphics::window::Compositor for Compositor { type Settings = Settings; type Renderer = Renderer; @@ -121,13 +193,7 @@ impl iced_graphics::window::Compositor for Compositor { settings: Self::Settings, compatible_window: Option<&W>, ) -> Result<(Self, Self::Renderer), Error> { - let compositor = futures::executor::block_on(Self::request( - settings, - compatible_window, - )) - .ok_or(Error::GraphicsAdapterNotFound)?; - - let backend = compositor.create_backend(); + let (compositor, backend) = new(settings, compatible_window)?; Ok((compositor, Renderer::new(backend))) } @@ -178,49 +244,16 @@ impl iced_graphics::window::Compositor for Compositor { background_color: Color, overlay: &[T], ) -> Result<(), compositor::SurfaceError> { - match surface.get_current_texture() { - Ok(frame) => { - let mut encoder = self.device.create_command_encoder( - &wgpu::CommandEncoderDescriptor { - label: Some("iced_wgpu encoder"), - }, - ); - - let view = &frame - .texture - .create_view(&wgpu::TextureViewDescriptor::default()); - - renderer.with_primitives(|backend, primitives| { - backend.present( - &self.device, - &self.queue, - &mut encoder, - Some(background_color), - view, - primitives, - viewport, - overlay, - ); - }); - - // Submit work - let _submission = self.queue.submit(Some(encoder.finish())); - frame.present(); - - Ok(()) - } - Err(error) => match error { - wgpu::SurfaceError::Timeout => { - Err(compositor::SurfaceError::Timeout) - } - wgpu::SurfaceError::Outdated => { - Err(compositor::SurfaceError::Outdated) - } - wgpu::SurfaceError::Lost => Err(compositor::SurfaceError::Lost), - wgpu::SurfaceError::OutOfMemory => { - Err(compositor::SurfaceError::OutOfMemory) - } - }, - } + renderer.with_primitives(|backend, primitives| { + present( + self, + backend, + surface, + primitives, + viewport, + background_color, + overlay, + ) + }) } } diff --git a/winit/src/application.rs b/winit/src/application.rs index 889becad..1bfce3a1 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -17,8 +17,8 @@ use crate::{ use iced_futures::futures; use iced_futures::futures::channel::mpsc; -use iced_graphics::compositor; use iced_graphics::window; +use iced_graphics::window::compositor; use iced_native::program::Program; use iced_native::time::Instant; use iced_native::user_interface::{self, UserInterface}; diff --git a/winit/src/system.rs b/winit/src/system.rs index 619086b8..8a7b2a11 100644 --- a/winit/src/system.rs +++ b/winit/src/system.rs @@ -2,7 +2,7 @@ use crate::command::{self, Command}; pub use iced_native::system::*; -use iced_graphics::compositor; +use iced_graphics::window::compositor; /// Query for available system information. pub fn fetch_information( -- cgit From 1475f5fa58273e45e67ebd94642ae9e1251fe5f9 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 25 Feb 2023 15:04:40 +0100 Subject: Update `resvg` in `iced_graphics` --- graphics/Cargo.toml | 12 ++---------- graphics/src/image/vector.rs | 17 +++++++---------- 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index 13ab61d8..a37c99a2 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -11,7 +11,7 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"] categories = ["gui"] [features] -svg = ["resvg", "usvg", "tiny-skia"] +svg = ["resvg"] image = ["png", "jpeg", "jpeg_rayon", "gif", "webp", "bmp"] png = ["image_rs/png"] jpeg = ["image_rs/jpeg"] @@ -71,15 +71,7 @@ default-features = false optional = true [dependencies.resvg] -version = "0.18" -optional = true - -[dependencies.usvg] -version = "0.18" -optional = true - -[dependencies.tiny-skia] -version = "0.6" +version = "0.29" optional = true [dependencies.kamadak-exif] diff --git a/graphics/src/image/vector.rs b/graphics/src/image/vector.rs index 82d77aff..c950ccd6 100644 --- a/graphics/src/image/vector.rs +++ b/graphics/src/image/vector.rs @@ -5,6 +5,8 @@ use crate::Color; use iced_native::svg; use iced_native::Size; +use resvg::tiny_skia; +use resvg::usvg; use std::collections::{HashMap, HashSet}; use std::fs; @@ -21,7 +23,7 @@ impl Svg { pub fn viewport_dimensions(&self) -> Size { match self { Svg::Loaded(tree) => { - let size = tree.svg_node().size; + let size = tree.size; Size::new(size.width() as u32, size.height() as u32) } @@ -51,20 +53,14 @@ impl Cache { let svg = match handle.data() { svg::Data::Path(path) => { let tree = fs::read_to_string(path).ok().and_then(|contents| { - usvg::Tree::from_str( - &contents, - &usvg::Options::default().to_ref(), - ) - .ok() + usvg::Tree::from_str(&contents, &usvg::Options::default()) + .ok() }); tree.map(Svg::Loaded).unwrap_or(Svg::NotFound) } svg::Data::Bytes(bytes) => { - match usvg::Tree::from_data( - bytes, - &usvg::Options::default().to_ref(), - ) { + match usvg::Tree::from_data(bytes, &usvg::Options::default()) { Ok(tree) => Svg::Loaded(tree), Err(_) => Svg::NotFound, } @@ -125,6 +121,7 @@ impl Cache { } else { usvg::FitTo::Height(height) }, + tiny_skia::Transform::default(), img.as_mut(), )?; -- cgit From a01bc865a0561ff1daf255c4746746acf67524f0 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 25 Feb 2023 15:11:35 +0100 Subject: Trim measurements in `renderer::Backend` --- renderer/src/backend.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/renderer/src/backend.rs b/renderer/src/backend.rs index a9a09593..7b09eea8 100644 --- a/renderer/src/backend.rs +++ b/renderer/src/backend.rs @@ -9,7 +9,13 @@ pub enum Backend { Wgpu(iced_wgpu::Backend), } -impl iced_graphics::Backend for Backend {} +impl iced_graphics::Backend for Backend { + fn trim_measurements(&mut self) { + match self { + Self::Wgpu(backend) => backend.trim_measurements(), + } + } +} impl backend::Text for Backend { const ICON_FONT: Font = Font::Name("Iced-Icons"); -- cgit From 8c373cd497e370d356b480380482779397bdb510 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 25 Feb 2023 15:38:25 +0100 Subject: Scaffold `iced_tiny_skia` and connect it to `iced_renderer` --- Cargo.toml | 1 + renderer/Cargo.toml | 8 +++- renderer/src/backend.rs | 20 ++++++++ renderer/src/window/compositor.rs | 54 +++++++++++++++++---- tiny_skia/Cargo.toml | 21 +++++++++ tiny_skia/src/backend.rs | 87 ++++++++++++++++++++++++++++++++++ tiny_skia/src/lib.rs | 16 +++++++ tiny_skia/src/settings.rs | 24 ++++++++++ tiny_skia/src/window.rs | 3 ++ tiny_skia/src/window/compositor.rs | 96 ++++++++++++++++++++++++++++++++++++++ 10 files changed, 318 insertions(+), 12 deletions(-) create mode 100644 tiny_skia/Cargo.toml create mode 100644 tiny_skia/src/backend.rs create mode 100644 tiny_skia/src/lib.rs create mode 100644 tiny_skia/src/settings.rs create mode 100644 tiny_skia/src/window.rs create mode 100644 tiny_skia/src/window/compositor.rs diff --git a/Cargo.toml b/Cargo.toml index d999febe..b1f5cf66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,7 @@ members = [ "native", "renderer", "style", + "tiny_skia", "wgpu", "winit", "examples/*", diff --git a/renderer/Cargo.toml b/renderer/Cargo.toml index 2a179f3a..5ba5d426 100644 --- a/renderer/Cargo.toml +++ b/renderer/Cargo.toml @@ -4,8 +4,8 @@ version = "0.1.0" edition = "2021" [features] -image = ["iced_wgpu/image"] -svg = ["iced_wgpu/svg"] +image = ["iced_wgpu/image", "iced_tiny_skia/image"] +svg = ["iced_wgpu/svg", "iced_tiny_skia/svg"] tracing = ["iced_wgpu/tracing"] [dependencies] @@ -19,6 +19,10 @@ path = "../native" version = "0.7" path = "../graphics" +[dependencies.iced_tiny_skia] +version = "0.1" +path = "../tiny_skia" + [target.'cfg(not(target_arch = "wasm32"))'.dependencies] iced_wgpu = { version = "0.9", path = "../wgpu" } diff --git a/renderer/src/backend.rs b/renderer/src/backend.rs index 7b09eea8..a46d6f9b 100644 --- a/renderer/src/backend.rs +++ b/renderer/src/backend.rs @@ -7,12 +7,14 @@ use std::borrow::Cow; pub enum Backend { Wgpu(iced_wgpu::Backend), + TinySkia(iced_tiny_skia::Backend), } impl iced_graphics::Backend for Backend { fn trim_measurements(&mut self) { match self { Self::Wgpu(backend) => backend.trim_measurements(), + Self::TinySkia(backend) => backend.trim_measurements(), } } } @@ -25,12 +27,14 @@ impl backend::Text for Backend { fn default_font(&self) -> Font { match self { Self::Wgpu(backend) => backend.default_font(), + Self::TinySkia(backend) => backend.default_font(), } } fn default_size(&self) -> f32 { match self { Self::Wgpu(backend) => backend.default_size(), + Self::TinySkia(backend) => backend.default_size(), } } @@ -45,6 +49,9 @@ impl backend::Text for Backend { Self::Wgpu(backend) => { backend.measure(contents, size, font, bounds) } + Self::TinySkia(backend) => { + backend.measure(contents, size, font, bounds) + } } } @@ -66,6 +73,14 @@ impl backend::Text for Backend { position, nearest_only, ), + Self::TinySkia(backend) => backend.hit_test( + contents, + size, + font, + bounds, + position, + nearest_only, + ), } } @@ -74,6 +89,9 @@ impl backend::Text for Backend { Self::Wgpu(backend) => { backend.load_font(font); } + Self::TinySkia(backend) => { + backend.load_font(font); + } } } } @@ -83,6 +101,7 @@ impl backend::Image for Backend { fn dimensions(&self, handle: &iced_native::image::Handle) -> Size { match self { Self::Wgpu(backend) => backend.dimensions(handle), + Self::TinySkia(backend) => backend.dimensions(handle), } } } @@ -95,6 +114,7 @@ impl backend::Svg for Backend { ) -> Size { match self { Self::Wgpu(backend) => backend.viewport_dimensions(handle), + Self::TinySkia(backend) => backend.viewport_dimensions(handle), } } } diff --git a/renderer/src/window/compositor.rs b/renderer/src/window/compositor.rs index ad78d8bf..42afddc4 100644 --- a/renderer/src/window/compositor.rs +++ b/renderer/src/window/compositor.rs @@ -6,10 +6,12 @@ pub use iced_graphics::window::compositor::{Information, SurfaceError}; pub enum Compositor { Wgpu(iced_wgpu::window::Compositor), + TinySkia(iced_tiny_skia::window::Compositor), } pub enum Surface { Wgpu(iced_wgpu::window::Surface), + TinySkia(iced_tiny_skia::window::Surface), } impl iced_graphics::window::Compositor for Compositor { @@ -19,21 +21,31 @@ impl iced_graphics::window::Compositor for Compositor { fn new( settings: Self::Settings, - compatible_window: Option<&W>, + _compatible_window: Option<&W>, ) -> Result<(Self, Self::Renderer), Error> { - let (compositor, backend) = iced_wgpu::window::compositor::new( - iced_wgpu::Settings { + //let (compositor, backend) = iced_wgpu::window::compositor::new( + // iced_wgpu::Settings { + // default_font: settings.default_font, + // default_text_size: settings.default_text_size, + // antialiasing: settings.antialiasing, + // ..iced_wgpu::Settings::from_env() + // }, + // compatible_window, + //)?; + + //Ok(( + // Self::Wgpu(compositor), + // Renderer::new(Backend::Wgpu(backend)), + //)) + let (compositor, backend) = + iced_tiny_skia::window::compositor::new(iced_tiny_skia::Settings { default_font: settings.default_font, default_text_size: settings.default_text_size, - antialiasing: settings.antialiasing, - ..iced_wgpu::Settings::from_env() - }, - compatible_window, - )?; + }); Ok(( - Self::Wgpu(compositor), - Renderer::new(Backend::Wgpu(backend)), + Self::TinySkia(compositor), + Renderer::new(Backend::TinySkia(backend)), )) } @@ -45,6 +57,9 @@ impl iced_graphics::window::Compositor for Compositor { Self::Wgpu(compositor) => { Surface::Wgpu(compositor.create_surface(window)) } + Self::TinySkia(compositor) => { + Surface::TinySkia(compositor.create_surface(window)) + } } } @@ -58,12 +73,17 @@ impl iced_graphics::window::Compositor for Compositor { (Self::Wgpu(compositor), Surface::Wgpu(surface)) => { compositor.configure_surface(surface, width, height); } + (Self::TinySkia(compositor), Surface::TinySkia(surface)) => { + compositor.configure_surface(surface, width, height); + } + _ => unreachable!(), } } fn fetch_information(&self) -> Information { match self { Self::Wgpu(compositor) => compositor.fetch_information(), + Self::TinySkia(compositor) => compositor.fetch_information(), } } @@ -90,6 +110,20 @@ impl iced_graphics::window::Compositor for Compositor { background_color, overlay, ), + ( + Self::TinySkia(compositor), + Backend::TinySkia(backend), + Surface::TinySkia(surface), + ) => iced_tiny_skia::window::compositor::present( + compositor, + backend, + surface, + primitives, + viewport, + background_color, + overlay, + ), + _ => unreachable!(), } }) } diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml new file mode 100644 index 00000000..7f4b0f8c --- /dev/null +++ b/tiny_skia/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "iced_tiny_skia" +version = "0.1.0" +edition = "2021" + +[features] +image = [] +svg = [] + +[dependencies] +raw-window-handle = "0.5" +softbuffer = "0.2" +tiny-skia = "0.8" + +[dependencies.iced_native] +version = "0.9" +path = "../native" + +[dependencies.iced_graphics] +version = "0.7" +path = "../graphics" diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs new file mode 100644 index 00000000..4282a745 --- /dev/null +++ b/tiny_skia/src/backend.rs @@ -0,0 +1,87 @@ +use crate::{Font, Settings, Size}; + +use iced_graphics::backend; +use iced_graphics::text; + +use std::borrow::Cow; + +pub struct Backend { + default_font: Font, + default_text_size: f32, +} + +impl Backend { + pub fn new(settings: Settings) -> Self { + Self { + default_font: settings.default_font, + default_text_size: settings.default_text_size, + } + } +} + +impl iced_graphics::Backend for Backend { + fn trim_measurements(&mut self) { + // TODO + } +} + +impl backend::Text for Backend { + const ICON_FONT: Font = Font::Name("Iced-Icons"); + const CHECKMARK_ICON: char = '\u{f00c}'; + const ARROW_DOWN_ICON: char = '\u{e800}'; + + fn default_font(&self) -> Font { + self.default_font + } + + fn default_size(&self) -> f32 { + self.default_text_size + } + + fn measure( + &self, + _contents: &str, + _size: f32, + _font: Font, + _bounds: Size, + ) -> (f32, f32) { + // TODO + (0.0, 0.0) + } + + fn hit_test( + &self, + _contents: &str, + _size: f32, + _font: Font, + _bounds: Size, + _point: iced_native::Point, + _nearest_only: bool, + ) -> Option { + // TODO + None + } + + fn load_font(&mut self, _font: Cow<'static, [u8]>) { + // TODO + } +} + +#[cfg(feature = "image")] +impl backend::Image for Backend { + fn dimensions(&self, _handle: &iced_native::image::Handle) -> Size { + // TODO + Size::new(0, 0) + } +} + +#[cfg(feature = "svg")] +impl backend::Svg for Backend { + fn viewport_dimensions( + &self, + _handle: &iced_native::svg::Handle, + ) -> Size { + // TODO + Size::new(0, 0) + } +} diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs new file mode 100644 index 00000000..fce44e9e --- /dev/null +++ b/tiny_skia/src/lib.rs @@ -0,0 +1,16 @@ +pub mod window; + +mod backend; +mod settings; + +pub use backend::Backend; +pub use settings::Settings; + +pub use iced_graphics::{Color, Error, Font, Point, Size, Vector, Viewport}; + +/// A [`tiny-skia`] graphics renderer for [`iced`]. +/// +/// [`tiny-skia`]: https://github.com/RazrFalcon/tiny-skia +/// [`iced`]: https://github.com/iced-rs/iced +pub type Renderer = + iced_graphics::Renderer; diff --git a/tiny_skia/src/settings.rs b/tiny_skia/src/settings.rs new file mode 100644 index 00000000..88098345 --- /dev/null +++ b/tiny_skia/src/settings.rs @@ -0,0 +1,24 @@ +use crate::Font; + +/// The settings of a [`Backend`]. +/// +/// [`Backend`]: crate::Backend +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Settings { + /// The default [`Font`] to use. + pub default_font: Font, + + /// The default size of text. + /// + /// By default, it will be set to `16.0`. + pub default_text_size: f32, +} + +impl Default for Settings { + fn default() -> Settings { + Settings { + default_font: Font::SansSerif, + default_text_size: 16.0, + } + } +} diff --git a/tiny_skia/src/window.rs b/tiny_skia/src/window.rs new file mode 100644 index 00000000..d8d9378e --- /dev/null +++ b/tiny_skia/src/window.rs @@ -0,0 +1,3 @@ +pub mod compositor; + +pub use compositor::{Compositor, Surface}; diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs new file mode 100644 index 00000000..053bb29b --- /dev/null +++ b/tiny_skia/src/window/compositor.rs @@ -0,0 +1,96 @@ +use crate::{Backend, Color, Error, Renderer, Settings, Viewport}; + +use iced_graphics::window::compositor::{self, Information, SurfaceError}; +use iced_graphics::Primitive; + +use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; +use std::marker::PhantomData; + +pub struct Compositor { + _theme: PhantomData, +} + +pub struct Surface; + +impl iced_graphics::window::Compositor for Compositor { + type Settings = Settings; + type Renderer = Renderer; + type Surface = Surface; + + fn new( + settings: Self::Settings, + _compatible_window: Option<&W>, + ) -> Result<(Self, Self::Renderer), Error> { + let (compositor, backend) = new(settings); + + Ok((compositor, Renderer::new(backend))) + } + + fn create_surface( + &mut self, + _window: &W, + ) -> Surface { + // TODO + Surface + } + + fn configure_surface( + &mut self, + _surface: &mut Surface, + _width: u32, + _height: u32, + ) { + // TODO + } + + fn fetch_information(&self) -> Information { + Information { + adapter: String::from("CPU"), + backend: String::from("tiny-skia"), + } + } + + fn present>( + &mut self, + renderer: &mut Self::Renderer, + surface: &mut Self::Surface, + viewport: &Viewport, + background_color: Color, + overlay: &[T], + ) -> Result<(), SurfaceError> { + renderer.with_primitives(|backend, primitives| { + present( + self, + backend, + surface, + primitives, + viewport, + background_color, + overlay, + ) + }) + } +} + +pub fn new(settings: Settings) -> (Compositor, Backend) { + // TODO + ( + Compositor { + _theme: PhantomData, + }, + Backend::new(settings), + ) +} + +pub fn present>( + _compositor: &mut Compositor, + _backend: &mut Backend, + _surface: &mut Surface, + _primitives: &[Primitive], + _viewport: &Viewport, + _background_color: Color, + _overlay: &[T], +) -> Result<(), compositor::SurfaceError> { + // TODO + Ok(()) +} -- cgit From 535d7a4d57e131e661587b36e41820dd6ccccc3e Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 25 Feb 2023 16:05:42 +0100 Subject: Implement basic presentation with `softbuffer` for `iced_tiny_skia` --- graphics/src/window/compositor.rs | 2 ++ renderer/src/window/compositor.rs | 10 +++--- tiny_skia/src/backend.rs | 19 ++++++++++- tiny_skia/src/window/compositor.rs | 66 ++++++++++++++++++++++++++++++-------- wgpu/src/window/compositor.rs | 10 ++++-- winit/src/application.rs | 13 +++----- 6 files changed, 90 insertions(+), 30 deletions(-) diff --git a/graphics/src/window/compositor.rs b/graphics/src/window/compositor.rs index db4ba45d..15f8dab5 100644 --- a/graphics/src/window/compositor.rs +++ b/graphics/src/window/compositor.rs @@ -28,6 +28,8 @@ pub trait Compositor: Sized { fn create_surface( &mut self, window: &W, + width: u32, + height: u32, ) -> Self::Surface; /// Configures a new [`Surface`] with the given dimensions. diff --git a/renderer/src/window/compositor.rs b/renderer/src/window/compositor.rs index 42afddc4..a11374ed 100644 --- a/renderer/src/window/compositor.rs +++ b/renderer/src/window/compositor.rs @@ -52,14 +52,16 @@ impl iced_graphics::window::Compositor for Compositor { fn create_surface( &mut self, window: &W, + width: u32, + height: u32, ) -> Surface { match self { Self::Wgpu(compositor) => { - Surface::Wgpu(compositor.create_surface(window)) - } - Self::TinySkia(compositor) => { - Surface::TinySkia(compositor.create_surface(window)) + Surface::Wgpu(compositor.create_surface(window, width, height)) } + Self::TinySkia(compositor) => Surface::TinySkia( + compositor.create_surface(window, width, height), + ), } } diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 4282a745..62373ec7 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -1,7 +1,8 @@ -use crate::{Font, Settings, Size}; +use crate::{Color, Font, Settings, Size, Viewport}; use iced_graphics::backend; use iced_graphics::text; +use iced_graphics::Primitive; use std::borrow::Cow; @@ -17,6 +18,22 @@ impl Backend { default_text_size: settings.default_text_size, } } + + pub fn draw>( + &mut self, + pixels: &mut tiny_skia::Pixmap, + _primitives: &[Primitive], + _viewport: &Viewport, + background_color: Color, + _overlay: &[T], + ) { + pixels.fill(into_color(background_color)); + } +} + +fn into_color(color: Color) -> tiny_skia::Color { + tiny_skia::Color::from_rgba(color.r, color.g, color.b, color.a) + .expect("Convert color from iced to tiny_skia") } impl iced_graphics::Backend for Backend { diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 053bb29b..164d99c6 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -10,7 +10,11 @@ pub struct Compositor { _theme: PhantomData, } -pub struct Surface; +pub struct Surface { + window: softbuffer::GraphicsContext, + pixels: tiny_skia::Pixmap, + buffer: Vec, +} impl iced_graphics::window::Compositor for Compositor { type Settings = Settings; @@ -28,19 +32,33 @@ impl iced_graphics::window::Compositor for Compositor { fn create_surface( &mut self, - _window: &W, + window: &W, + width: u32, + height: u32, ) -> Surface { - // TODO - Surface + let window = + unsafe { softbuffer::GraphicsContext::new(window, window) } + .expect("Create softbuffer for window"); + + let pixels = tiny_skia::Pixmap::new(width, height) + .expect("Create pixmap for window"); + + Surface { + window, + pixels, + buffer: vec![0; (width * height) as usize], + } } fn configure_surface( &mut self, - _surface: &mut Surface, - _width: u32, - _height: u32, + surface: &mut Surface, + width: u32, + height: u32, ) { - // TODO + surface.pixels = tiny_skia::Pixmap::new(width, height) + .expect("Create pixmap for window"); + surface.buffer = vec![0; (width * height) as usize]; } fn fetch_information(&self) -> Information { @@ -84,13 +102,33 @@ pub fn new(settings: Settings) -> (Compositor, Backend) { pub fn present>( _compositor: &mut Compositor, - _backend: &mut Backend, - _surface: &mut Surface, - _primitives: &[Primitive], - _viewport: &Viewport, - _background_color: Color, - _overlay: &[T], + backend: &mut Backend, + surface: &mut Surface, + primitives: &[Primitive], + viewport: &Viewport, + background_color: Color, + overlay: &[T], ) -> Result<(), compositor::SurfaceError> { + backend.draw( + &mut surface.pixels, + primitives, + viewport, + background_color, + overlay, + ); + + for (i, pixel) in surface.pixels.pixels_mut().iter().enumerate() { + surface.buffer[i] = u32::from(pixel.red()) << 16 + | u32::from(pixel.green()) << 8 + | u32::from(pixel.blue()); + } + + surface.window.set_buffer( + &surface.buffer, + surface.pixels.width() as u16, + surface.pixels.height() as u16, + ); + // TODO Ok(()) } diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 7406bfb8..3a4a7123 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -201,11 +201,15 @@ impl iced_graphics::window::Compositor for Compositor { fn create_surface( &mut self, window: &W, + width: u32, + height: u32, ) -> wgpu::Surface { #[allow(unsafe_code)] - unsafe { - self.instance.create_surface(window) - } + let mut surface = unsafe { self.instance.create_surface(window) }; + + self.configure_surface(&mut surface, width, height); + + surface } fn configure_surface( diff --git a/winit/src/application.rs b/winit/src/application.rs index 1bfce3a1..b52f0197 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -285,21 +285,18 @@ async fn run_instance( use winit::event; use winit::event_loop::ControlFlow; - let mut clipboard = Clipboard::connect(&window); - let mut cache = user_interface::Cache::default(); - let mut surface = compositor.create_surface(&window); - let mut should_exit = false; - let mut state = State::new(&application, &window); let mut viewport_version = state.viewport_version(); - let physical_size = state.physical_size(); - compositor.configure_surface( - &mut surface, + let mut clipboard = Clipboard::connect(&window); + let mut cache = user_interface::Cache::default(); + let mut surface = compositor.create_surface( + &window, physical_size.width, physical_size.height, ); + let mut should_exit = false; if should_be_visible { window.set_visible(true); -- cgit From 445b31c6c5f16ecc9f07bd072f246e827aa5b854 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 25 Feb 2023 21:06:58 +0100 Subject: Resize `Surface::buffer` instead of reallocating in `iced_tiny_skia` --- tiny_skia/src/window/compositor.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 164d99c6..8bb52a03 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -58,7 +58,8 @@ impl iced_graphics::window::Compositor for Compositor { ) { surface.pixels = tiny_skia::Pixmap::new(width, height) .expect("Create pixmap for window"); - surface.buffer = vec![0; (width * height) as usize]; + + surface.buffer.resize((width * height) as usize, 0); } fn fetch_information(&self) -> Information { -- cgit From 871b7e0311c73358213aae6326973300f0b56faa Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 25 Feb 2023 21:28:21 +0100 Subject: Fix `Padding::fit` on irregular values for an axis --- core/src/padding.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/core/src/padding.rs b/core/src/padding.rs index 752b2b86..0b1bba13 100644 --- a/core/src/padding.rs +++ b/core/src/padding.rs @@ -77,12 +77,14 @@ impl Padding { /// Fits the [`Padding`] between the provided `inner` and `outer` [`Size`]. pub fn fit(self, inner: Size, outer: Size) -> Self { let available = (outer - inner).max(Size::ZERO); + let new_top = self.top.min(available.height); + let new_left = self.left.min(available.width); Padding { - top: self.top.min(available.height / 2.0), - right: self.right.min(available.width / 2.0), - bottom: self.bottom.min(available.height / 2.0), - left: self.left.min(available.width / 2.0), + top: new_top, + bottom: self.bottom.min(available.height - new_top), + left: new_left, + right: self.right.min(available.width - new_left), } } } -- cgit From 5f93437285c4451a8b5ffac1b3c9e1b91e8a5918 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 25 Feb 2023 21:43:53 +0100 Subject: Bump version of `iced_core` :tada: --- core/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/Cargo.toml b/core/Cargo.toml index 43865e4d..0d6310d3 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_core" -version = "0.8.0" +version = "0.8.1" authors = ["Héctor Ramón Jiménez "] edition = "2021" description = "The essential concepts of Iced" -- cgit From df5d66423de141a009bbed993d99d491ed6373c9 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 26 Feb 2023 00:38:46 +0100 Subject: Draft support for `Quad` and `Clip` primitives in `iced_tiny_skia` --- tiny_skia/src/backend.rs | 153 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 150 insertions(+), 3 deletions(-) diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 62373ec7..54752a21 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -2,7 +2,7 @@ use crate::{Color, Font, Settings, Size, Viewport}; use iced_graphics::backend; use iced_graphics::text; -use iced_graphics::Primitive; +use iced_graphics::{Background, Primitive, Rectangle, Vector}; use std::borrow::Cow; @@ -22,12 +22,133 @@ impl Backend { pub fn draw>( &mut self, pixels: &mut tiny_skia::Pixmap, - _primitives: &[Primitive], - _viewport: &Viewport, + primitives: &[Primitive], + viewport: &Viewport, background_color: Color, _overlay: &[T], ) { pixels.fill(into_color(background_color)); + + let scale_factor = viewport.scale_factor() as f32; + + for primitive in primitives { + draw_primitive(primitive, pixels, None, scale_factor, Vector::ZERO); + } + } +} + +fn draw_primitive( + primitive: &Primitive, + pixels: &mut tiny_skia::Pixmap, + clip_mask: Option<&tiny_skia::ClipMask>, + scale_factor: f32, + translation: Vector, +) { + match primitive { + Primitive::None => {} + Primitive::Quad { + bounds, + background, + border_radius: _, // TODO + border_width, + border_color, + } => { + let transform = tiny_skia::Transform::from_translate( + translation.x, + translation.y, + ) + .post_scale(scale_factor, scale_factor); + + let path = tiny_skia::PathBuilder::from_rect( + tiny_skia::Rect::from_xywh( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + ) + .expect("Create quad rectangle"), + ); + + pixels.fill_path( + &path, + &tiny_skia::Paint { + shader: match background { + Background::Color(color) => { + tiny_skia::Shader::SolidColor(into_color(*color)) + } + }, + anti_alias: true, + ..tiny_skia::Paint::default() + }, + tiny_skia::FillRule::EvenOdd, + transform, + clip_mask, + ); + + if *border_width > 0.0 { + pixels.stroke_path( + &path, + &tiny_skia::Paint { + shader: tiny_skia::Shader::SolidColor(into_color( + *border_color, + )), + anti_alias: true, + ..tiny_skia::Paint::default() + }, + &tiny_skia::Stroke { + width: *border_width, + ..tiny_skia::Stroke::default() + }, + transform, + clip_mask, + ); + } + } + Primitive::Text { .. } => { + // TODO + } + Primitive::Image { .. } => { + // TODO + } + Primitive::Svg { .. } => { + // TODO + } + Primitive::Group { primitives } => { + for primitive in primitives { + draw_primitive( + primitive, + pixels, + clip_mask, + scale_factor, + translation, + ); + } + } + Primitive::Translate { + translation: offset, + content, + } => { + draw_primitive( + content, + pixels, + clip_mask, + scale_factor, + translation + *offset, + ); + } + Primitive::Clip { bounds, content } => { + draw_primitive( + content, + pixels, + Some(&rectangular_clip_mask(pixels, *bounds * scale_factor)), + scale_factor, + translation, + ); + } + Primitive::Cached { cache } => { + draw_primitive(cache, pixels, clip_mask, scale_factor, translation); + } + Primitive::SolidMesh { .. } | Primitive::GradientMesh { .. } => {} } } @@ -36,6 +157,32 @@ fn into_color(color: Color) -> tiny_skia::Color { .expect("Convert color from iced to tiny_skia") } +fn rectangular_clip_mask( + pixels: &tiny_skia::Pixmap, + bounds: Rectangle, +) -> tiny_skia::ClipMask { + let mut clip_mask = tiny_skia::ClipMask::new(); + + let path = { + let mut builder = tiny_skia::PathBuilder::new(); + builder.push_rect(bounds.x, bounds.y, bounds.width, bounds.height); + + builder.finish().unwrap() + }; + + clip_mask + .set_path( + pixels.width(), + pixels.height(), + &path, + tiny_skia::FillRule::EvenOdd, + true, + ) + .expect("Set path of clipping area"); + + clip_mask +} + impl iced_graphics::Backend for Backend { fn trim_measurements(&mut self) { // TODO -- cgit From 744f3028f484c44899fed56d9190387569828a95 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 26 Feb 2023 00:49:27 +0100 Subject: Use `Surface::buffer` directly for drawing in `iced_tiny_skia` ... with a nice little color trick :^) --- tiny_skia/Cargo.toml | 1 + tiny_skia/src/backend.rs | 8 ++++---- tiny_skia/src/window/compositor.rs | 30 +++++++++++------------------- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml index 7f4b0f8c..70b1f5e7 100644 --- a/tiny_skia/Cargo.toml +++ b/tiny_skia/Cargo.toml @@ -11,6 +11,7 @@ svg = [] raw-window-handle = "0.5" softbuffer = "0.2" tiny-skia = "0.8" +bytemuck = "1" [dependencies.iced_native] version = "0.9" diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 54752a21..9eea1a32 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -21,7 +21,7 @@ impl Backend { pub fn draw>( &mut self, - pixels: &mut tiny_skia::Pixmap, + pixels: &mut tiny_skia::PixmapMut<'_>, primitives: &[Primitive], viewport: &Viewport, background_color: Color, @@ -39,7 +39,7 @@ impl Backend { fn draw_primitive( primitive: &Primitive, - pixels: &mut tiny_skia::Pixmap, + pixels: &mut tiny_skia::PixmapMut<'_>, clip_mask: Option<&tiny_skia::ClipMask>, scale_factor: f32, translation: Vector, @@ -153,12 +153,12 @@ fn draw_primitive( } fn into_color(color: Color) -> tiny_skia::Color { - tiny_skia::Color::from_rgba(color.r, color.g, color.b, color.a) + tiny_skia::Color::from_rgba(color.b, color.g, color.r, color.a) .expect("Convert color from iced to tiny_skia") } fn rectangular_clip_mask( - pixels: &tiny_skia::Pixmap, + pixels: &tiny_skia::PixmapMut<'_>, bounds: Rectangle, ) -> tiny_skia::ClipMask { let mut clip_mask = tiny_skia::ClipMask::new(); diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 8bb52a03..2bd5831e 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -12,7 +12,6 @@ pub struct Compositor { pub struct Surface { window: softbuffer::GraphicsContext, - pixels: tiny_skia::Pixmap, buffer: Vec, } @@ -40,13 +39,9 @@ impl iced_graphics::window::Compositor for Compositor { unsafe { softbuffer::GraphicsContext::new(window, window) } .expect("Create softbuffer for window"); - let pixels = tiny_skia::Pixmap::new(width, height) - .expect("Create pixmap for window"); - Surface { window, - pixels, - buffer: vec![0; (width * height) as usize], + buffer: vec![0; width as usize * height as usize], } } @@ -56,9 +51,6 @@ impl iced_graphics::window::Compositor for Compositor { width: u32, height: u32, ) { - surface.pixels = tiny_skia::Pixmap::new(width, height) - .expect("Create pixmap for window"); - surface.buffer.resize((width * height) as usize, 0); } @@ -110,26 +102,26 @@ pub fn present>( background_color: Color, overlay: &[T], ) -> Result<(), compositor::SurfaceError> { + let physical_size = viewport.physical_size(); + backend.draw( - &mut surface.pixels, + &mut tiny_skia::PixmapMut::from_bytes( + bytemuck::cast_slice_mut(&mut surface.buffer), + physical_size.width, + physical_size.height, + ) + .expect("Create pixel map"), primitives, viewport, background_color, overlay, ); - for (i, pixel) in surface.pixels.pixels_mut().iter().enumerate() { - surface.buffer[i] = u32::from(pixel.red()) << 16 - | u32::from(pixel.green()) << 8 - | u32::from(pixel.blue()); - } - surface.window.set_buffer( &surface.buffer, - surface.pixels.width() as u16, - surface.pixels.height() as u16, + physical_size.width as u16, + physical_size.height as u16, ); - // TODO Ok(()) } -- cgit From 64fb722dfe8769d4a92edb0133f1863383ecfd86 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 26 Feb 2023 23:40:17 +0100 Subject: Draft text support in `iced_tiny_skia` --- tiny_skia/Cargo.toml | 15 ++ tiny_skia/src/backend.rs | 269 +++++++++++++++++++++--------------- tiny_skia/src/lib.rs | 1 + tiny_skia/src/text.rs | 350 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 523 insertions(+), 112 deletions(-) create mode 100644 tiny_skia/src/text.rs diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml index 70b1f5e7..55f7f5b7 100644 --- a/tiny_skia/Cargo.toml +++ b/tiny_skia/Cargo.toml @@ -12,6 +12,8 @@ raw-window-handle = "0.5" softbuffer = "0.2" tiny-skia = "0.8" bytemuck = "1" +rustc-hash = "1.1" +ouroboros = "0.15" [dependencies.iced_native] version = "0.9" @@ -20,3 +22,16 @@ path = "../native" [dependencies.iced_graphics] version = "0.7" path = "../graphics" + +[dependencies.cosmic-text] +features = ["std", "swash"] +git = "https://github.com/hecrj/cosmic-text" +rev = "dc83efbf00a2efb4118403538e8a47bfd69c3e5e" + +[dependencies.twox-hash] +version = "1.6" +default-features = false + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies.twox-hash] +version = "1.6.1" +features = ["std"] diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 9eea1a32..5e743479 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -9,6 +9,7 @@ use std::borrow::Cow; pub struct Backend { default_font: Font, default_text_size: f32, + text_pipeline: crate::text::Pipeline, } impl Backend { @@ -16,6 +17,7 @@ impl Backend { Self { default_font: settings.default_font, default_text_size: settings.default_text_size, + text_pipeline: crate::text::Pipeline::new(), } } @@ -32,123 +34,161 @@ impl Backend { let scale_factor = viewport.scale_factor() as f32; for primitive in primitives { - draw_primitive(primitive, pixels, None, scale_factor, Vector::ZERO); + self.draw_primitive( + primitive, + pixels, + None, + scale_factor, + Vector::ZERO, + ); } + + self.text_pipeline.end_frame(); } -} -fn draw_primitive( - primitive: &Primitive, - pixels: &mut tiny_skia::PixmapMut<'_>, - clip_mask: Option<&tiny_skia::ClipMask>, - scale_factor: f32, - translation: Vector, -) { - match primitive { - Primitive::None => {} - Primitive::Quad { - bounds, - background, - border_radius: _, // TODO - border_width, - border_color, - } => { - let transform = tiny_skia::Transform::from_translate( - translation.x, - translation.y, - ) - .post_scale(scale_factor, scale_factor); - - let path = tiny_skia::PathBuilder::from_rect( - tiny_skia::Rect::from_xywh( - bounds.x, - bounds.y, - bounds.width, - bounds.height, + fn draw_primitive( + &mut self, + primitive: &Primitive, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: Option<&tiny_skia::ClipMask>, + scale_factor: f32, + translation: Vector, + ) { + match primitive { + Primitive::None => {} + Primitive::Quad { + bounds, + background, + border_radius: _, // TODO + border_width, + border_color, + } => { + let transform = tiny_skia::Transform::from_translate( + translation.x, + translation.y, ) - .expect("Create quad rectangle"), - ); + .post_scale(scale_factor, scale_factor); - pixels.fill_path( - &path, - &tiny_skia::Paint { - shader: match background { - Background::Color(color) => { - tiny_skia::Shader::SolidColor(into_color(*color)) - } - }, - anti_alias: true, - ..tiny_skia::Paint::default() - }, - tiny_skia::FillRule::EvenOdd, - transform, - clip_mask, - ); + let path = tiny_skia::PathBuilder::from_rect( + tiny_skia::Rect::from_xywh( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + ) + .expect("Create quad rectangle"), + ); - if *border_width > 0.0 { - pixels.stroke_path( + pixels.fill_path( &path, &tiny_skia::Paint { - shader: tiny_skia::Shader::SolidColor(into_color( - *border_color, - )), + shader: match background { + Background::Color(color) => { + tiny_skia::Shader::SolidColor(into_color( + *color, + )) + } + }, anti_alias: true, ..tiny_skia::Paint::default() }, - &tiny_skia::Stroke { - width: *border_width, - ..tiny_skia::Stroke::default() - }, + tiny_skia::FillRule::EvenOdd, transform, clip_mask, ); + + if *border_width > 0.0 { + pixels.stroke_path( + &path, + &tiny_skia::Paint { + shader: tiny_skia::Shader::SolidColor(into_color( + *border_color, + )), + anti_alias: true, + ..tiny_skia::Paint::default() + }, + &tiny_skia::Stroke { + width: *border_width, + ..tiny_skia::Stroke::default() + }, + transform, + clip_mask, + ); + } } - } - Primitive::Text { .. } => { - // TODO - } - Primitive::Image { .. } => { - // TODO - } - Primitive::Svg { .. } => { - // TODO - } - Primitive::Group { primitives } => { - for primitive in primitives { - draw_primitive( - primitive, + Primitive::Text { + content, + bounds, + color, + size, + font, + horizontal_alignment, + vertical_alignment, + } => { + self.text_pipeline.draw( + content, + (*bounds + translation) * scale_factor, + *color, + *size * scale_factor, + *font, + *horizontal_alignment, + *vertical_alignment, + pixels, + clip_mask, + ); + } + Primitive::Image { .. } => { + // TODO + } + Primitive::Svg { .. } => { + // TODO + } + Primitive::Group { primitives } => { + for primitive in primitives { + self.draw_primitive( + primitive, + pixels, + clip_mask, + scale_factor, + translation, + ); + } + } + Primitive::Translate { + translation: offset, + content, + } => { + self.draw_primitive( + content, pixels, clip_mask, scale_factor, + translation + *offset, + ); + } + Primitive::Clip { bounds, content } => { + self.draw_primitive( + content, + pixels, + Some(&rectangular_clip_mask( + pixels, + *bounds * scale_factor, + )), + scale_factor, translation, ); } + Primitive::Cached { cache } => { + self.draw_primitive( + cache, + pixels, + clip_mask, + scale_factor, + translation, + ); + } + Primitive::SolidMesh { .. } | Primitive::GradientMesh { .. } => {} } - Primitive::Translate { - translation: offset, - content, - } => { - draw_primitive( - content, - pixels, - clip_mask, - scale_factor, - translation + *offset, - ); - } - Primitive::Clip { bounds, content } => { - draw_primitive( - content, - pixels, - Some(&rectangular_clip_mask(pixels, *bounds * scale_factor)), - scale_factor, - translation, - ); - } - Primitive::Cached { cache } => { - draw_primitive(cache, pixels, clip_mask, scale_factor, translation); - } - Primitive::SolidMesh { .. } | Primitive::GradientMesh { .. } => {} } } @@ -185,7 +225,7 @@ fn rectangular_clip_mask( impl iced_graphics::Backend for Backend { fn trim_measurements(&mut self) { - // TODO + self.text_pipeline.trim_measurement_cache(); } } @@ -204,30 +244,35 @@ impl backend::Text for Backend { fn measure( &self, - _contents: &str, - _size: f32, - _font: Font, - _bounds: Size, + contents: &str, + size: f32, + font: Font, + bounds: Size, ) -> (f32, f32) { - // TODO - (0.0, 0.0) + self.text_pipeline.measure(contents, size, font, bounds) } fn hit_test( &self, - _contents: &str, - _size: f32, - _font: Font, - _bounds: Size, - _point: iced_native::Point, - _nearest_only: bool, + contents: &str, + size: f32, + font: Font, + bounds: Size, + point: iced_native::Point, + nearest_only: bool, ) -> Option { - // TODO - None + self.text_pipeline.hit_test( + contents, + size, + font, + bounds, + point, + nearest_only, + ) } - fn load_font(&mut self, _font: Cow<'static, [u8]>) { - // TODO + fn load_font(&mut self, font: Cow<'static, [u8]>) { + self.text_pipeline.load_font(font); } } diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index fce44e9e..420a1ffb 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -2,6 +2,7 @@ pub mod window; mod backend; mod settings; +mod text; pub use backend::Backend; pub use settings::Settings; diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs new file mode 100644 index 00000000..5bd6eff1 --- /dev/null +++ b/tiny_skia/src/text.rs @@ -0,0 +1,350 @@ +pub use iced_native::text::Hit; + +use iced_native::alignment; +use iced_native::{Color, Font, Rectangle, Size}; + +use rustc_hash::{FxHashMap, FxHashSet}; +use std::borrow::Cow; +use std::cell::RefCell; +use std::collections::hash_map; +use std::hash::{BuildHasher, Hash, Hasher}; +use std::sync::Arc; + +#[allow(missing_debug_implementations)] +pub struct Pipeline { + system: Option, +} + +#[ouroboros::self_referencing] +struct System { + fonts: cosmic_text::FontSystem, + + #[borrows(fonts)] + #[not_covariant] + measurement_cache: RefCell>, + + #[borrows(fonts)] + #[not_covariant] + render_cache: Cache<'this>, +} + +impl Pipeline { + pub fn new() -> Self { + Pipeline { + system: Some( + SystemBuilder { + fonts: cosmic_text::FontSystem::new_with_fonts( + [cosmic_text::fontdb::Source::Binary(Arc::new( + include_bytes!("../../wgpu/fonts/Iced-Icons.ttf") + .as_slice(), + ))] + .into_iter(), + ), + measurement_cache_builder: |_| RefCell::new(Cache::new()), + render_cache_builder: |_| Cache::new(), + } + .build(), + ), + } + } + + pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) { + let heads = self.system.take().unwrap().into_heads(); + + let (locale, mut db) = heads.fonts.into_locale_and_db(); + + db.load_font_source(cosmic_text::fontdb::Source::Binary(Arc::new( + bytes.into_owned(), + ))); + + self.system = Some( + SystemBuilder { + fonts: cosmic_text::FontSystem::new_with_locale_and_db( + locale, db, + ), + measurement_cache_builder: |_| RefCell::new(Cache::new()), + render_cache_builder: |_| Cache::new(), + } + .build(), + ); + } + + pub fn draw( + &mut self, + content: &str, + bounds: Rectangle, + color: Color, + size: f32, + font: Font, + _horizontal_alignment: alignment::Horizontal, // TODO + _vertical_alignment: alignment::Vertical, // TODO + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: Option<&tiny_skia::ClipMask>, + ) { + self.system.as_mut().unwrap().with_mut(|fields| { + let key = Key { + bounds: bounds.size(), + content, + font, + size, + }; + + let (_, buffer) = fields.render_cache.allocate(&fields.fonts, key); + + let mut swash = cosmic_text::SwashCache::new(&fields.fonts); + + for run in buffer.layout_runs() { + for glyph in run.glyphs { + // TODO: Outline support + if let Some(image) = swash.get_image(glyph.cache_key) { + let glyph_size = image.placement.width as usize + * image.placement.height as usize; + + if glyph_size == 0 { + continue; + } + + // TODO: Cache glyph rasterization + let mut buffer = vec![0u32; glyph_size]; + + match image.content { + cosmic_text::SwashContent::Mask => { + let mut i = 0; + + // TODO: Blend alpha + let [r, g, b, _a] = color.into_rgba8(); + + for _y in 0..image.placement.height { + for _x in 0..image.placement.width { + buffer[i] = + tiny_skia::ColorU8::from_rgba( + b, + g, + r, + image.data[i], + ) + .premultiply() + .get(); + + i += 1; + } + } + } + cosmic_text::SwashContent::Color => { + let mut i = 0; + + for _y in 0..image.placement.height { + for _x in 0..image.placement.width { + // TODO: Blend alpha + buffer[i] = (image.data[i + 3] as u32) + << 24 + | (image.data[i + 2] as u32) << 16 + | (image.data[i + 1] as u32) << 8 + | image.data[i] as u32; + + i += 1; + } + } + } + cosmic_text::SwashContent::SubpixelMask => { + // TODO + } + } + + let pixmap = tiny_skia::PixmapRef::from_bytes( + bytemuck::cast_slice(&buffer), + image.placement.width, + image.placement.height, + ) + .expect("Create glyph pixel map"); + + pixels.draw_pixmap( + bounds.x as i32 + + glyph.x_int + + image.placement.left, + bounds.y as i32 - glyph.y_int - image.placement.top + + run.line_y as i32, + pixmap, + &tiny_skia::PixmapPaint::default(), + tiny_skia::Transform::identity(), + clip_mask, + ); + } + } + } + }); + } + + pub fn end_frame(&mut self) { + self.system + .as_mut() + .unwrap() + .with_render_cache_mut(|cache| cache.trim()); + } + + pub fn measure( + &self, + content: &str, + size: f32, + font: Font, + bounds: Size, + ) -> (f32, f32) { + self.system.as_ref().unwrap().with(|fields| { + let mut measurement_cache = fields.measurement_cache.borrow_mut(); + + let (_, paragraph) = measurement_cache.allocate( + fields.fonts, + Key { + content, + size, + font, + bounds, + }, + ); + + let (total_lines, max_width) = paragraph + .layout_runs() + .enumerate() + .fold((0, 0.0), |(_, max), (i, buffer)| { + (i + 1, buffer.line_w.max(max)) + }); + + (max_width, size * 1.2 * total_lines as f32) + }) + } + + pub fn hit_test( + &self, + content: &str, + size: f32, + font: iced_native::Font, + bounds: iced_native::Size, + point: iced_native::Point, + _nearest_only: bool, + ) -> Option { + self.system.as_ref().unwrap().with(|fields| { + let mut measurement_cache = fields.measurement_cache.borrow_mut(); + + let (_, paragraph) = measurement_cache.allocate( + fields.fonts, + Key { + content, + size, + font, + bounds, + }, + ); + + let cursor = paragraph.hit(point.x, point.y)?; + + Some(Hit::CharOffset(cursor.index)) + }) + } + + pub fn trim_measurement_cache(&mut self) { + self.system + .as_mut() + .unwrap() + .with_measurement_cache_mut(|cache| cache.borrow_mut().trim()); + } +} + +fn to_family(font: Font) -> cosmic_text::Family<'static> { + match font { + Font::Name(name) => cosmic_text::Family::Name(name), + Font::SansSerif => cosmic_text::Family::SansSerif, + Font::Serif => cosmic_text::Family::Serif, + Font::Cursive => cosmic_text::Family::Cursive, + Font::Fantasy => cosmic_text::Family::Fantasy, + Font::Monospace => cosmic_text::Family::Monospace, + } +} + +struct Cache<'a> { + entries: FxHashMap>, + recently_used: FxHashSet, + hasher: HashBuilder, + trim_count: usize, +} + +#[cfg(not(target_arch = "wasm32"))] +type HashBuilder = twox_hash::RandomXxHashBuilder64; + +#[cfg(target_arch = "wasm32")] +type HashBuilder = std::hash::BuildHasherDefault; + +impl<'a> Cache<'a> { + const TRIM_INTERVAL: usize = 300; + + fn new() -> Self { + Self { + entries: FxHashMap::default(), + recently_used: FxHashSet::default(), + hasher: HashBuilder::default(), + trim_count: 0, + } + } + + fn allocate( + &mut self, + fonts: &'a cosmic_text::FontSystem, + key: Key<'_>, + ) -> (KeyHash, &mut cosmic_text::Buffer<'a>) { + let hash = { + let mut hasher = self.hasher.build_hasher(); + + key.content.hash(&mut hasher); + key.size.to_bits().hash(&mut hasher); + key.font.hash(&mut hasher); + key.bounds.width.to_bits().hash(&mut hasher); + key.bounds.height.to_bits().hash(&mut hasher); + + hasher.finish() + }; + + if let hash_map::Entry::Vacant(entry) = self.entries.entry(hash) { + let metrics = cosmic_text::Metrics::new(key.size, key.size * 1.2); + let mut buffer = cosmic_text::Buffer::new(fonts, metrics); + + buffer.set_size( + key.bounds.width, + key.bounds.height.max(key.size * 1.2), + ); + buffer.set_text( + key.content, + cosmic_text::Attrs::new() + .family(to_family(key.font)) + .monospaced(matches!(key.font, Font::Monospace)), + ); + + let _ = entry.insert(buffer); + } + + let _ = self.recently_used.insert(hash); + + (hash, self.entries.get_mut(&hash).unwrap()) + } + + fn trim(&mut self) { + if self.trim_count >= Self::TRIM_INTERVAL { + self.entries + .retain(|key, _| self.recently_used.contains(key)); + + self.recently_used.clear(); + + self.trim_count = 0; + } else { + self.trim_count += 1; + } + } +} + +#[derive(Debug, Clone, Copy)] +struct Key<'a> { + content: &'a str, + size: f32, + font: Font, + bounds: Size, +} + +type KeyHash = u64; -- cgit From 3386402f5a3e75cdacd230f5e76cd54f4868d87d Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 26 Feb 2023 23:44:50 +0100 Subject: Implement text alignment support in `iced_tiny_skia` --- tiny_skia/src/text.rs | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index 5bd6eff1..64f31aae 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -76,8 +76,8 @@ impl Pipeline { color: Color, size: f32, font: Font, - _horizontal_alignment: alignment::Horizontal, // TODO - _vertical_alignment: alignment::Vertical, // TODO + horizontal_alignment: alignment::Horizontal, + vertical_alignment: alignment::Vertical, pixels: &mut tiny_skia::PixmapMut<'_>, clip_mask: Option<&tiny_skia::ClipMask>, ) { @@ -91,6 +91,27 @@ impl Pipeline { let (_, buffer) = fields.render_cache.allocate(&fields.fonts, key); + let (total_lines, max_width) = buffer + .layout_runs() + .enumerate() + .fold((0, 0.0), |(_, max), (i, buffer)| { + (i + 1, buffer.line_w.max(max)) + }); + + let total_height = total_lines as f32 * size * 1.2; + + let x = match horizontal_alignment { + alignment::Horizontal::Left => bounds.x, + alignment::Horizontal::Center => bounds.x - max_width / 2.0, + alignment::Horizontal::Right => bounds.x - max_width, + }; + + let y = match vertical_alignment { + alignment::Vertical::Top => bounds.y, + alignment::Vertical::Center => bounds.y - total_height / 2.0, + alignment::Vertical::Bottom => bounds.y - total_height, + }; + let mut swash = cosmic_text::SwashCache::new(&fields.fonts); for run in buffer.layout_runs() { @@ -159,10 +180,8 @@ impl Pipeline { .expect("Create glyph pixel map"); pixels.draw_pixmap( - bounds.x as i32 - + glyph.x_int - + image.placement.left, - bounds.y as i32 - glyph.y_int - image.placement.top + x as i32 + glyph.x_int + image.placement.left, + y as i32 - glyph.y_int - image.placement.top + run.line_y as i32, pixmap, &tiny_skia::PixmapPaint::default(), -- cgit From 4067c427db19eb59c4ec6c8c6d6658a9643df580 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 26 Feb 2023 23:49:58 +0100 Subject: Fix glyphs with color mask in `iced_tiny_skia` --- tiny_skia/src/text.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index 64f31aae..fc26703b 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -157,13 +157,17 @@ impl Pipeline { for _y in 0..image.placement.height { for _x in 0..image.placement.width { // TODO: Blend alpha - buffer[i] = (image.data[i + 3] as u32) - << 24 - | (image.data[i + 2] as u32) << 16 - | (image.data[i + 1] as u32) << 8 - | image.data[i] as u32; + buffer[i >> 2] = + tiny_skia::ColorU8::from_rgba( + image.data[i + 2], + image.data[i + 1], + image.data[i], + image.data[i + 3], + ) + .premultiply() + .get(); - i += 1; + i += 4; } } } -- cgit From 53573cf7cfd48f9bfd97d9e8b82308a0290d2b9d Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 26 Feb 2023 23:59:00 +0100 Subject: Draw debug overlay in `iced_tiny_skia` --- tiny_skia/src/backend.rs | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 5e743479..cefed71f 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -1,5 +1,6 @@ use crate::{Color, Font, Settings, Size, Viewport}; +use iced_graphics::alignment; use iced_graphics::backend; use iced_graphics::text; use iced_graphics::{Background, Primitive, Rectangle, Vector}; @@ -27,7 +28,7 @@ impl Backend { primitives: &[Primitive], viewport: &Viewport, background_color: Color, - _overlay: &[T], + overlay: &[T], ) { pixels.fill(into_color(background_color)); @@ -43,6 +44,31 @@ impl Backend { ); } + for (i, text) in overlay.iter().enumerate() { + const OVERLAY_TEXT_SIZE: f32 = 20.0; + + self.draw_primitive( + &Primitive::Text { + content: text.as_ref().to_owned(), + size: OVERLAY_TEXT_SIZE, + bounds: Rectangle { + x: 10.0, + y: 10.0 + i as f32 * OVERLAY_TEXT_SIZE * 1.2, + width: f32::INFINITY, + height: f32::INFINITY, + }, + color: Color::BLACK, + font: Font::Monospace, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + }, + pixels, + None, + scale_factor, + Vector::ZERO, + ); + } + self.text_pipeline.end_frame(); } -- cgit From fbb14bf9b879d3d154618fa8d6a81bac018fee69 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 27 Feb 2023 00:47:53 +0100 Subject: Implement `border_radius` support for quads in `iced_tiny_skia` --- tiny_skia/src/backend.rs | 80 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 10 deletions(-) diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index cefed71f..b1dd6a46 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -85,7 +85,7 @@ impl Backend { Primitive::Quad { bounds, background, - border_radius: _, // TODO + border_radius, border_width, border_color, } => { @@ -95,15 +95,7 @@ impl Backend { ) .post_scale(scale_factor, scale_factor); - let path = tiny_skia::PathBuilder::from_rect( - tiny_skia::Rect::from_xywh( - bounds.x, - bounds.y, - bounds.width, - bounds.height, - ) - .expect("Create quad rectangle"), - ); + let path = rounded_rectangle(*bounds, *border_radius); pixels.fill_path( &path, @@ -223,6 +215,74 @@ fn into_color(color: Color) -> tiny_skia::Color { .expect("Convert color from iced to tiny_skia") } +fn rounded_rectangle( + bounds: Rectangle, + border_radius: [f32; 4], +) -> tiny_skia::Path { + let [top_left, top_right, bottom_right, bottom_left] = border_radius; + + if top_left == top_right + && top_left == bottom_right + && top_left == bottom_left + && top_left == bounds.width / 2.0 + && top_left == bounds.height / 2.0 + { + return tiny_skia::PathBuilder::from_circle( + bounds.x + bounds.width / 2.0, + bounds.y + bounds.height / 2.0, + top_left, + ) + .expect("Build circle path"); + } + + let mut builder = tiny_skia::PathBuilder::new(); + + builder.move_to(bounds.x + top_left, bounds.y); + builder.line_to(bounds.x + bounds.width - top_right, bounds.y); + + if top_right > 0.0 { + builder.quad_to( + bounds.x + bounds.width, + bounds.y, + bounds.x + bounds.width, + bounds.y + top_right, + ); + } + + builder.line_to( + bounds.x + bounds.width, + bounds.y + bounds.height - bottom_right, + ); + + if bottom_right > 0.0 { + builder.quad_to( + bounds.x + bounds.width, + bounds.y + bounds.height, + bounds.x + bounds.width - bottom_right, + bounds.y + bounds.height, + ); + } + + builder.line_to(bounds.x + bottom_left, bounds.y + bounds.height); + + if bottom_right > 0.0 { + builder.quad_to( + bounds.x, + bounds.y + bounds.height, + bounds.x, + bounds.y + bounds.height - bottom_left, + ); + } + + builder.line_to(bounds.x, bounds.y + top_left); + + if top_left > 0.0 { + builder.quad_to(bounds.x, bounds.y, bounds.x + top_left, bounds.y); + } + + builder.finish().expect("Build rounded rectangle path") +} + fn rectangular_clip_mask( pixels: &tiny_skia::PixmapMut<'_>, bounds: Rectangle, -- cgit From 4e615a65cab9dfc5fa4a17a72580c573c1c040d9 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 27 Feb 2023 01:12:06 +0100 Subject: Fix `clippy` lints --- renderer/src/backend.rs | 1 + tiny_skia/src/text.rs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/renderer/src/backend.rs b/renderer/src/backend.rs index a46d6f9b..b0a409dc 100644 --- a/renderer/src/backend.rs +++ b/renderer/src/backend.rs @@ -5,6 +5,7 @@ use iced_graphics::text; use std::borrow::Cow; +#[allow(clippy::large_enum_variant)] pub enum Backend { Wgpu(iced_wgpu::Backend), TinySkia(iced_tiny_skia::Backend), diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index fc26703b..6d4cfe96 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -89,7 +89,7 @@ impl Pipeline { size, }; - let (_, buffer) = fields.render_cache.allocate(&fields.fonts, key); + let (_, buffer) = fields.render_cache.allocate(fields.fonts, key); let (total_lines, max_width) = buffer .layout_runs() @@ -112,7 +112,7 @@ impl Pipeline { alignment::Vertical::Bottom => bounds.y - total_height, }; - let mut swash = cosmic_text::SwashCache::new(&fields.fonts); + let mut swash = cosmic_text::SwashCache::new(fields.fonts); for run in buffer.layout_runs() { for glyph in run.glyphs { -- cgit From 37ce30f360ce7cba9ad05654e1faf26276a1dc17 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 27 Feb 2023 02:58:02 +0100 Subject: Use `kurbo` to approximate arcs in `iced_tiny_skia` --- tiny_skia/Cargo.toml | 1 + tiny_skia/src/backend.rs | 80 ++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 71 insertions(+), 10 deletions(-) diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml index 55f7f5b7..7fee49cb 100644 --- a/tiny_skia/Cargo.toml +++ b/tiny_skia/Cargo.toml @@ -14,6 +14,7 @@ tiny-skia = "0.8" bytemuck = "1" rustc-hash = "1.1" ouroboros = "0.15" +kurbo = "0.9" [dependencies.iced_native] version = "0.9" diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index b1dd6a46..38a6c51d 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -241,48 +241,108 @@ fn rounded_rectangle( builder.line_to(bounds.x + bounds.width - top_right, bounds.y); if top_right > 0.0 { - builder.quad_to( - bounds.x + bounds.width, + arc_to( + &mut builder, + bounds.x + bounds.width - top_right, bounds.y, bounds.x + bounds.width, bounds.y + top_right, + top_right, ); } - builder.line_to( + maybe_line_to( + &mut builder, bounds.x + bounds.width, bounds.y + bounds.height - bottom_right, ); if bottom_right > 0.0 { - builder.quad_to( + arc_to( + &mut builder, bounds.x + bounds.width, - bounds.y + bounds.height, + bounds.y + bounds.height - bottom_right, bounds.x + bounds.width - bottom_right, bounds.y + bounds.height, + bottom_right, ); } - builder.line_to(bounds.x + bottom_left, bounds.y + bounds.height); + maybe_line_to( + &mut builder, + bounds.x + bottom_left, + bounds.y + bounds.height, + ); if bottom_right > 0.0 { - builder.quad_to( - bounds.x, + arc_to( + &mut builder, + bounds.x + bottom_left, bounds.y + bounds.height, bounds.x, bounds.y + bounds.height - bottom_left, + bottom_left, ); } - builder.line_to(bounds.x, bounds.y + top_left); + maybe_line_to(&mut builder, bounds.x, bounds.y + top_left); if top_left > 0.0 { - builder.quad_to(bounds.x, bounds.y, bounds.x + top_left, bounds.y); + arc_to( + &mut builder, + bounds.x, + bounds.y + top_left, + bounds.x + top_left, + bounds.y, + top_left, + ); } builder.finish().expect("Build rounded rectangle path") } +fn maybe_line_to(path: &mut tiny_skia::PathBuilder, x: f32, y: f32) { + if path.last_point() != Some(tiny_skia::Point { x, y }) { + path.line_to(x, y); + } +} + +fn arc_to( + path: &mut tiny_skia::PathBuilder, + x_from: f32, + y_from: f32, + x_to: f32, + y_to: f32, + radius: f32, +) { + let svg_arc = kurbo::SvgArc { + from: kurbo::Point::new(f64::from(x_from), f64::from(y_from)), + to: kurbo::Point::new(f64::from(x_to), f64::from(y_to)), + radii: kurbo::Vec2::new(f64::from(radius), f64::from(radius)), + x_rotation: 0.0, + large_arc: false, + sweep: true, + }; + + match kurbo::Arc::from_svg_arc(&svg_arc) { + Some(arc) => { + arc.to_cubic_beziers(0.1, |p1, p2, p| { + path.cubic_to( + p1.x as f32, + p1.y as f32, + p2.x as f32, + p2.y as f32, + p.x as f32, + p.y as f32, + ); + }); + } + None => { + path.line_to(x_to as f32, y_to as f32); + } + } +} + fn rectangular_clip_mask( pixels: &tiny_skia::PixmapMut<'_>, bounds: Rectangle, -- cgit From 8750d83337041f1d0fac8c2c0f4c40fd3a051a2c Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 27 Feb 2023 03:02:13 +0100 Subject: Short-circuit rectangle path building in `iced_tiny_skia` --- tiny_skia/src/backend.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 38a6c51d..d0977462 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -221,6 +221,22 @@ fn rounded_rectangle( ) -> tiny_skia::Path { let [top_left, top_right, bottom_right, bottom_left] = border_radius; + if top_left == 0.0 + && top_right == 0.0 + && bottom_right == 0.0 + && bottom_left == 0.0 + { + return tiny_skia::PathBuilder::from_rect( + tiny_skia::Rect::from_xywh( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + ) + .expect("Build quad rectangle"), + ); + } + if top_left == top_right && top_left == bottom_right && top_left == bottom_left -- cgit From 3105ad2e0036e101e66b5e6ab437b570f2d923a3 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 27 Feb 2023 03:04:05 +0100 Subject: Remove useless `f32` conversion in `iced_tiny_skia` --- tiny_skia/src/backend.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index d0977462..66d83221 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -354,7 +354,7 @@ fn arc_to( }); } None => { - path.line_to(x_to as f32, y_to as f32); + path.line_to(x_to, y_to); } } } -- cgit From c1ff803b8f98beb2a73bb4252b34921110aa6cf0 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 27 Feb 2023 16:28:19 +0100 Subject: Implement basic glyph cache in `iced_tiny_skia` --- tiny_skia/Cargo.toml | 2 +- tiny_skia/src/text.rs | 183 ++++++++++++++++++++++++++++++++------------------ 2 files changed, 120 insertions(+), 65 deletions(-) diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml index 7fee49cb..781e7d34 100644 --- a/tiny_skia/Cargo.toml +++ b/tiny_skia/Cargo.toml @@ -27,7 +27,7 @@ path = "../graphics" [dependencies.cosmic-text] features = ["std", "swash"] git = "https://github.com/hecrj/cosmic-text" -rev = "dc83efbf00a2efb4118403538e8a47bfd69c3e5e" +rev = "81080c1b9498933b43c1889601a7ea6a3d16e161" [dependencies.twox-hash] version = "1.6" diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index 6d4cfe96..cd7eb1ed 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -13,6 +13,7 @@ use std::sync::Arc; #[allow(missing_debug_implementations)] pub struct Pipeline { system: Option, + glyph_cache: GlyphCache, } #[ouroboros::self_referencing] @@ -45,6 +46,7 @@ impl Pipeline { } .build(), ), + glyph_cache: GlyphCache::new(), } } @@ -116,76 +118,20 @@ impl Pipeline { for run in buffer.layout_runs() { for glyph in run.glyphs { - // TODO: Outline support - if let Some(image) = swash.get_image(glyph.cache_key) { - let glyph_size = image.placement.width as usize - * image.placement.height as usize; - - if glyph_size == 0 { - continue; - } - - // TODO: Cache glyph rasterization - let mut buffer = vec![0u32; glyph_size]; - - match image.content { - cosmic_text::SwashContent::Mask => { - let mut i = 0; - - // TODO: Blend alpha - let [r, g, b, _a] = color.into_rgba8(); - - for _y in 0..image.placement.height { - for _x in 0..image.placement.width { - buffer[i] = - tiny_skia::ColorU8::from_rgba( - b, - g, - r, - image.data[i], - ) - .premultiply() - .get(); - - i += 1; - } - } - } - cosmic_text::SwashContent::Color => { - let mut i = 0; - - for _y in 0..image.placement.height { - for _x in 0..image.placement.width { - // TODO: Blend alpha - buffer[i >> 2] = - tiny_skia::ColorU8::from_rgba( - image.data[i + 2], - image.data[i + 1], - image.data[i], - image.data[i + 3], - ) - .premultiply() - .get(); - - i += 4; - } - } - } - cosmic_text::SwashContent::SubpixelMask => { - // TODO - } - } - + if let Some((buffer, placement)) = self + .glyph_cache + .allocate(glyph.cache_key, color, &mut swash) + { let pixmap = tiny_skia::PixmapRef::from_bytes( bytemuck::cast_slice(&buffer), - image.placement.width, - image.placement.height, + placement.width, + placement.height, ) .expect("Create glyph pixel map"); pixels.draw_pixmap( - x as i32 + glyph.x_int + image.placement.left, - y as i32 - glyph.y_int - image.placement.top + x as i32 + glyph.x_int + placement.left, + y as i32 - glyph.y_int - placement.top + run.line_y as i32, pixmap, &tiny_skia::PixmapPaint::default(), @@ -203,6 +149,8 @@ impl Pipeline { .as_mut() .unwrap() .with_render_cache_mut(|cache| cache.trim()); + + self.glyph_cache.trim(); } pub fn measure( @@ -283,6 +231,113 @@ fn to_family(font: Font) -> cosmic_text::Family<'static> { } } +#[derive(Debug, Clone, Default)] +struct GlyphCache { + entries: FxHashMap< + (cosmic_text::CacheKey, [u8; 3]), + (Vec, cosmic_text::Placement), + >, + recently_used: FxHashSet<(cosmic_text::CacheKey, [u8; 3])>, + trim_count: usize, +} + +impl GlyphCache { + fn new() -> Self { + GlyphCache::default() + } + + fn allocate( + &mut self, + cache_key: cosmic_text::CacheKey, + color: Color, + swash: &mut cosmic_text::SwashCache<'_>, + ) -> Option<(&[u8], cosmic_text::Placement)> { + let [r, g, b, _a] = color.into_rgba8(); + let key = (cache_key, [r, g, b]); + + if let hash_map::Entry::Vacant(entry) = self.entries.entry(key) { + // TODO: Outline support + let image = swash.get_image(cache_key).as_ref()?; + + let glyph_size = image.placement.width as usize + * image.placement.height as usize; + + if glyph_size == 0 { + return None; + } + + // TODO: Cache glyph rasterization + let mut buffer = vec![0u32; glyph_size]; + + match image.content { + cosmic_text::SwashContent::Mask => { + let mut i = 0; + + // TODO: Blend alpha + + for _y in 0..image.placement.height { + for _x in 0..image.placement.width { + buffer[i] = tiny_skia::ColorU8::from_rgba( + b, + g, + r, + image.data[i], + ) + .premultiply() + .get(); + + i += 1; + } + } + } + cosmic_text::SwashContent::Color => { + let mut i = 0; + + for _y in 0..image.placement.height { + for _x in 0..image.placement.width { + // TODO: Blend alpha + buffer[i >> 2] = tiny_skia::ColorU8::from_rgba( + image.data[i + 2], + image.data[i + 1], + image.data[i], + image.data[i + 3], + ) + .premultiply() + .get(); + + i += 4; + } + } + } + cosmic_text::SwashContent::SubpixelMask => { + // TODO + } + } + + entry.insert((buffer, image.placement)); + } + + self.recently_used.insert(key); + + self.entries.get(&key).map(|(buffer, placement)| { + (bytemuck::cast_slice(buffer.as_slice()), *placement) + }) + } + + pub fn trim(&mut self) { + if self.trim_count > 300 { + self.entries + .retain(|key, _| self.recently_used.contains(key)); + + self.recently_used.clear(); + + self.trim_count = 0; + } else { + self.trim_count += 1; + } + } +} + struct Cache<'a> { entries: FxHashMap>, recently_used: FxHashSet, -- cgit From 151daf95b70d5a53007496e58f49fc618c1a22e4 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 27 Feb 2023 16:30:54 +0100 Subject: Remove unnecessary `cast_slice` in `iced_tiny_skia` --- tiny_skia/src/text.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index cd7eb1ed..37b0fe47 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -123,7 +123,7 @@ impl Pipeline { .allocate(glyph.cache_key, color, &mut swash) { let pixmap = tiny_skia::PixmapRef::from_bytes( - bytemuck::cast_slice(&buffer), + buffer, placement.width, placement.height, ) @@ -266,7 +266,6 @@ impl GlyphCache { return None; } - // TODO: Cache glyph rasterization let mut buffer = vec![0u32; glyph_size]; match image.content { -- cgit From 9e815cb749f1bf6bce0232e870be266aca0c0742 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 27 Feb 2023 16:49:25 +0100 Subject: Remove `Fill` variant for `Alignment` Implementing this generically in our `flex` logic has an exponential cost. Let's explore other options! --- core/src/alignment.rs | 3 -- examples/component/src/main.rs | 5 +-- examples/scrollable/src/main.rs | 1 - examples/websocket/src/main.rs | 4 ++- native/src/layout/flex.rs | 67 +++++------------------------------------ native/src/layout/node.rs | 6 ---- 6 files changed, 14 insertions(+), 72 deletions(-) diff --git a/core/src/alignment.rs b/core/src/alignment.rs index 73f41d3f..51b7fca9 100644 --- a/core/src/alignment.rs +++ b/core/src/alignment.rs @@ -11,9 +11,6 @@ pub enum Alignment { /// Align at the end of the axis. End, - - /// Fill the entire axis. - Fill, } impl From for Alignment { diff --git a/examples/component/src/main.rs b/examples/component/src/main.rs index c407bb06..bbf549e7 100644 --- a/examples/component/src/main.rs +++ b/examples/component/src/main.rs @@ -127,7 +127,8 @@ mod numeric_input { .horizontal_alignment(alignment::Horizontal::Center) .vertical_alignment(alignment::Vertical::Center), ) - .width(50) + .width(40) + .height(40) .on_press(on_press) }; @@ -145,7 +146,7 @@ mod numeric_input { .padding(10), button("+", Event::IncrementPressed), ] - .align_items(Alignment::Fill) + .align_items(Alignment::Center) .spacing(10) .into() } diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index 7c858961..a3ade54f 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -254,7 +254,6 @@ impl Application for ScrollableDemo { scroll_to_beginning_button(), vertical_space(40), ] - .align_items(Alignment::Fill) .spacing(40), horizontal_space(1200), text("Horizontal - End!"), diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index ccd9c815..e617b8ce 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -146,7 +146,9 @@ impl Application for WebSocket { } } - row![input, button].spacing(10).align_items(Alignment::Fill) + row![input, button] + .spacing(10) + .align_items(Alignment::Center) }; column![message_log, new_message_input] diff --git a/native/src/layout/flex.rs b/native/src/layout/flex.rs index 5d70c2fc..8b967849 100644 --- a/native/src/layout/flex.rs +++ b/native/src/layout/flex.rs @@ -81,32 +81,6 @@ where let mut nodes: Vec = Vec::with_capacity(items.len()); nodes.resize(items.len(), Node::default()); - if align_items == Alignment::Fill { - let mut fill_cross = axis.cross(limits.min()); - - items.iter().for_each(|child| { - let cross_fill_factor = match axis { - Axis::Horizontal => child.as_widget().height(), - Axis::Vertical => child.as_widget().width(), - } - .fill_factor(); - - if cross_fill_factor == 0 { - let (max_width, max_height) = axis.pack(available, max_cross); - - let child_limits = - Limits::new(Size::ZERO, Size::new(max_width, max_height)); - - let layout = child.as_widget().layout(renderer, &child_limits); - let size = layout.size(); - - fill_cross = fill_cross.max(axis.cross(size)); - } - }); - - cross = fill_cross; - } - for (i, child) in items.iter().enumerate() { let fill_factor = match axis { Axis::Horizontal => child.as_widget().width(), @@ -115,31 +89,16 @@ where .fill_factor(); if fill_factor == 0 { - let (min_width, min_height) = if align_items == Alignment::Fill { - axis.pack(0.0, cross) - } else { - axis.pack(0.0, 0.0) - }; + let (max_width, max_height) = axis.pack(available, max_cross); - let (max_width, max_height) = if align_items == Alignment::Fill { - axis.pack(available, cross) - } else { - axis.pack(available, max_cross) - }; - - let child_limits = Limits::new( - Size::new(min_width, min_height), - Size::new(max_width, max_height), - ); + let child_limits = + Limits::new(Size::ZERO, Size::new(max_width, max_height)); let layout = child.as_widget().layout(renderer, &child_limits); let size = layout.size(); available -= axis.main(size); - - if align_items != Alignment::Fill { - cross = cross.max(axis.cross(size)); - } + cross = cross.max(axis.cross(size)); nodes[i] = layout; } else { @@ -164,17 +123,10 @@ where max_main }; - let (min_width, min_height) = if align_items == Alignment::Fill { - axis.pack(min_main, cross) - } else { - axis.pack(min_main, axis.cross(limits.min())) - }; + let (min_width, min_height) = + axis.pack(min_main, axis.cross(limits.min())); - let (max_width, max_height) = if align_items == Alignment::Fill { - axis.pack(max_main, cross) - } else { - axis.pack(max_main, max_cross) - }; + let (max_width, max_height) = axis.pack(max_main, max_cross); let child_limits = Limits::new( Size::new(min_width, min_height), @@ -182,10 +134,7 @@ where ); let layout = child.as_widget().layout(renderer, &child_limits); - - if align_items != Alignment::Fill { - cross = cross.max(axis.cross(layout.size())); - } + cross = cross.max(axis.cross(layout.size())); nodes[i] = layout; } diff --git a/native/src/layout/node.rs b/native/src/layout/node.rs index e0c7dcb2..2b44a7d5 100644 --- a/native/src/layout/node.rs +++ b/native/src/layout/node.rs @@ -56,9 +56,6 @@ impl Node { Alignment::End => { self.bounds.x += space.width - self.bounds.width; } - Alignment::Fill => { - self.bounds.width = space.width; - } } match vertical_alignment { @@ -69,9 +66,6 @@ impl Node { Alignment::End => { self.bounds.y += space.height - self.bounds.height; } - Alignment::Fill => { - self.bounds.height = space.height; - } } } -- cgit From fd06de5d9c5afa05c5b11cda730b0769aef92caa Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 28 Feb 2023 03:48:34 +0100 Subject: Use `get_image_uncached` in `iced_tiny_skia` ... since we are not reusing the `SwashCache` --- tiny_skia/src/text.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index 37b0fe47..da3a06bf 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -257,7 +257,7 @@ impl GlyphCache { if let hash_map::Entry::Vacant(entry) = self.entries.entry(key) { // TODO: Outline support - let image = swash.get_image(cache_key).as_ref()?; + let image = swash.get_image_uncached(cache_key)?; let glyph_size = image.placement.width as usize * image.placement.height as usize; -- cgit From 3f6e28fa9b1b8d911f765c9efb5249a9e0c942d5 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 28 Feb 2023 20:47:13 +0100 Subject: Use `iced_renderer` instead of `iced_graphics` in root crate --- Cargo.toml | 5 ++--- renderer/Cargo.toml | 2 ++ renderer/src/lib.rs | 1 + renderer/src/widget.rs | 5 +++++ src/error.rs | 2 +- src/widget.rs | 4 ++-- 6 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 renderer/src/widget.rs diff --git a/Cargo.toml b/Cargo.toml index b1f5cf66..942966e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,9 +17,9 @@ image = ["iced_renderer/image", "image_rs"] # Enables the `Svg` widget svg = ["iced_renderer/svg"] # Enables the `Canvas` widget -canvas = ["iced_graphics/canvas"] +canvas = ["iced_renderer/canvas"] # Enables the `QRCode` widget -qr_code = ["iced_graphics/qr_code"] +qr_code = ["iced_renderer/qr_code"] # Enables a debug view in native platforms (press F12) debug = ["iced_winit/debug"] # Enables `tokio` as the `executor::Default` on native platforms @@ -60,7 +60,6 @@ members = [ iced_core = { version = "0.8", path = "core" } iced_futures = { version = "0.6", path = "futures" } iced_native = { version = "0.9", path = "native" } -iced_graphics = { version = "0.7", path = "graphics" } iced_renderer = { version = "0.1", path = "renderer" } iced_winit = { version = "0.8", path = "winit", features = ["application"] } thiserror = "1.0" diff --git a/renderer/Cargo.toml b/renderer/Cargo.toml index 5ba5d426..1f21f06b 100644 --- a/renderer/Cargo.toml +++ b/renderer/Cargo.toml @@ -6,6 +6,8 @@ edition = "2021" [features] image = ["iced_wgpu/image", "iced_tiny_skia/image"] svg = ["iced_wgpu/svg", "iced_tiny_skia/svg"] +canvas = ["iced_graphics/canvas"] +qr_code = ["iced_graphics/qr_code"] tracing = ["iced_wgpu/tracing"] [dependencies] diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index d0ba6793..f9bfc373 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -1,3 +1,4 @@ +pub mod widget; pub mod window; mod backend; diff --git a/renderer/src/widget.rs b/renderer/src/widget.rs new file mode 100644 index 00000000..417cc06f --- /dev/null +++ b/renderer/src/widget.rs @@ -0,0 +1,5 @@ +#[cfg(feature = "qr_code")] +pub use iced_graphics::widget::qr_code; + +#[cfg(feature = "canvas")] +pub use iced_graphics::widget::canvas; diff --git a/src/error.rs b/src/error.rs index 0bfa3ff1..5326718f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -13,7 +13,7 @@ pub enum Error { /// The application graphics context could not be created. #[error("the application graphics context could not be created")] - GraphicsCreationFailed(iced_graphics::Error), + GraphicsCreationFailed(iced_renderer::Error), } impl From for Error { diff --git a/src/widget.rs b/src/widget.rs index e2b0537e..04cb0e50 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -165,7 +165,7 @@ pub use vertical_slider::VerticalSlider; #[cfg(feature = "canvas")] #[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] -pub use iced_graphics::widget::canvas; +pub use iced_renderer::widget::canvas; #[cfg(feature = "canvas")] #[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] @@ -192,7 +192,7 @@ pub mod image { #[cfg(feature = "qr_code")] #[cfg_attr(docsrs, doc(cfg(feature = "qr_code")))] -pub use iced_graphics::widget::qr_code; +pub use iced_renderer::widget::qr_code; #[cfg(feature = "svg")] #[cfg_attr(docsrs, doc(cfg(feature = "svg")))] -- cgit From 5fd5d1cdf8e5354788dc40729c4565ef377d3bba Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 1 Mar 2023 21:34:26 +0100 Subject: Implement `Canvas` support for `iced_tiny_skia` --- core/Cargo.toml | 1 + core/src/gradient.rs | 117 ++++++ core/src/gradient/linear.rs | 112 ++++++ core/src/lib.rs | 2 + examples/arc/src/main.rs | 9 +- examples/bezier_tool/src/main.rs | 25 +- examples/clock/src/main.rs | 9 +- examples/color_palette/src/main.rs | 9 +- examples/game_of_life/src/main.rs | 73 ++-- examples/geometry/src/main.rs | 6 +- examples/modern_art/src/main.rs | 5 +- examples/multitouch/src/main.rs | 7 +- examples/sierpinski_triangle/src/main.rs | 9 +- examples/solar_system/src/main.rs | 28 +- graphics/Cargo.toml | 13 +- graphics/src/backend.rs | 2 + graphics/src/gradient.rs | 117 ------ graphics/src/gradient/linear.rs | 112 ------ graphics/src/layer.rs | 270 ------------- graphics/src/layer/image.rs | 27 -- graphics/src/layer/mesh.rs | 93 ----- graphics/src/layer/quad.rs | 30 -- graphics/src/layer/text.rs | 26 -- graphics/src/lib.rs | 13 +- graphics/src/primitive.rs | 128 ++++-- graphics/src/renderer.rs | 43 +- graphics/src/triangle.rs | 32 -- graphics/src/widget.rs | 16 - graphics/src/widget/canvas.rs | 268 ------------- graphics/src/widget/canvas/cache.rs | 100 ----- graphics/src/widget/canvas/cursor.rs | 64 --- graphics/src/widget/canvas/event.rs | 21 - graphics/src/widget/canvas/fill.rs | 72 ---- graphics/src/widget/canvas/frame.rs | 530 ------------------------- graphics/src/widget/canvas/geometry.rs | 24 -- graphics/src/widget/canvas/path.rs | 109 ------ graphics/src/widget/canvas/path/arc.rs | 42 -- graphics/src/widget/canvas/path/builder.rs | 198 ---------- graphics/src/widget/canvas/program.rs | 102 ----- graphics/src/widget/canvas/stroke.rs | 126 ------ graphics/src/widget/canvas/style.rs | 23 -- graphics/src/widget/canvas/text.rs | 57 --- graphics/src/widget/qr_code.rs | 301 -------------- native/Cargo.toml | 6 + native/src/lib.rs | 4 +- native/src/widget.rs | 16 + native/src/widget/canvas.rs | 259 ++++++++++++ native/src/widget/canvas/cursor.rs | 64 +++ native/src/widget/canvas/event.rs | 21 + native/src/widget/canvas/fill.rs | 64 +++ native/src/widget/canvas/path.rs | 67 ++++ native/src/widget/canvas/path/arc.rs | 42 ++ native/src/widget/canvas/path/builder.rs | 192 +++++++++ native/src/widget/canvas/program.rs | 108 +++++ native/src/widget/canvas/stroke.rs | 106 +++++ native/src/widget/canvas/style.rs | 24 ++ native/src/widget/canvas/text.rs | 57 +++ renderer/Cargo.toml | 10 +- renderer/src/backend.rs | 4 +- renderer/src/lib.rs | 14 +- renderer/src/widget.rs | 12 +- renderer/src/widget/canvas.rs | 177 +++++++++ renderer/src/widget/canvas/cache.rs | 85 ++++ renderer/src/widget/qr_code.rs | 301 ++++++++++++++ src/widget.rs | 5 +- tiny_skia/Cargo.toml | 1 + tiny_skia/src/backend.rs | 48 ++- tiny_skia/src/canvas.rs | 276 +++++++++++++ tiny_skia/src/lib.rs | 10 +- tiny_skia/src/primitive.rs | 82 ++++ tiny_skia/src/window/compositor.rs | 3 +- wgpu/Cargo.toml | 15 +- wgpu/src/backend.rs | 7 +- wgpu/src/image.rs | 2 +- wgpu/src/layer.rs | 274 +++++++++++++ wgpu/src/layer/image.rs | 27 ++ wgpu/src/layer/mesh.rs | 93 +++++ wgpu/src/layer/quad.rs | 30 ++ wgpu/src/layer/text.rs | 26 ++ wgpu/src/lib.rs | 15 +- wgpu/src/quad.rs | 3 +- wgpu/src/text.rs | 3 +- wgpu/src/triangle.rs | 71 ++-- wgpu/src/widget.rs | 9 + wgpu/src/widget/canvas.rs | 16 + wgpu/src/widget/canvas/cache.rs | 93 +++++ wgpu/src/widget/canvas/frame.rs | 609 +++++++++++++++++++++++++++++ wgpu/src/widget/canvas/geometry.rs | 24 ++ 88 files changed, 3765 insertions(+), 2981 deletions(-) create mode 100644 core/src/gradient.rs create mode 100644 core/src/gradient/linear.rs delete mode 100644 graphics/src/gradient.rs delete mode 100644 graphics/src/gradient/linear.rs delete mode 100644 graphics/src/layer.rs delete mode 100644 graphics/src/layer/image.rs delete mode 100644 graphics/src/layer/mesh.rs delete mode 100644 graphics/src/layer/quad.rs delete mode 100644 graphics/src/layer/text.rs delete mode 100644 graphics/src/widget.rs delete mode 100644 graphics/src/widget/canvas.rs delete mode 100644 graphics/src/widget/canvas/cache.rs delete mode 100644 graphics/src/widget/canvas/cursor.rs delete mode 100644 graphics/src/widget/canvas/event.rs delete mode 100644 graphics/src/widget/canvas/fill.rs delete mode 100644 graphics/src/widget/canvas/frame.rs delete mode 100644 graphics/src/widget/canvas/geometry.rs delete mode 100644 graphics/src/widget/canvas/path.rs delete mode 100644 graphics/src/widget/canvas/path/arc.rs delete mode 100644 graphics/src/widget/canvas/path/builder.rs delete mode 100644 graphics/src/widget/canvas/program.rs delete mode 100644 graphics/src/widget/canvas/stroke.rs delete mode 100644 graphics/src/widget/canvas/style.rs delete mode 100644 graphics/src/widget/canvas/text.rs delete mode 100644 graphics/src/widget/qr_code.rs create mode 100644 native/src/widget/canvas.rs create mode 100644 native/src/widget/canvas/cursor.rs create mode 100644 native/src/widget/canvas/event.rs create mode 100644 native/src/widget/canvas/fill.rs create mode 100644 native/src/widget/canvas/path.rs create mode 100644 native/src/widget/canvas/path/arc.rs create mode 100644 native/src/widget/canvas/path/builder.rs create mode 100644 native/src/widget/canvas/program.rs create mode 100644 native/src/widget/canvas/stroke.rs create mode 100644 native/src/widget/canvas/style.rs create mode 100644 native/src/widget/canvas/text.rs create mode 100644 renderer/src/widget/canvas.rs create mode 100644 renderer/src/widget/canvas/cache.rs create mode 100644 renderer/src/widget/qr_code.rs create mode 100644 tiny_skia/src/canvas.rs create mode 100644 tiny_skia/src/primitive.rs create mode 100644 wgpu/src/layer.rs create mode 100644 wgpu/src/layer/image.rs create mode 100644 wgpu/src/layer/mesh.rs create mode 100644 wgpu/src/layer/quad.rs create mode 100644 wgpu/src/layer/text.rs create mode 100644 wgpu/src/widget.rs create mode 100644 wgpu/src/widget/canvas.rs create mode 100644 wgpu/src/widget/canvas/cache.rs create mode 100644 wgpu/src/widget/canvas/frame.rs create mode 100644 wgpu/src/widget/canvas/geometry.rs diff --git a/core/Cargo.toml b/core/Cargo.toml index 43865e4d..7ccb7b7a 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -9,6 +9,7 @@ repository = "https://github.com/iced-rs/iced" [dependencies] bitflags = "1.2" +thiserror = "1" [dependencies.palette] version = "0.6" diff --git a/core/src/gradient.rs b/core/src/gradient.rs new file mode 100644 index 00000000..61e919d6 --- /dev/null +++ b/core/src/gradient.rs @@ -0,0 +1,117 @@ +//! For creating a Gradient. +pub mod linear; + +pub use linear::Linear; + +use crate::{Color, Point, Size}; + +#[derive(Debug, Clone, PartialEq)] +/// A fill which transitions colors progressively along a direction, either linearly, radially (TBD), +/// or conically (TBD). +pub enum Gradient { + /// A linear gradient interpolates colors along a direction from its `start` to its `end` + /// point. + Linear(Linear), +} + +impl Gradient { + /// Creates a new linear [`linear::Builder`]. + pub fn linear(position: impl Into) -> linear::Builder { + linear::Builder::new(position.into()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +/// A point along the gradient vector where the specified [`color`] is unmixed. +/// +/// [`color`]: Self::color +pub struct ColorStop { + /// Offset along the gradient vector. + pub offset: f32, + + /// The color of the gradient at the specified [`offset`]. + /// + /// [`offset`]: Self::offset + pub color: Color, +} + +#[derive(Debug)] +/// The position of the gradient within its bounds. +pub enum Position { + /// The gradient will be positioned with respect to two points. + Absolute { + /// The starting point of the gradient. + start: Point, + /// The ending point of the gradient. + end: Point, + }, + /// The gradient will be positioned relative to the provided bounds. + Relative { + /// The top left position of the bounds. + top_left: Point, + /// The width & height of the bounds. + size: Size, + /// The start [Location] of the gradient. + start: Location, + /// The end [Location] of the gradient. + end: Location, + }, +} + +impl From<(Point, Point)> for Position { + fn from((start, end): (Point, Point)) -> Self { + Self::Absolute { start, end } + } +} + +#[derive(Debug, Clone, Copy)] +/// The location of a relatively-positioned gradient. +pub enum Location { + /// Top left. + TopLeft, + /// Top. + Top, + /// Top right. + TopRight, + /// Right. + Right, + /// Bottom right. + BottomRight, + /// Bottom. + Bottom, + /// Bottom left. + BottomLeft, + /// Left. + Left, +} + +impl Location { + fn to_absolute(self, top_left: Point, size: Size) -> Point { + match self { + Location::TopLeft => top_left, + Location::Top => { + Point::new(top_left.x + size.width / 2.0, top_left.y) + } + Location::TopRight => { + Point::new(top_left.x + size.width, top_left.y) + } + Location::Right => Point::new( + top_left.x + size.width, + top_left.y + size.height / 2.0, + ), + Location::BottomRight => { + Point::new(top_left.x + size.width, top_left.y + size.height) + } + Location::Bottom => Point::new( + top_left.x + size.width / 2.0, + top_left.y + size.height, + ), + Location::BottomLeft => { + Point::new(top_left.x, top_left.y + size.height) + } + Location::Left => { + Point::new(top_left.x, top_left.y + size.height / 2.0) + } + } + } +} diff --git a/core/src/gradient/linear.rs b/core/src/gradient/linear.rs new file mode 100644 index 00000000..c886db47 --- /dev/null +++ b/core/src/gradient/linear.rs @@ -0,0 +1,112 @@ +//! Linear gradient builder & definition. +use crate::gradient::{ColorStop, Gradient, Position}; +use crate::{Color, Point}; + +/// A linear gradient that can be used in the style of [`Fill`] or [`Stroke`]. +/// +/// [`Fill`]: crate::widget::canvas::Fill +/// [`Stroke`]: crate::widget::canvas::Stroke +#[derive(Debug, Clone, PartialEq)] +pub struct Linear { + /// The point where the linear gradient begins. + pub start: Point, + /// The point where the linear gradient ends. + pub end: Point, + /// [`ColorStop`]s along the linear gradient path. + pub color_stops: Vec, +} + +/// A [`Linear`] builder. +#[derive(Debug)] +pub struct Builder { + start: Point, + end: Point, + stops: Vec, + error: Option, +} + +impl Builder { + /// Creates a new [`Builder`]. + pub fn new(position: Position) -> Self { + let (start, end) = match position { + Position::Absolute { start, end } => (start, end), + Position::Relative { + top_left, + size, + start, + end, + } => ( + start.to_absolute(top_left, size), + end.to_absolute(top_left, size), + ), + }; + + Self { + start, + end, + stops: vec![], + error: None, + } + } + + /// Adds a new stop, defined by an offset and a color, to the gradient. + /// + /// `offset` must be between `0.0` and `1.0` or the gradient cannot be built. + /// + /// Note: when using the [`glow`] backend, any color stop added after the 16th + /// will not be displayed. + /// + /// On the [`wgpu`] backend this limitation does not exist (technical limit is 524,288 stops). + /// + /// [`glow`]: https://docs.rs/iced_glow + /// [`wgpu`]: https://docs.rs/iced_wgpu + pub fn add_stop(mut self, offset: f32, color: Color) -> Self { + if offset.is_finite() && (0.0..=1.0).contains(&offset) { + match self.stops.binary_search_by(|stop| { + stop.offset.partial_cmp(&offset).unwrap() + }) { + Ok(_) => { + self.error = Some(BuilderError::DuplicateOffset(offset)) + } + Err(index) => { + self.stops.insert(index, ColorStop { offset, color }); + } + } + } else { + self.error = Some(BuilderError::InvalidOffset(offset)) + }; + + self + } + + /// Builds the linear [`Gradient`] of this [`Builder`]. + /// + /// Returns `BuilderError` if gradient in invalid. + pub fn build(self) -> Result { + if self.stops.is_empty() { + Err(BuilderError::MissingColorStop) + } else if let Some(error) = self.error { + Err(error) + } else { + Ok(Gradient::Linear(Linear { + start: self.start, + end: self.end, + color_stops: self.stops, + })) + } + } +} + +/// An error that happened when building a [`Linear`] gradient. +#[derive(Debug, thiserror::Error)] +pub enum BuilderError { + #[error("Gradients must contain at least one color stop.")] + /// Gradients must contain at least one color stop. + MissingColorStop, + #[error("Offset {0} must be a unique, finite number.")] + /// Offsets in a gradient must all be unique & finite. + DuplicateOffset(f32), + #[error("Offset {0} must be between 0.0..=1.0.")] + /// Offsets in a gradient must be between 0.0..=1.0. + InvalidOffset(f32), +} diff --git a/core/src/lib.rs b/core/src/lib.rs index d7314851..1e4f0411 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -26,6 +26,7 @@ #![allow(clippy::inherent_to_string, clippy::type_complexity)] pub mod alignment; pub mod font; +pub mod gradient; pub mod keyboard; pub mod mouse; pub mod time; @@ -46,6 +47,7 @@ pub use background::Background; pub use color::Color; pub use content_fit::ContentFit; pub use font::Font; +pub use gradient::Gradient; pub use length::Length; pub use padding::Padding; pub use pixels::Pixels; diff --git a/examples/arc/src/main.rs b/examples/arc/src/main.rs index 7b6ea0e1..d71ba6f6 100644 --- a/examples/arc/src/main.rs +++ b/examples/arc/src/main.rs @@ -5,8 +5,8 @@ use iced::widget::canvas::{ self, stroke, Cache, Canvas, Cursor, Geometry, Path, Stroke, }; use iced::{ - Application, Command, Element, Length, Point, Rectangle, Settings, - Subscription, Theme, + Application, Command, Element, Length, Point, Rectangle, Renderer, + Settings, Subscription, Theme, }; pub fn main() -> iced::Result { @@ -69,17 +69,18 @@ impl Application for Arc { } } -impl canvas::Program for Arc { +impl canvas::Program for Arc { type State = (); fn draw( &self, _state: &Self::State, + renderer: &Renderer, theme: &Theme, bounds: Rectangle, _cursor: Cursor, ) -> Vec { - let geometry = self.cache.draw(bounds.size(), |frame| { + let geometry = self.cache.draw(renderer, bounds.size(), |frame| { let palette = theme.palette(); let center = frame.center(); diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index 7c3916d4..5bb463c3 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -64,7 +64,7 @@ mod bezier { use iced::widget::canvas::{ self, Canvas, Cursor, Frame, Geometry, Path, Stroke, }; - use iced::{Element, Length, Point, Rectangle, Theme}; + use iced::{Element, Length, Point, Rectangle, Renderer, Theme}; #[derive(Default)] pub struct State { @@ -92,7 +92,7 @@ mod bezier { curves: &'a [Curve], } - impl<'a> canvas::Program for Bezier<'a> { + impl<'a> canvas::Program for Bezier<'a> { type State = Option; fn update( @@ -152,22 +152,26 @@ mod bezier { fn draw( &self, state: &Self::State, + renderer: &Renderer, _theme: &Theme, bounds: Rectangle, cursor: Cursor, ) -> Vec { - let content = - self.state.cache.draw(bounds.size(), |frame: &mut Frame| { + let content = self.state.cache.draw( + renderer, + bounds.size(), + |frame: &mut Frame| { Curve::draw_all(self.curves, frame); frame.stroke( &Path::rectangle(Point::ORIGIN, frame.size()), Stroke::default().with_width(2.0), ); - }); + }, + ); if let Some(pending) = state { - let pending_curve = pending.draw(bounds, cursor); + let pending_curve = pending.draw(renderer, bounds, cursor); vec![content, pending_curve] } else { @@ -216,8 +220,13 @@ mod bezier { } impl Pending { - fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Geometry { - let mut frame = Frame::new(bounds.size()); + fn draw( + &self, + renderer: &Renderer, + bounds: Rectangle, + cursor: Cursor, + ) -> Geometry { + let mut frame = Frame::new(renderer, bounds.size()); if let Some(cursor_position) = cursor.position_in(&bounds) { match *self { diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index a389c54f..6425e2da 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -4,8 +4,8 @@ use iced::widget::canvas::{ }; use iced::widget::{canvas, container}; use iced::{ - Application, Color, Command, Element, Length, Point, Rectangle, Settings, - Subscription, Theme, Vector, + Application, Color, Command, Element, Length, Point, Rectangle, Renderer, + Settings, Subscription, Theme, Vector, }; pub fn main() -> iced::Result { @@ -83,17 +83,18 @@ impl Application for Clock { } } -impl canvas::Program for Clock { +impl canvas::Program for Clock { type State = (); fn draw( &self, _state: &Self::State, + renderer: &Renderer, _theme: &Theme, bounds: Rectangle, _cursor: Cursor, ) -> Vec { - let clock = self.clock.draw(bounds.size(), |frame| { + let clock = self.clock.draw(renderer, bounds.size(), |frame| { let center = frame.center(); let radius = frame.width().min(frame.height()) / 2.0; diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs index a2df36c2..1109a883 100644 --- a/examples/color_palette/src/main.rs +++ b/examples/color_palette/src/main.rs @@ -1,8 +1,8 @@ use iced::widget::canvas::{self, Canvas, Cursor, Frame, Geometry, Path}; use iced::widget::{column, row, text, Slider}; use iced::{ - alignment, Alignment, Color, Element, Length, Point, Rectangle, Sandbox, - Settings, Size, Vector, + alignment, Alignment, Color, Element, Length, Point, Rectangle, Renderer, + Sandbox, Settings, Size, Vector, }; use palette::{self, convert::FromColor, Hsl, Srgb}; use std::marker::PhantomData; @@ -237,17 +237,18 @@ impl Theme { } } -impl canvas::Program for Theme { +impl canvas::Program for Theme { type State = (); fn draw( &self, _state: &Self::State, + renderer: &Renderer, _theme: &iced::Theme, bounds: Rectangle, _cursor: Cursor, ) -> Vec { - let theme = self.canvas_cache.draw(bounds.size(), |frame| { + let theme = self.canvas_cache.draw(renderer, bounds.size(), |frame| { self.draw(frame); }); diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index ed911160..494f71a6 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -211,8 +211,8 @@ mod grid { Cache, Canvas, Cursor, Frame, Geometry, Path, Text, }; use iced::{ - alignment, mouse, Color, Element, Length, Point, Rectangle, Size, - Theme, Vector, + alignment, mouse, Color, Element, Length, Point, Rectangle, Renderer, + Size, Theme, Vector, }; use rustc_hash::{FxHashMap, FxHashSet}; use std::future::Future; @@ -393,7 +393,7 @@ mod grid { } } - impl canvas::Program for Grid { + impl canvas::Program for Grid { type State = Interaction; fn update( @@ -536,13 +536,14 @@ mod grid { fn draw( &self, _interaction: &Interaction, + renderer: &Renderer, _theme: &Theme, bounds: Rectangle, cursor: Cursor, ) -> Vec { let center = Vector::new(bounds.width / 2.0, bounds.height / 2.0); - let life = self.life_cache.draw(bounds.size(), |frame| { + let life = self.life_cache.draw(renderer, bounds.size(), |frame| { let background = Path::rectangle(Point::ORIGIN, frame.size()); frame.fill(&background, Color::from_rgb8(0x40, 0x44, 0x4B)); @@ -565,7 +566,7 @@ mod grid { }); let overlay = { - let mut frame = Frame::new(bounds.size()); + let mut frame = Frame::new(renderer, bounds.size()); let hovered_cell = cursor.position_in(&bounds).map(|position| { @@ -626,38 +627,40 @@ mod grid { if self.scaling < 0.2 || !self.show_lines { vec![life, overlay] } else { - let grid = self.grid_cache.draw(bounds.size(), |frame| { - frame.translate(center); - frame.scale(self.scaling); - frame.translate(self.translation); - frame.scale(Cell::SIZE as f32); - - let region = self.visible_region(frame.size()); - let rows = region.rows(); - let columns = region.columns(); - let (total_rows, total_columns) = - (rows.clone().count(), columns.clone().count()); - let width = 2.0 / Cell::SIZE as f32; - let color = Color::from_rgb8(70, 74, 83); - - frame.translate(Vector::new(-width / 2.0, -width / 2.0)); + let grid = + self.grid_cache.draw(renderer, bounds.size(), |frame| { + frame.translate(center); + frame.scale(self.scaling); + frame.translate(self.translation); + frame.scale(Cell::SIZE as f32); - for row in region.rows() { - frame.fill_rectangle( - Point::new(*columns.start() as f32, row as f32), - Size::new(total_columns as f32, width), - color, - ); - } + let region = self.visible_region(frame.size()); + let rows = region.rows(); + let columns = region.columns(); + let (total_rows, total_columns) = + (rows.clone().count(), columns.clone().count()); + let width = 2.0 / Cell::SIZE as f32; + let color = Color::from_rgb8(70, 74, 83); + + frame + .translate(Vector::new(-width / 2.0, -width / 2.0)); + + for row in region.rows() { + frame.fill_rectangle( + Point::new(*columns.start() as f32, row as f32), + Size::new(total_columns as f32, width), + color, + ); + } - for column in region.columns() { - frame.fill_rectangle( - Point::new(column as f32, *rows.start() as f32), - Size::new(width, total_rows as f32), - color, - ); - } - }); + for column in region.columns() { + frame.fill_rectangle( + Point::new(column as f32, *rows.start() as f32), + Size::new(width, total_rows as f32), + color, + ); + } + }); vec![life, grid, overlay] } diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index 9bacce7f..a77772d5 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -10,9 +10,9 @@ mod rainbow { // Of course, you can choose to make the implementation renderer-agnostic, // if you wish to, by creating your own `Renderer` trait, which could be // implemented by `iced_wgpu` and other renderers. + use iced_graphics::primitive::{ColoredVertex2D, Primitive}; use iced_graphics::renderer::{self, Renderer}; - use iced_graphics::triangle::ColoredVertex2D; - use iced_graphics::{Backend, Primitive}; + use iced_graphics::Backend; use iced_native::layout; use iced_native::widget::{self, Widget}; @@ -59,7 +59,7 @@ mod rainbow { cursor_position: Point, _viewport: &Rectangle, ) { - use iced_graphics::triangle::Mesh2D; + use iced_graphics::primitive::Mesh2D; use iced_native::Renderer as _; let b = layout.bounds(); diff --git a/examples/modern_art/src/main.rs b/examples/modern_art/src/main.rs index 28ed3e21..a43a2b2b 100644 --- a/examples/modern_art/src/main.rs +++ b/examples/modern_art/src/main.rs @@ -55,17 +55,18 @@ impl Application for ModernArt { } } -impl canvas::Program for ModernArt { +impl canvas::Program for ModernArt { type State = (); fn draw( &self, _state: &Self::State, + renderer: &Renderer, _theme: &Theme, bounds: Rectangle, _cursor: Cursor, ) -> Vec { - let geometry = self.cache.draw(bounds.size(), |frame| { + let geometry = self.cache.draw(renderer, bounds.size(), |frame| { let num_squares = thread_rng().gen_range(0..1200); let mut i = 0; diff --git a/examples/multitouch/src/main.rs b/examples/multitouch/src/main.rs index f5faae0f..7df6c929 100644 --- a/examples/multitouch/src/main.rs +++ b/examples/multitouch/src/main.rs @@ -6,7 +6,7 @@ use iced::widget::canvas::stroke::{self, Stroke}; use iced::widget::canvas::{self, Canvas, Cursor, Geometry}; use iced::{ executor, touch, window, Application, Color, Command, Element, Length, - Point, Rectangle, Settings, Subscription, Theme, + Point, Rectangle, Renderer, Settings, Subscription, Theme, }; use std::collections::HashMap; @@ -95,7 +95,7 @@ impl Application for Multitouch { } } -impl canvas::Program for State { +impl canvas::Program for State { type State = (); fn update( @@ -125,11 +125,12 @@ impl canvas::Program for State { fn draw( &self, _state: &Self::State, + renderer: &Renderer, _theme: &Theme, bounds: Rectangle, _cursor: Cursor, ) -> Vec { - let fingerweb = self.cache.draw(bounds.size(), |frame| { + let fingerweb = self.cache.draw(renderer, bounds.size(), |frame| { if self.fingers.len() < 2 { return; } diff --git a/examples/sierpinski_triangle/src/main.rs b/examples/sierpinski_triangle/src/main.rs index 1d25d171..e85f8391 100644 --- a/examples/sierpinski_triangle/src/main.rs +++ b/examples/sierpinski_triangle/src/main.rs @@ -5,8 +5,8 @@ use iced::widget::canvas::event::{self, Event}; use iced::widget::canvas::{self, Canvas}; use iced::widget::{column, row, slider, text}; use iced::{ - Application, Color, Command, Length, Point, Rectangle, Settings, Size, - Theme, + Application, Color, Command, Length, Point, Rectangle, Renderer, Settings, + Size, Theme, }; use rand::Rng; @@ -97,7 +97,7 @@ struct SierpinskiGraph { cache: canvas::Cache, } -impl canvas::Program for SierpinskiGraph { +impl canvas::Program for SierpinskiGraph { type State = (); fn update( @@ -134,11 +134,12 @@ impl canvas::Program for SierpinskiGraph { fn draw( &self, _state: &Self::State, + renderer: &Renderer, _theme: &Theme, bounds: Rectangle, _cursor: canvas::Cursor, ) -> Vec { - let geom = self.cache.draw(bounds.size(), |frame| { + let geom = self.cache.draw(renderer, bounds.size(), |frame| { frame.stroke( &canvas::Path::rectangle(Point::ORIGIN, frame.size()), canvas::Stroke::default(), diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index 9a4ee754..0023a69b 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -15,8 +15,8 @@ use iced::widget::canvas::stroke::{self, Stroke}; use iced::widget::canvas::{Cursor, Path}; use iced::window; use iced::{ - Application, Color, Command, Element, Length, Point, Rectangle, Settings, - Size, Subscription, Vector, + Application, Color, Command, Element, Length, Point, Rectangle, Renderer, + Settings, Size, Subscription, Vector, }; use std::time::Instant; @@ -150,30 +150,32 @@ impl State { } } -impl canvas::Program for State { +impl canvas::Program for State { type State = (); fn draw( &self, _state: &Self::State, + renderer: &Renderer, _theme: &Theme, bounds: Rectangle, _cursor: Cursor, ) -> Vec { use std::f32::consts::PI; - let background = self.space_cache.draw(bounds.size(), |frame| { - let stars = Path::new(|path| { - for (p, size) in &self.stars { - path.rectangle(*p, Size::new(*size, *size)); - } - }); + let background = + self.space_cache.draw(renderer, bounds.size(), |frame| { + let stars = Path::new(|path| { + for (p, size) in &self.stars { + path.rectangle(*p, Size::new(*size, *size)); + } + }); - frame.translate(frame.center() - Point::ORIGIN); - frame.fill(&stars, Color::WHITE); - }); + frame.translate(frame.center() - Point::ORIGIN); + frame.fill(&stars, Color::WHITE); + }); - let system = self.system_cache.draw(bounds.size(), |frame| { + let system = self.system_cache.draw(renderer, bounds.size(), |frame| { let center = frame.center(); let sun = Path::circle(center, Self::SUN_RADIUS); diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index 9795d31f..36d8a516 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -24,8 +24,7 @@ bmp = ["image_rs/bmp"] hdr = ["image_rs/hdr"] dds = ["image_rs/dds"] farbfeld = ["image_rs/farbfeld"] -canvas = ["lyon"] -qr_code = ["qrcode", "canvas"] +canvas = ["iced_native/canvas"] opengl = [] image_rs = ["kamadak-exif"] @@ -35,6 +34,7 @@ log = "0.4" raw-window-handle = "0.5" thiserror = "1.0" bitflags = "1.2" +tiny-skia = "0.8" [dependencies.bytemuck] version = "1.4" @@ -48,15 +48,6 @@ path = "../native" version = "0.7" path = "../style" -[dependencies.lyon] -version = "1.0" -optional = true - -[dependencies.qrcode] -version = "0.12" -optional = true -default-features = false - [dependencies.image_rs] version = "0.24" package = "image" diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs index 8658cffe..c44372e8 100644 --- a/graphics/src/backend.rs +++ b/graphics/src/backend.rs @@ -10,6 +10,8 @@ use std::borrow::Cow; /// /// [`Renderer`]: crate::Renderer pub trait Backend { + type Geometry: Into; + /// Trims the measurements cache. /// /// This method is currently necessary to properly trim the text cache in diff --git a/graphics/src/gradient.rs b/graphics/src/gradient.rs deleted file mode 100644 index 61e919d6..00000000 --- a/graphics/src/gradient.rs +++ /dev/null @@ -1,117 +0,0 @@ -//! For creating a Gradient. -pub mod linear; - -pub use linear::Linear; - -use crate::{Color, Point, Size}; - -#[derive(Debug, Clone, PartialEq)] -/// A fill which transitions colors progressively along a direction, either linearly, radially (TBD), -/// or conically (TBD). -pub enum Gradient { - /// A linear gradient interpolates colors along a direction from its `start` to its `end` - /// point. - Linear(Linear), -} - -impl Gradient { - /// Creates a new linear [`linear::Builder`]. - pub fn linear(position: impl Into) -> linear::Builder { - linear::Builder::new(position.into()) - } -} - -#[derive(Debug, Clone, Copy, PartialEq)] -/// A point along the gradient vector where the specified [`color`] is unmixed. -/// -/// [`color`]: Self::color -pub struct ColorStop { - /// Offset along the gradient vector. - pub offset: f32, - - /// The color of the gradient at the specified [`offset`]. - /// - /// [`offset`]: Self::offset - pub color: Color, -} - -#[derive(Debug)] -/// The position of the gradient within its bounds. -pub enum Position { - /// The gradient will be positioned with respect to two points. - Absolute { - /// The starting point of the gradient. - start: Point, - /// The ending point of the gradient. - end: Point, - }, - /// The gradient will be positioned relative to the provided bounds. - Relative { - /// The top left position of the bounds. - top_left: Point, - /// The width & height of the bounds. - size: Size, - /// The start [Location] of the gradient. - start: Location, - /// The end [Location] of the gradient. - end: Location, - }, -} - -impl From<(Point, Point)> for Position { - fn from((start, end): (Point, Point)) -> Self { - Self::Absolute { start, end } - } -} - -#[derive(Debug, Clone, Copy)] -/// The location of a relatively-positioned gradient. -pub enum Location { - /// Top left. - TopLeft, - /// Top. - Top, - /// Top right. - TopRight, - /// Right. - Right, - /// Bottom right. - BottomRight, - /// Bottom. - Bottom, - /// Bottom left. - BottomLeft, - /// Left. - Left, -} - -impl Location { - fn to_absolute(self, top_left: Point, size: Size) -> Point { - match self { - Location::TopLeft => top_left, - Location::Top => { - Point::new(top_left.x + size.width / 2.0, top_left.y) - } - Location::TopRight => { - Point::new(top_left.x + size.width, top_left.y) - } - Location::Right => Point::new( - top_left.x + size.width, - top_left.y + size.height / 2.0, - ), - Location::BottomRight => { - Point::new(top_left.x + size.width, top_left.y + size.height) - } - Location::Bottom => Point::new( - top_left.x + size.width / 2.0, - top_left.y + size.height, - ), - Location::BottomLeft => { - Point::new(top_left.x, top_left.y + size.height) - } - Location::Left => { - Point::new(top_left.x, top_left.y + size.height / 2.0) - } - } - } -} diff --git a/graphics/src/gradient/linear.rs b/graphics/src/gradient/linear.rs deleted file mode 100644 index c886db47..00000000 --- a/graphics/src/gradient/linear.rs +++ /dev/null @@ -1,112 +0,0 @@ -//! Linear gradient builder & definition. -use crate::gradient::{ColorStop, Gradient, Position}; -use crate::{Color, Point}; - -/// A linear gradient that can be used in the style of [`Fill`] or [`Stroke`]. -/// -/// [`Fill`]: crate::widget::canvas::Fill -/// [`Stroke`]: crate::widget::canvas::Stroke -#[derive(Debug, Clone, PartialEq)] -pub struct Linear { - /// The point where the linear gradient begins. - pub start: Point, - /// The point where the linear gradient ends. - pub end: Point, - /// [`ColorStop`]s along the linear gradient path. - pub color_stops: Vec, -} - -/// A [`Linear`] builder. -#[derive(Debug)] -pub struct Builder { - start: Point, - end: Point, - stops: Vec, - error: Option, -} - -impl Builder { - /// Creates a new [`Builder`]. - pub fn new(position: Position) -> Self { - let (start, end) = match position { - Position::Absolute { start, end } => (start, end), - Position::Relative { - top_left, - size, - start, - end, - } => ( - start.to_absolute(top_left, size), - end.to_absolute(top_left, size), - ), - }; - - Self { - start, - end, - stops: vec![], - error: None, - } - } - - /// Adds a new stop, defined by an offset and a color, to the gradient. - /// - /// `offset` must be between `0.0` and `1.0` or the gradient cannot be built. - /// - /// Note: when using the [`glow`] backend, any color stop added after the 16th - /// will not be displayed. - /// - /// On the [`wgpu`] backend this limitation does not exist (technical limit is 524,288 stops). - /// - /// [`glow`]: https://docs.rs/iced_glow - /// [`wgpu`]: https://docs.rs/iced_wgpu - pub fn add_stop(mut self, offset: f32, color: Color) -> Self { - if offset.is_finite() && (0.0..=1.0).contains(&offset) { - match self.stops.binary_search_by(|stop| { - stop.offset.partial_cmp(&offset).unwrap() - }) { - Ok(_) => { - self.error = Some(BuilderError::DuplicateOffset(offset)) - } - Err(index) => { - self.stops.insert(index, ColorStop { offset, color }); - } - } - } else { - self.error = Some(BuilderError::InvalidOffset(offset)) - }; - - self - } - - /// Builds the linear [`Gradient`] of this [`Builder`]. - /// - /// Returns `BuilderError` if gradient in invalid. - pub fn build(self) -> Result { - if self.stops.is_empty() { - Err(BuilderError::MissingColorStop) - } else if let Some(error) = self.error { - Err(error) - } else { - Ok(Gradient::Linear(Linear { - start: self.start, - end: self.end, - color_stops: self.stops, - })) - } - } -} - -/// An error that happened when building a [`Linear`] gradient. -#[derive(Debug, thiserror::Error)] -pub enum BuilderError { - #[error("Gradients must contain at least one color stop.")] - /// Gradients must contain at least one color stop. - MissingColorStop, - #[error("Offset {0} must be a unique, finite number.")] - /// Offsets in a gradient must all be unique & finite. - DuplicateOffset(f32), - #[error("Offset {0} must be between 0.0..=1.0.")] - /// Offsets in a gradient must be between 0.0..=1.0. - InvalidOffset(f32), -} diff --git a/graphics/src/layer.rs b/graphics/src/layer.rs deleted file mode 100644 index f6eb2fdd..00000000 --- a/graphics/src/layer.rs +++ /dev/null @@ -1,270 +0,0 @@ -//! Organize rendering primitives into a flattened list of layers. -mod image; -mod quad; -mod text; - -pub mod mesh; - -pub use image::Image; -pub use mesh::Mesh; -pub use quad::Quad; -pub use text::Text; - -use crate::alignment; -use crate::{ - Background, Color, Font, Point, Primitive, Rectangle, Size, Vector, - Viewport, -}; - -/// A group of primitives that should be clipped together. -#[derive(Debug)] -pub struct Layer<'a> { - /// The clipping bounds of the [`Layer`]. - pub bounds: Rectangle, - - /// The quads of the [`Layer`]. - pub quads: Vec, - - /// The triangle meshes of the [`Layer`]. - pub meshes: Vec>, - - /// The text of the [`Layer`]. - pub text: Vec>, - - /// The images of the [`Layer`]. - pub images: Vec, -} - -impl<'a> Layer<'a> { - /// Creates a new [`Layer`] with the given clipping bounds. - pub fn new(bounds: Rectangle) -> Self { - Self { - bounds, - quads: Vec::new(), - meshes: Vec::new(), - text: Vec::new(), - images: Vec::new(), - } - } - - /// Creates a new [`Layer`] for the provided overlay text. - /// - /// This can be useful for displaying debug information. - pub fn overlay(lines: &'a [impl AsRef], viewport: &Viewport) -> Self { - let mut overlay = - Layer::new(Rectangle::with_size(viewport.logical_size())); - - for (i, line) in lines.iter().enumerate() { - let text = Text { - content: line.as_ref(), - bounds: Rectangle::new( - Point::new(11.0, 11.0 + 25.0 * i as f32), - Size::INFINITY, - ), - color: Color::new(0.9, 0.9, 0.9, 1.0), - size: 20.0, - font: Font::Monospace, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, - }; - - overlay.text.push(text); - - overlay.text.push(Text { - bounds: text.bounds + Vector::new(-1.0, -1.0), - color: Color::BLACK, - ..text - }); - } - - overlay - } - - /// Distributes the given [`Primitive`] and generates a list of layers based - /// on its contents. - pub fn generate( - primitives: &'a [Primitive], - viewport: &Viewport, - ) -> Vec { - let first_layer = - Layer::new(Rectangle::with_size(viewport.logical_size())); - - let mut layers = vec![first_layer]; - - for primitive in primitives { - Self::process_primitive( - &mut layers, - Vector::new(0.0, 0.0), - primitive, - 0, - ); - } - - layers - } - - fn process_primitive( - layers: &mut Vec, - translation: Vector, - primitive: &'a Primitive, - current_layer: usize, - ) { - match primitive { - Primitive::None => {} - Primitive::Group { primitives } => { - // TODO: Inspect a bit and regroup (?) - for primitive in primitives { - Self::process_primitive( - layers, - translation, - primitive, - current_layer, - ) - } - } - Primitive::Text { - content, - bounds, - size, - color, - font, - horizontal_alignment, - vertical_alignment, - } => { - let layer = &mut layers[current_layer]; - - layer.text.push(Text { - content, - bounds: *bounds + translation, - size: *size, - color: *color, - font: *font, - horizontal_alignment: *horizontal_alignment, - vertical_alignment: *vertical_alignment, - }); - } - Primitive::Quad { - bounds, - background, - border_radius, - border_width, - border_color, - } => { - let layer = &mut layers[current_layer]; - - // TODO: Move some of these computations to the GPU (?) - layer.quads.push(Quad { - position: [ - bounds.x + translation.x, - bounds.y + translation.y, - ], - size: [bounds.width, bounds.height], - color: match background { - Background::Color(color) => color.into_linear(), - }, - border_radius: *border_radius, - border_width: *border_width, - border_color: border_color.into_linear(), - }); - } - Primitive::SolidMesh { buffers, size } => { - let layer = &mut layers[current_layer]; - - let bounds = Rectangle::new( - Point::new(translation.x, translation.y), - *size, - ); - - // Only draw visible content - if let Some(clip_bounds) = layer.bounds.intersection(&bounds) { - layer.meshes.push(Mesh::Solid { - origin: Point::new(translation.x, translation.y), - buffers, - clip_bounds, - }); - } - } - Primitive::GradientMesh { - buffers, - size, - gradient, - } => { - let layer = &mut layers[current_layer]; - - let bounds = Rectangle::new( - Point::new(translation.x, translation.y), - *size, - ); - - // Only draw visible content - if let Some(clip_bounds) = layer.bounds.intersection(&bounds) { - layer.meshes.push(Mesh::Gradient { - origin: Point::new(translation.x, translation.y), - buffers, - clip_bounds, - gradient, - }); - } - } - Primitive::Clip { bounds, content } => { - let layer = &mut layers[current_layer]; - let translated_bounds = *bounds + translation; - - // Only draw visible content - if let Some(clip_bounds) = - layer.bounds.intersection(&translated_bounds) - { - let clip_layer = Layer::new(clip_bounds); - layers.push(clip_layer); - - Self::process_primitive( - layers, - translation, - content, - layers.len() - 1, - ); - } - } - Primitive::Translate { - translation: new_translation, - content, - } => { - Self::process_primitive( - layers, - translation + *new_translation, - content, - current_layer, - ); - } - Primitive::Cached { cache } => { - Self::process_primitive( - layers, - translation, - cache, - current_layer, - ); - } - Primitive::Image { handle, bounds } => { - let layer = &mut layers[current_layer]; - - layer.images.push(Image::Raster { - handle: handle.clone(), - bounds: *bounds + translation, - }); - } - Primitive::Svg { - handle, - color, - bounds, - } => { - let layer = &mut layers[current_layer]; - - layer.images.push(Image::Vector { - handle: handle.clone(), - color: *color, - bounds: *bounds + translation, - }); - } - } - } -} diff --git a/graphics/src/layer/image.rs b/graphics/src/layer/image.rs deleted file mode 100644 index 3eff2397..00000000 --- a/graphics/src/layer/image.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::{Color, Rectangle}; - -use iced_native::{image, svg}; - -/// A raster or vector image. -#[derive(Debug, Clone)] -pub enum Image { - /// A raster image. - Raster { - /// The handle of a raster image. - handle: image::Handle, - - /// The bounds of the image. - bounds: Rectangle, - }, - /// A vector image. - Vector { - /// The handle of a vector image. - handle: svg::Handle, - - /// The [`Color`] filter - color: Option, - - /// The bounds of the image. - bounds: Rectangle, - }, -} diff --git a/graphics/src/layer/mesh.rs b/graphics/src/layer/mesh.rs deleted file mode 100644 index 7661c5c9..00000000 --- a/graphics/src/layer/mesh.rs +++ /dev/null @@ -1,93 +0,0 @@ -//! A collection of triangle primitives. -use crate::triangle; -use crate::{Gradient, Point, Rectangle}; - -/// A mesh of triangles. -#[derive(Debug, Clone, Copy)] -pub enum Mesh<'a> { - /// A mesh of triangles with a solid color. - Solid { - /// The origin of the vertices of the [`Mesh`]. - origin: Point, - - /// The vertex and index buffers of the [`Mesh`]. - buffers: &'a triangle::Mesh2D, - - /// The clipping bounds of the [`Mesh`]. - clip_bounds: Rectangle, - }, - /// A mesh of triangles with a gradient color. - Gradient { - /// The origin of the vertices of the [`Mesh`]. - origin: Point, - - /// The vertex and index buffers of the [`Mesh`]. - buffers: &'a triangle::Mesh2D, - - /// The clipping bounds of the [`Mesh`]. - clip_bounds: Rectangle, - - /// The gradient to apply to the [`Mesh`]. - gradient: &'a Gradient, - }, -} - -impl Mesh<'_> { - /// Returns the origin of the [`Mesh`]. - pub fn origin(&self) -> Point { - match self { - Self::Solid { origin, .. } | Self::Gradient { origin, .. } => { - *origin - } - } - } - - /// Returns the indices of the [`Mesh`]. - pub fn indices(&self) -> &[u32] { - match self { - Self::Solid { buffers, .. } => &buffers.indices, - Self::Gradient { buffers, .. } => &buffers.indices, - } - } - - /// Returns the clip bounds of the [`Mesh`]. - pub fn clip_bounds(&self) -> Rectangle { - match self { - Self::Solid { clip_bounds, .. } - | Self::Gradient { clip_bounds, .. } => *clip_bounds, - } - } -} - -/// The result of counting the attributes of a set of meshes. -#[derive(Debug, Clone, Copy, Default)] -pub struct AttributeCount { - /// The total amount of solid vertices. - pub solid_vertices: usize, - - /// The total amount of gradient vertices. - pub gradient_vertices: usize, - - /// The total amount of indices. - pub indices: usize, -} - -/// Returns the number of total vertices & total indices of all [`Mesh`]es. -pub fn attribute_count_of<'a>(meshes: &'a [Mesh<'a>]) -> AttributeCount { - meshes - .iter() - .fold(AttributeCount::default(), |mut count, mesh| { - match mesh { - Mesh::Solid { buffers, .. } => { - count.solid_vertices += buffers.vertices.len(); - count.indices += buffers.indices.len(); - } - Mesh::Gradient { buffers, .. } => { - count.gradient_vertices += buffers.vertices.len(); - count.indices += buffers.indices.len(); - } - } - - count - }) -} diff --git a/graphics/src/layer/quad.rs b/graphics/src/layer/quad.rs deleted file mode 100644 index 0d8bde9d..00000000 --- a/graphics/src/layer/quad.rs +++ /dev/null @@ -1,30 +0,0 @@ -/// A colored rectangle with a border. -/// -/// This type can be directly uploaded to GPU memory. -#[derive(Debug, Clone, Copy)] -#[repr(C)] -pub struct Quad { - /// The position of the [`Quad`]. - pub position: [f32; 2], - - /// The size of the [`Quad`]. - pub size: [f32; 2], - - /// The color of the [`Quad`], in __linear RGB__. - pub color: [f32; 4], - - /// The border color of the [`Quad`], in __linear RGB__. - pub border_color: [f32; 4], - - /// The border radius of the [`Quad`]. - pub border_radius: [f32; 4], - - /// The border width of the [`Quad`]. - pub border_width: f32, -} - -#[allow(unsafe_code)] -unsafe impl bytemuck::Zeroable for Quad {} - -#[allow(unsafe_code)] -unsafe impl bytemuck::Pod for Quad {} diff --git a/graphics/src/layer/text.rs b/graphics/src/layer/text.rs deleted file mode 100644 index 38d62616..00000000 --- a/graphics/src/layer/text.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::{alignment, Color, Font, Rectangle}; - -/// A paragraph of text. -#[derive(Debug, Clone, Copy)] -pub struct Text<'a> { - /// The content of the [`Text`]. - pub content: &'a str, - - /// The layout bounds of the [`Text`]. - pub bounds: Rectangle, - - /// The color of the [`Text`], in __linear RGB_. - pub color: Color, - - /// The size of the [`Text`]. - pub size: f32, - - /// The font of the [`Text`]. - pub font: Font, - - /// The horizontal alignment of the [`Text`]. - pub horizontal_alignment: alignment::Horizontal, - - /// The vertical alignment of the [`Text`]. - pub vertical_alignment: alignment::Vertical, -} diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index bbbdfa0e..576b2d78 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -9,7 +9,7 @@ )] #![deny( missing_debug_implementations, - missing_docs, + //missing_docs, unsafe_code, unused_results, clippy::extra_unused_lifetimes, @@ -23,25 +23,19 @@ #![cfg_attr(docsrs, feature(doc_cfg))] mod antialiasing; mod error; -mod primitive; mod transformation; mod viewport; pub mod backend; -pub mod gradient; pub mod image; -pub mod layer; pub mod overlay; +pub mod primitive; pub mod renderer; -pub mod triangle; -pub mod widget; pub mod window; pub use antialiasing::Antialiasing; pub use backend::Backend; pub use error::Error; -pub use gradient::Gradient; -pub use layer::Layer; pub use primitive::Primitive; pub use renderer::Renderer; pub use transformation::Transformation; @@ -50,5 +44,6 @@ pub use viewport::Viewport; pub use iced_native::alignment; pub use iced_native::text; pub use iced_native::{ - Alignment, Background, Color, Font, Point, Rectangle, Size, Vector, + Alignment, Background, Color, Font, Gradient, Point, Rectangle, Size, + Vector, }; diff --git a/graphics/src/primitive.rs b/graphics/src/primitive.rs index 5a163a2f..e4826591 100644 --- a/graphics/src/primitive.rs +++ b/graphics/src/primitive.rs @@ -1,23 +1,15 @@ +use crate::alignment; + use iced_native::image; use iced_native::svg; -use iced_native::{Background, Color, Font, Rectangle, Size, Vector}; - -use crate::alignment; -use crate::gradient::Gradient; -use crate::triangle; +use iced_native::{Background, Color, Font, Gradient, Rectangle, Size, Vector}; +use bytemuck::{Pod, Zeroable}; use std::sync::Arc; /// A rendering primitive. #[derive(Debug, Clone)] pub enum Primitive { - /// An empty primitive - None, - /// A group of primitives - Group { - /// The primitives of the group - primitives: Vec, - }, /// A text primitive Text { /// The contents of the text @@ -66,27 +58,12 @@ pub enum Primitive { /// The bounds of the viewport bounds: Rectangle, }, - /// A clip primitive - Clip { - /// The bounds of the clip - bounds: Rectangle, - /// The content of the clip - content: Box, - }, - /// A primitive that applies a translation - Translate { - /// The translation vector - translation: Vector, - - /// The primitive to translate - content: Box, - }, /// A low-level primitive to render a mesh of triangles with a solid color. /// /// It can be used to render many kinds of geometry freely. SolidMesh { /// The vertices and indices of the mesh. - buffers: triangle::Mesh2D, + buffers: Mesh2D, /// The size of the drawable region of the mesh. /// @@ -98,7 +75,7 @@ pub enum Primitive { /// It can be used to render many kinds of geometry freely. GradientMesh { /// The vertices and indices of the mesh. - buffers: triangle::Mesh2D, + buffers: Mesh2D, /// The size of the drawable region of the mesh. /// @@ -108,18 +85,101 @@ pub enum Primitive { /// The [`Gradient`] to apply to the mesh. gradient: Gradient, }, + Fill { + path: tiny_skia::Path, + paint: tiny_skia::Paint<'static>, + rule: tiny_skia::FillRule, + transform: tiny_skia::Transform, + }, + Stroke { + path: tiny_skia::Path, + paint: tiny_skia::Paint<'static>, + stroke: tiny_skia::Stroke, + transform: tiny_skia::Transform, + }, + /// A group of primitives + Group { + /// The primitives of the group + primitives: Vec, + }, + /// A clip primitive + Clip { + /// The bounds of the clip + bounds: Rectangle, + /// The content of the clip + content: Box, + }, + /// A primitive that applies a translation + Translate { + /// The translation vector + translation: Vector, + + /// The primitive to translate + content: Box, + }, /// A cached primitive. /// /// This can be useful if you are implementing a widget where primitive /// generation is expensive. - Cached { + Cache { /// The cached primitive - cache: Arc, + content: Arc, }, } -impl Default for Primitive { - fn default() -> Primitive { - Primitive::None +impl Primitive { + pub fn group(primitives: Vec) -> Self { + Self::Group { primitives } + } + + pub fn clip(self, bounds: Rectangle) -> Self { + Self::Clip { + bounds, + content: Box::new(self), + } + } + + pub fn translate(self, translation: Vector) -> Self { + Self::Translate { + translation, + content: Box::new(self), + } + } +} + +/// A set of [`Vertex2D`] and indices representing a list of triangles. +#[derive(Clone, Debug)] +pub struct Mesh2D { + /// The vertices of the mesh + pub vertices: Vec, + + /// The list of vertex indices that defines the triangles of the mesh. + /// + /// Therefore, this list should always have a length that is a multiple of 3. + pub indices: Vec, +} + +/// A two-dimensional vertex. +#[derive(Copy, Clone, Debug, Zeroable, Pod)] +#[repr(C)] +pub struct Vertex2D { + /// The vertex position in 2D space. + pub position: [f32; 2], +} + +/// A two-dimensional vertex with a color. +#[derive(Copy, Clone, Debug, Zeroable, Pod)] +#[repr(C)] +pub struct ColoredVertex2D { + /// The vertex position in 2D space. + pub position: [f32; 2], + + /// The color of the vertex in __linear__ RGBA. + pub color: [f32; 4], +} + +impl From<()> for Primitive { + fn from(_: ()) -> Self { + Self::Group { primitives: vec![] } } } diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index 859ebc04..793ee7d7 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -1,6 +1,7 @@ //! Create a renderer from a [`Backend`]. use crate::backend::{self, Backend}; use crate::{Primitive, Vector}; + use iced_native::image; use iced_native::layout; use iced_native::renderer; @@ -70,19 +71,13 @@ where } fn with_layer(&mut self, bounds: Rectangle, f: impl FnOnce(&mut Self)) { - let current_primitives = std::mem::take(&mut self.primitives); + let current = std::mem::take(&mut self.primitives); f(self); - let layer_primitives = - std::mem::replace(&mut self.primitives, current_primitives); + let layer = std::mem::replace(&mut self.primitives, current); - self.primitives.push(Primitive::Clip { - bounds, - content: Box::new(Primitive::Group { - primitives: layer_primitives, - }), - }); + self.primitives.push(Primitive::group(layer).clip(bounds)); } fn with_translation( @@ -90,19 +85,14 @@ where translation: Vector, f: impl FnOnce(&mut Self), ) { - let current_primitives = std::mem::take(&mut self.primitives); + let current = std::mem::take(&mut self.primitives); f(self); - let layer_primitives = - std::mem::replace(&mut self.primitives, current_primitives); + let layer = std::mem::replace(&mut self.primitives, current); - self.primitives.push(Primitive::Translate { - translation, - content: Box::new(Primitive::Group { - primitives: layer_primitives, - }), - }); + self.primitives + .push(Primitive::group(layer).translate(translation)); } fn fill_quad( @@ -199,7 +189,7 @@ where } fn draw(&mut self, handle: image::Handle, bounds: Rectangle) { - self.draw_primitive(Primitive::Image { handle, bounds }) + self.primitives.push(Primitive::Image { handle, bounds }) } } @@ -217,10 +207,23 @@ where color: Option, bounds: Rectangle, ) { - self.draw_primitive(Primitive::Svg { + self.primitives.push(Primitive::Svg { handle, color, bounds, }) } } + +#[cfg(feature = "canvas")] +impl iced_native::widget::canvas::Renderer for Renderer +where + B: Backend, +{ + type Geometry = B::Geometry; + + fn draw(&mut self, layers: Vec) { + self.primitives + .extend(layers.into_iter().map(B::Geometry::into)); + } +} diff --git a/graphics/src/triangle.rs b/graphics/src/triangle.rs index f52b2339..09b61767 100644 --- a/graphics/src/triangle.rs +++ b/graphics/src/triangle.rs @@ -1,33 +1 @@ //! Draw geometry using meshes of triangles. -use bytemuck::{Pod, Zeroable}; - -/// A set of [`Vertex2D`] and indices representing a list of triangles. -#[derive(Clone, Debug)] -pub struct Mesh2D { - /// The vertices of the mesh - pub vertices: Vec, - - /// The list of vertex indices that defines the triangles of the mesh. - /// - /// Therefore, this list should always have a length that is a multiple of 3. - pub indices: Vec, -} - -/// A two-dimensional vertex. -#[derive(Copy, Clone, Debug, Zeroable, Pod)] -#[repr(C)] -pub struct Vertex2D { - /// The vertex position in 2D space. - pub position: [f32; 2], -} - -/// A two-dimensional vertex with a color. -#[derive(Copy, Clone, Debug, Zeroable, Pod)] -#[repr(C)] -pub struct ColoredVertex2D { - /// The vertex position in 2D space. - pub position: [f32; 2], - - /// The color of the vertex in __linear__ RGBA. - pub color: [f32; 4], -} diff --git a/graphics/src/widget.rs b/graphics/src/widget.rs deleted file mode 100644 index e7fab97c..00000000 --- a/graphics/src/widget.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Use the graphical widgets supported out-of-the-box. -#[cfg(feature = "canvas")] -#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] -pub mod canvas; - -#[cfg(feature = "canvas")] -#[doc(no_inline)] -pub use canvas::Canvas; - -#[cfg(feature = "qr_code")] -#[cfg_attr(docsrs, doc(cfg(feature = "qr_code")))] -pub mod qr_code; - -#[cfg(feature = "qr_code")] -#[doc(no_inline)] -pub use qr_code::QRCode; diff --git a/graphics/src/widget/canvas.rs b/graphics/src/widget/canvas.rs deleted file mode 100644 index a8d050f5..00000000 --- a/graphics/src/widget/canvas.rs +++ /dev/null @@ -1,268 +0,0 @@ -//! Draw 2D graphics for your users. -//! -//! A [`Canvas`] widget can be used to draw different kinds of 2D shapes in a -//! [`Frame`]. It can be used for animation, data visualization, game graphics, -//! and more! -pub mod event; -pub mod fill; -pub mod path; -pub mod stroke; - -mod cache; -mod cursor; -mod frame; -mod geometry; -mod program; -mod style; -mod text; - -pub use crate::gradient::{self, Gradient}; -pub use cache::Cache; -pub use cursor::Cursor; -pub use event::Event; -pub use fill::{Fill, FillRule}; -pub use frame::Frame; -pub use geometry::Geometry; -pub use path::Path; -pub use program::Program; -pub use stroke::{LineCap, LineDash, LineJoin, Stroke}; -pub use style::Style; -pub use text::Text; - -use crate::{Backend, Primitive, Renderer}; - -use iced_native::layout::{self, Layout}; -use iced_native::mouse; -use iced_native::renderer; -use iced_native::widget::tree::{self, Tree}; -use iced_native::{ - Clipboard, Element, Length, Point, Rectangle, Shell, Size, Vector, Widget, -}; - -use std::marker::PhantomData; - -/// A widget capable of drawing 2D graphics. -/// -/// ## Drawing a simple circle -/// If you want to get a quick overview, here's how we can draw a simple circle: -/// -/// ```no_run -/// # mod iced { -/// # pub mod widget { -/// # pub use iced_graphics::widget::canvas; -/// # } -/// # pub use iced_native::{Color, Rectangle, Theme}; -/// # } -/// use iced::widget::canvas::{self, Canvas, Cursor, Fill, Frame, Geometry, Path, Program}; -/// use iced::{Color, Rectangle, Theme}; -/// -/// // First, we define the data we need for drawing -/// #[derive(Debug)] -/// struct Circle { -/// radius: f32, -/// } -/// -/// // Then, we implement the `Program` trait -/// impl Program<()> for Circle { -/// type State = (); -/// -/// fn draw(&self, _state: &(), _theme: &Theme, bounds: Rectangle, _cursor: Cursor) -> Vec{ -/// // We prepare a new `Frame` -/// let mut frame = Frame::new(bounds.size()); -/// -/// // We create a `Path` representing a simple circle -/// let circle = Path::circle(frame.center(), self.radius); -/// -/// // And fill it with some color -/// frame.fill(&circle, Color::BLACK); -/// -/// // Finally, we produce the geometry -/// vec![frame.into_geometry()] -/// } -/// } -/// -/// // Finally, we simply use our `Circle` to create the `Canvas`! -/// let canvas = Canvas::new(Circle { radius: 50.0 }); -/// ``` -#[derive(Debug)] -pub struct Canvas -where - P: Program, -{ - width: Length, - height: Length, - program: P, - message_: PhantomData, - theme_: PhantomData, -} - -impl Canvas -where - P: Program, -{ - const DEFAULT_SIZE: f32 = 100.0; - - /// Creates a new [`Canvas`]. - pub fn new(program: P) -> Self { - Canvas { - width: Length::Fixed(Self::DEFAULT_SIZE), - height: Length::Fixed(Self::DEFAULT_SIZE), - program, - message_: PhantomData, - theme_: PhantomData, - } - } - - /// Sets the width of the [`Canvas`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the height of the [`Canvas`]. - pub fn height(mut self, height: impl Into) -> Self { - self.height = height.into(); - self - } -} - -impl Widget> for Canvas -where - P: Program, - B: Backend, -{ - fn tag(&self) -> tree::Tag { - struct Tag(T); - tree::Tag::of::>() - } - - fn state(&self) -> tree::State { - tree::State::new(P::State::default()) - } - - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height - } - - fn layout( - &self, - _renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - let size = limits.resolve(Size::ZERO); - - layout::Node::new(size) - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: iced_native::Event, - layout: Layout<'_>, - cursor_position: Point, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - let bounds = layout.bounds(); - - let canvas_event = match event { - iced_native::Event::Mouse(mouse_event) => { - Some(Event::Mouse(mouse_event)) - } - iced_native::Event::Touch(touch_event) => { - Some(Event::Touch(touch_event)) - } - iced_native::Event::Keyboard(keyboard_event) => { - Some(Event::Keyboard(keyboard_event)) - } - _ => None, - }; - - let cursor = Cursor::from_window_position(cursor_position); - - if let Some(canvas_event) = canvas_event { - let state = tree.state.downcast_mut::(); - - let (event_status, message) = - self.program.update(state, canvas_event, bounds, cursor); - - if let Some(message) = message { - shell.publish(message); - } - - return event_status; - } - - event::Status::Ignored - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - let bounds = layout.bounds(); - let cursor = Cursor::from_window_position(cursor_position); - let state = tree.state.downcast_ref::(); - - self.program.mouse_interaction(state, bounds, cursor) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &T, - _style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - use iced_native::Renderer as _; - - let bounds = layout.bounds(); - - if bounds.width < 1.0 || bounds.height < 1.0 { - return; - } - - let translation = Vector::new(bounds.x, bounds.y); - let cursor = Cursor::from_window_position(cursor_position); - let state = tree.state.downcast_ref::(); - - renderer.with_translation(translation, |renderer| { - renderer.draw_primitive(Primitive::Group { - primitives: self - .program - .draw(state, theme, bounds, cursor) - .into_iter() - .map(Geometry::into_primitive) - .collect(), - }); - }); - } -} - -impl<'a, Message, P, B, T> From> - for Element<'a, Message, Renderer> -where - Message: 'a, - P: Program + 'a, - B: Backend, - T: 'a, -{ - fn from( - canvas: Canvas, - ) -> Element<'a, Message, Renderer> { - Element::new(canvas) - } -} diff --git a/graphics/src/widget/canvas/cache.rs b/graphics/src/widget/canvas/cache.rs deleted file mode 100644 index 52217bbb..00000000 --- a/graphics/src/widget/canvas/cache.rs +++ /dev/null @@ -1,100 +0,0 @@ -use crate::widget::canvas::{Frame, Geometry}; -use crate::Primitive; - -use iced_native::Size; -use std::{cell::RefCell, sync::Arc}; - -enum State { - Empty, - Filled { - bounds: Size, - primitive: Arc, - }, -} - -impl Default for State { - fn default() -> Self { - State::Empty - } -} -/// A simple cache that stores generated [`Geometry`] to avoid recomputation. -/// -/// A [`Cache`] will not redraw its geometry unless the dimensions of its layer -/// change or it is explicitly cleared. -#[derive(Debug, Default)] -pub struct Cache { - state: RefCell, -} - -impl Cache { - /// Creates a new empty [`Cache`]. - pub fn new() -> Self { - Cache { - state: Default::default(), - } - } - - /// Clears the [`Cache`], forcing a redraw the next time it is used. - pub fn clear(&self) { - *self.state.borrow_mut() = State::Empty; - } - - /// Draws [`Geometry`] using the provided closure and stores it in the - /// [`Cache`]. - /// - /// The closure will only be called when - /// - the bounds have changed since the previous draw call. - /// - the [`Cache`] is empty or has been explicitly cleared. - /// - /// Otherwise, the previously stored [`Geometry`] will be returned. The - /// [`Cache`] is not cleared in this case. In other words, it will keep - /// returning the stored [`Geometry`] if needed. - pub fn draw( - &self, - bounds: Size, - draw_fn: impl FnOnce(&mut Frame), - ) -> Geometry { - use std::ops::Deref; - - if let State::Filled { - bounds: cached_bounds, - primitive, - } = self.state.borrow().deref() - { - if *cached_bounds == bounds { - return Geometry::from_primitive(Primitive::Cached { - cache: primitive.clone(), - }); - } - } - - let mut frame = Frame::new(bounds); - draw_fn(&mut frame); - - let primitive = { - let geometry = frame.into_geometry(); - - Arc::new(geometry.into_primitive()) - }; - - *self.state.borrow_mut() = State::Filled { - bounds, - primitive: primitive.clone(), - }; - - Geometry::from_primitive(Primitive::Cached { cache: primitive }) - } -} - -impl std::fmt::Debug for State { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - State::Empty => write!(f, "Empty"), - State::Filled { primitive, bounds } => f - .debug_struct("Filled") - .field("primitive", primitive) - .field("bounds", bounds) - .finish(), - } - } -} diff --git a/graphics/src/widget/canvas/cursor.rs b/graphics/src/widget/canvas/cursor.rs deleted file mode 100644 index 9588d129..00000000 --- a/graphics/src/widget/canvas/cursor.rs +++ /dev/null @@ -1,64 +0,0 @@ -use iced_native::{Point, Rectangle}; - -/// The mouse cursor state. -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Cursor { - /// The cursor has a defined position. - Available(Point), - - /// The cursor is currently unavailable (i.e. out of bounds or busy). - Unavailable, -} - -impl Cursor { - // TODO: Remove this once this type is used in `iced_native` to encode - // proper cursor availability - pub(crate) fn from_window_position(position: Point) -> Self { - if position.x < 0.0 || position.y < 0.0 { - Cursor::Unavailable - } else { - Cursor::Available(position) - } - } - - /// Returns the absolute position of the [`Cursor`], if available. - pub fn position(&self) -> Option { - match self { - Cursor::Available(position) => Some(*position), - Cursor::Unavailable => None, - } - } - - /// Returns the relative position of the [`Cursor`] inside the given bounds, - /// if available. - /// - /// If the [`Cursor`] is not over the provided bounds, this method will - /// return `None`. - pub fn position_in(&self, bounds: &Rectangle) -> Option { - if self.is_over(bounds) { - self.position_from(bounds.position()) - } else { - None - } - } - - /// Returns the relative position of the [`Cursor`] from the given origin, - /// if available. - pub fn position_from(&self, origin: Point) -> Option { - match self { - Cursor::Available(position) => { - Some(Point::new(position.x - origin.x, position.y - origin.y)) - } - Cursor::Unavailable => None, - } - } - - /// Returns whether the [`Cursor`] is currently over the provided bounds - /// or not. - pub fn is_over(&self, bounds: &Rectangle) -> bool { - match self { - Cursor::Available(position) => bounds.contains(*position), - Cursor::Unavailable => false, - } - } -} diff --git a/graphics/src/widget/canvas/event.rs b/graphics/src/widget/canvas/event.rs deleted file mode 100644 index 7c733a4d..00000000 --- a/graphics/src/widget/canvas/event.rs +++ /dev/null @@ -1,21 +0,0 @@ -//! Handle events of a canvas. -use iced_native::keyboard; -use iced_native::mouse; -use iced_native::touch; - -pub use iced_native::event::Status; - -/// A [`Canvas`] event. -/// -/// [`Canvas`]: crate::widget::Canvas -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Event { - /// A mouse event. - Mouse(mouse::Event), - - /// A touch event. - Touch(touch::Event), - - /// A keyboard event. - Keyboard(keyboard::Event), -} diff --git a/graphics/src/widget/canvas/fill.rs b/graphics/src/widget/canvas/fill.rs deleted file mode 100644 index e954ebb5..00000000 --- a/graphics/src/widget/canvas/fill.rs +++ /dev/null @@ -1,72 +0,0 @@ -//! Fill [crate::widget::canvas::Geometry] with a certain style. -use crate::{Color, Gradient}; - -pub use crate::widget::canvas::Style; - -/// The style used to fill geometry. -#[derive(Debug, Clone)] -pub struct Fill { - /// The color or gradient of the fill. - /// - /// By default, it is set to [`Style::Solid`] with [`Color::BLACK`]. - pub style: Style, - - /// The fill rule defines how to determine what is inside and what is - /// outside of a shape. - /// - /// See the [SVG specification][1] for more details. - /// - /// By default, it is set to `NonZero`. - /// - /// [1]: https://www.w3.org/TR/SVG/painting.html#FillRuleProperty - pub rule: FillRule, -} - -impl Default for Fill { - fn default() -> Self { - Self { - style: Style::Solid(Color::BLACK), - rule: FillRule::NonZero, - } - } -} - -impl From for Fill { - fn from(color: Color) -> Fill { - Fill { - style: Style::Solid(color), - ..Fill::default() - } - } -} - -impl From for Fill { - fn from(gradient: Gradient) -> Self { - Fill { - style: Style::Gradient(gradient), - ..Default::default() - } - } -} - -/// The fill rule defines how to determine what is inside and what is outside of -/// a shape. -/// -/// See the [SVG specification][1]. -/// -/// [1]: https://www.w3.org/TR/SVG/painting.html#FillRuleProperty -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[allow(missing_docs)] -pub enum FillRule { - NonZero, - EvenOdd, -} - -impl From for lyon::tessellation::FillRule { - fn from(rule: FillRule) -> lyon::tessellation::FillRule { - match rule { - FillRule::NonZero => lyon::tessellation::FillRule::NonZero, - FillRule::EvenOdd => lyon::tessellation::FillRule::EvenOdd, - } - } -} diff --git a/graphics/src/widget/canvas/frame.rs b/graphics/src/widget/canvas/frame.rs deleted file mode 100644 index d68548ae..00000000 --- a/graphics/src/widget/canvas/frame.rs +++ /dev/null @@ -1,530 +0,0 @@ -use crate::gradient::Gradient; -use crate::triangle; -use crate::widget::canvas::{path, Fill, Geometry, Path, Stroke, Style, Text}; -use crate::Primitive; - -use iced_native::{Point, Rectangle, Size, Vector}; - -use lyon::geom::euclid; -use lyon::tessellation; -use std::borrow::Cow; - -/// The frame of a [`Canvas`]. -/// -/// [`Canvas`]: crate::widget::Canvas -#[allow(missing_debug_implementations)] -pub struct Frame { - size: Size, - buffers: BufferStack, - primitives: Vec, - transforms: Transforms, - fill_tessellator: tessellation::FillTessellator, - stroke_tessellator: tessellation::StrokeTessellator, -} - -enum Buffer { - Solid(tessellation::VertexBuffers), - Gradient( - tessellation::VertexBuffers, - Gradient, - ), -} - -struct BufferStack { - stack: Vec, -} - -impl BufferStack { - fn new() -> Self { - Self { stack: Vec::new() } - } - - fn get_mut(&mut self, style: &Style) -> &mut Buffer { - match style { - Style::Solid(_) => match self.stack.last() { - Some(Buffer::Solid(_)) => {} - _ => { - self.stack.push(Buffer::Solid( - tessellation::VertexBuffers::new(), - )); - } - }, - Style::Gradient(gradient) => match self.stack.last() { - Some(Buffer::Gradient(_, last)) if gradient == last => {} - _ => { - self.stack.push(Buffer::Gradient( - tessellation::VertexBuffers::new(), - gradient.clone(), - )); - } - }, - } - - self.stack.last_mut().unwrap() - } - - fn get_fill<'a>( - &'a mut self, - style: &Style, - ) -> Box { - match (style, self.get_mut(style)) { - (Style::Solid(color), Buffer::Solid(buffer)) => { - Box::new(tessellation::BuffersBuilder::new( - buffer, - TriangleVertex2DBuilder(color.into_linear()), - )) - } - (Style::Gradient(_), Buffer::Gradient(buffer, _)) => Box::new( - tessellation::BuffersBuilder::new(buffer, Vertex2DBuilder), - ), - _ => unreachable!(), - } - } - - fn get_stroke<'a>( - &'a mut self, - style: &Style, - ) -> Box { - match (style, self.get_mut(style)) { - (Style::Solid(color), Buffer::Solid(buffer)) => { - Box::new(tessellation::BuffersBuilder::new( - buffer, - TriangleVertex2DBuilder(color.into_linear()), - )) - } - (Style::Gradient(_), Buffer::Gradient(buffer, _)) => Box::new( - tessellation::BuffersBuilder::new(buffer, Vertex2DBuilder), - ), - _ => unreachable!(), - } - } -} - -#[derive(Debug)] -struct Transforms { - previous: Vec, - current: Transform, -} - -#[derive(Debug, Clone, Copy)] -struct Transform { - raw: lyon::math::Transform, - is_identity: bool, -} - -impl Transform { - /// Transforms the given [Point] by the transformation matrix. - fn transform_point(&self, point: &mut Point) { - let transformed = self - .raw - .transform_point(euclid::Point2D::new(point.x, point.y)); - point.x = transformed.x; - point.y = transformed.y; - } - - fn transform_style(&self, style: Style) -> Style { - match style { - Style::Solid(color) => Style::Solid(color), - Style::Gradient(gradient) => { - Style::Gradient(self.transform_gradient(gradient)) - } - } - } - - fn transform_gradient(&self, mut gradient: Gradient) -> Gradient { - let (start, end) = match &mut gradient { - Gradient::Linear(linear) => (&mut linear.start, &mut linear.end), - }; - self.transform_point(start); - self.transform_point(end); - gradient - } -} - -impl Frame { - /// Creates a new empty [`Frame`] with the given dimensions. - /// - /// The default coordinate system of a [`Frame`] has its origin at the - /// top-left corner of its bounds. - pub fn new(size: Size) -> Frame { - Frame { - size, - buffers: BufferStack::new(), - primitives: Vec::new(), - transforms: Transforms { - previous: Vec::new(), - current: Transform { - raw: lyon::math::Transform::identity(), - is_identity: true, - }, - }, - fill_tessellator: tessellation::FillTessellator::new(), - stroke_tessellator: tessellation::StrokeTessellator::new(), - } - } - - /// Returns the width of the [`Frame`]. - #[inline] - pub fn width(&self) -> f32 { - self.size.width - } - - /// Returns the height of the [`Frame`]. - #[inline] - pub fn height(&self) -> f32 { - self.size.height - } - - /// Returns the dimensions of the [`Frame`]. - #[inline] - pub fn size(&self) -> Size { - self.size - } - - /// Returns the coordinate of the center of the [`Frame`]. - #[inline] - pub fn center(&self) -> Point { - Point::new(self.size.width / 2.0, self.size.height / 2.0) - } - - /// Draws the given [`Path`] on the [`Frame`] by filling it with the - /// provided style. - pub fn fill(&mut self, path: &Path, fill: impl Into) { - let Fill { style, rule } = fill.into(); - - let mut buffer = self - .buffers - .get_fill(&self.transforms.current.transform_style(style)); - - let options = - tessellation::FillOptions::default().with_fill_rule(rule.into()); - - if self.transforms.current.is_identity { - self.fill_tessellator.tessellate_path( - path.raw(), - &options, - buffer.as_mut(), - ) - } else { - let path = path.transformed(&self.transforms.current.raw); - - self.fill_tessellator.tessellate_path( - path.raw(), - &options, - buffer.as_mut(), - ) - } - .expect("Tessellate path."); - } - - /// Draws an axis-aligned rectangle given its top-left corner coordinate and - /// its `Size` on the [`Frame`] by filling it with the provided style. - pub fn fill_rectangle( - &mut self, - top_left: Point, - size: Size, - fill: impl Into, - ) { - let Fill { style, rule } = fill.into(); - - let mut buffer = self - .buffers - .get_fill(&self.transforms.current.transform_style(style)); - - let top_left = - self.transforms.current.raw.transform_point( - lyon::math::Point::new(top_left.x, top_left.y), - ); - - let size = - self.transforms.current.raw.transform_vector( - lyon::math::Vector::new(size.width, size.height), - ); - - let options = - tessellation::FillOptions::default().with_fill_rule(rule.into()); - - self.fill_tessellator - .tessellate_rectangle( - &lyon::math::Box2D::new(top_left, top_left + size), - &options, - buffer.as_mut(), - ) - .expect("Fill rectangle"); - } - - /// Draws the stroke of the given [`Path`] on the [`Frame`] with the - /// provided style. - pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { - let stroke = stroke.into(); - - let mut buffer = self - .buffers - .get_stroke(&self.transforms.current.transform_style(stroke.style)); - - let mut options = tessellation::StrokeOptions::default(); - options.line_width = stroke.width; - options.start_cap = stroke.line_cap.into(); - options.end_cap = stroke.line_cap.into(); - options.line_join = stroke.line_join.into(); - - let path = if stroke.line_dash.segments.is_empty() { - Cow::Borrowed(path) - } else { - Cow::Owned(path::dashed(path, stroke.line_dash)) - }; - - if self.transforms.current.is_identity { - self.stroke_tessellator.tessellate_path( - path.raw(), - &options, - buffer.as_mut(), - ) - } else { - let path = path.transformed(&self.transforms.current.raw); - - self.stroke_tessellator.tessellate_path( - path.raw(), - &options, - buffer.as_mut(), - ) - } - .expect("Stroke path"); - } - - /// Draws the characters of the given [`Text`] on the [`Frame`], filling - /// them with the given color. - /// - /// __Warning:__ Text currently does not work well with rotations and scale - /// transforms! The position will be correctly transformed, but the - /// resulting glyphs will not be rotated or scaled properly. - /// - /// Additionally, all text will be rendered on top of all the layers of - /// a [`Canvas`]. Therefore, it is currently only meant to be used for - /// overlays, which is the most common use case. - /// - /// Support for vectorial text is planned, and should address all these - /// limitations. - /// - /// [`Canvas`]: crate::widget::Canvas - pub fn fill_text(&mut self, text: impl Into) { - let text = text.into(); - - let position = if self.transforms.current.is_identity { - text.position - } else { - let transformed = self.transforms.current.raw.transform_point( - lyon::math::Point::new(text.position.x, text.position.y), - ); - - Point::new(transformed.x, transformed.y) - }; - - // TODO: Use vectorial text instead of primitive - self.primitives.push(Primitive::Text { - content: text.content, - bounds: Rectangle { - x: position.x, - y: position.y, - width: f32::INFINITY, - height: f32::INFINITY, - }, - color: text.color, - size: text.size, - font: text.font, - horizontal_alignment: text.horizontal_alignment, - vertical_alignment: text.vertical_alignment, - }); - } - - /// Stores the current transform of the [`Frame`] and executes the given - /// drawing operations, restoring the transform afterwards. - /// - /// This method is useful to compose transforms and perform drawing - /// operations in different coordinate systems. - #[inline] - pub fn with_save(&mut self, f: impl FnOnce(&mut Frame)) { - self.transforms.previous.push(self.transforms.current); - - f(self); - - self.transforms.current = self.transforms.previous.pop().unwrap(); - } - - /// Executes the given drawing operations within a [`Rectangle`] region, - /// clipping any geometry that overflows its bounds. Any transformations - /// performed are local to the provided closure. - /// - /// This method is useful to perform drawing operations that need to be - /// clipped. - #[inline] - pub fn with_clip(&mut self, region: Rectangle, f: impl FnOnce(&mut Frame)) { - let mut frame = Frame::new(region.size()); - - f(&mut frame); - - let primitives = frame.into_primitives(); - - let (text, meshes) = primitives - .into_iter() - .partition(|primitive| matches!(primitive, Primitive::Text { .. })); - - let translation = Vector::new(region.x, region.y); - - self.primitives.push(Primitive::Group { - primitives: vec![ - Primitive::Translate { - translation, - content: Box::new(Primitive::Group { primitives: meshes }), - }, - Primitive::Translate { - translation, - content: Box::new(Primitive::Clip { - bounds: Rectangle::with_size(region.size()), - content: Box::new(Primitive::Group { - primitives: text, - }), - }), - }, - ], - }); - } - - /// Applies a translation to the current transform of the [`Frame`]. - #[inline] - pub fn translate(&mut self, translation: Vector) { - self.transforms.current.raw = self - .transforms - .current - .raw - .pre_translate(lyon::math::Vector::new( - translation.x, - translation.y, - )); - self.transforms.current.is_identity = false; - } - - /// Applies a rotation in radians to the current transform of the [`Frame`]. - #[inline] - pub fn rotate(&mut self, angle: f32) { - self.transforms.current.raw = self - .transforms - .current - .raw - .pre_rotate(lyon::math::Angle::radians(angle)); - self.transforms.current.is_identity = false; - } - - /// Applies a scaling to the current transform of the [`Frame`]. - #[inline] - pub fn scale(&mut self, scale: f32) { - self.transforms.current.raw = - self.transforms.current.raw.pre_scale(scale, scale); - self.transforms.current.is_identity = false; - } - - /// Produces the [`Geometry`] representing everything drawn on the [`Frame`]. - pub fn into_geometry(self) -> Geometry { - Geometry::from_primitive(Primitive::Group { - primitives: self.into_primitives(), - }) - } - - fn into_primitives(mut self) -> Vec { - for buffer in self.buffers.stack { - match buffer { - Buffer::Solid(buffer) => { - if !buffer.indices.is_empty() { - self.primitives.push(Primitive::SolidMesh { - buffers: triangle::Mesh2D { - vertices: buffer.vertices, - indices: buffer.indices, - }, - size: self.size, - }) - } - } - Buffer::Gradient(buffer, gradient) => { - if !buffer.indices.is_empty() { - self.primitives.push(Primitive::GradientMesh { - buffers: triangle::Mesh2D { - vertices: buffer.vertices, - indices: buffer.indices, - }, - size: self.size, - gradient, - }) - } - } - } - } - - self.primitives - } -} - -struct Vertex2DBuilder; - -impl tessellation::FillVertexConstructor - for Vertex2DBuilder -{ - fn new_vertex( - &mut self, - vertex: tessellation::FillVertex<'_>, - ) -> triangle::Vertex2D { - let position = vertex.position(); - - triangle::Vertex2D { - position: [position.x, position.y], - } - } -} - -impl tessellation::StrokeVertexConstructor - for Vertex2DBuilder -{ - fn new_vertex( - &mut self, - vertex: tessellation::StrokeVertex<'_, '_>, - ) -> triangle::Vertex2D { - let position = vertex.position(); - - triangle::Vertex2D { - position: [position.x, position.y], - } - } -} - -struct TriangleVertex2DBuilder([f32; 4]); - -impl tessellation::FillVertexConstructor - for TriangleVertex2DBuilder -{ - fn new_vertex( - &mut self, - vertex: tessellation::FillVertex<'_>, - ) -> triangle::ColoredVertex2D { - let position = vertex.position(); - - triangle::ColoredVertex2D { - position: [position.x, position.y], - color: self.0, - } - } -} - -impl tessellation::StrokeVertexConstructor - for TriangleVertex2DBuilder -{ - fn new_vertex( - &mut self, - vertex: tessellation::StrokeVertex<'_, '_>, - ) -> triangle::ColoredVertex2D { - let position = vertex.position(); - - triangle::ColoredVertex2D { - position: [position.x, position.y], - color: self.0, - } - } -} diff --git a/graphics/src/widget/canvas/geometry.rs b/graphics/src/widget/canvas/geometry.rs deleted file mode 100644 index e8ac621d..00000000 --- a/graphics/src/widget/canvas/geometry.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::Primitive; - -/// A bunch of shapes that can be drawn. -/// -/// [`Geometry`] can be easily generated with a [`Frame`] or stored in a -/// [`Cache`]. -/// -/// [`Frame`]: crate::widget::canvas::Frame -/// [`Cache`]: crate::widget::canvas::Cache -#[derive(Debug, Clone)] -pub struct Geometry(Primitive); - -impl Geometry { - pub(crate) fn from_primitive(primitive: Primitive) -> Self { - Self(primitive) - } - - /// Turns the [`Geometry`] into a [`Primitive`]. - /// - /// This can be useful if you are building a custom widget. - pub fn into_primitive(self) -> Primitive { - self.0 - } -} diff --git a/graphics/src/widget/canvas/path.rs b/graphics/src/widget/canvas/path.rs deleted file mode 100644 index aeb2589e..00000000 --- a/graphics/src/widget/canvas/path.rs +++ /dev/null @@ -1,109 +0,0 @@ -//! Build different kinds of 2D shapes. -pub mod arc; - -mod builder; - -#[doc(no_inline)] -pub use arc::Arc; -pub use builder::Builder; - -use crate::widget::canvas::LineDash; - -use iced_native::{Point, Size}; -use lyon::algorithms::walk::{walk_along_path, RepeatedPattern, WalkerEvent}; -use lyon::path::iterator::PathIterator; - -/// An immutable set of points that may or may not be connected. -/// -/// A single [`Path`] can represent different kinds of 2D shapes! -#[derive(Debug, Clone)] -pub struct Path { - raw: lyon::path::Path, -} - -impl Path { - /// Creates a new [`Path`] with the provided closure. - /// - /// Use the [`Builder`] to configure your [`Path`]. - pub fn new(f: impl FnOnce(&mut Builder)) -> Self { - let mut builder = Builder::new(); - - // TODO: Make it pure instead of side-effect-based (?) - f(&mut builder); - - builder.build() - } - - /// Creates a new [`Path`] representing a line segment given its starting - /// and end points. - pub fn line(from: Point, to: Point) -> Self { - Self::new(|p| { - p.move_to(from); - p.line_to(to); - }) - } - - /// Creates a new [`Path`] representing a rectangle given its top-left - /// corner coordinate and its `Size`. - pub fn rectangle(top_left: Point, size: Size) -> Self { - Self::new(|p| p.rectangle(top_left, size)) - } - - /// Creates a new [`Path`] representing a circle given its center - /// coordinate and its radius. - pub fn circle(center: Point, radius: f32) -> Self { - Self::new(|p| p.circle(center, radius)) - } - - #[inline] - pub(crate) fn raw(&self) -> &lyon::path::Path { - &self.raw - } - - #[inline] - pub(crate) fn transformed( - &self, - transform: &lyon::math::Transform, - ) -> Path { - Path { - raw: self.raw.clone().transformed(transform), - } - } -} - -pub(super) fn dashed(path: &Path, line_dash: LineDash<'_>) -> Path { - Path::new(|builder| { - let segments_odd = (line_dash.segments.len() % 2 == 1) - .then(|| [line_dash.segments, line_dash.segments].concat()); - - let mut draw_line = false; - - walk_along_path( - path.raw().iter().flattened(0.01), - 0.0, - lyon::tessellation::StrokeOptions::DEFAULT_TOLERANCE, - &mut RepeatedPattern { - callback: |event: WalkerEvent<'_>| { - let point = Point { - x: event.position.x, - y: event.position.y, - }; - - if draw_line { - builder.line_to(point); - } else { - builder.move_to(point); - } - - draw_line = !draw_line; - - true - }, - index: line_dash.offset, - intervals: segments_odd - .as_deref() - .unwrap_or(line_dash.segments), - }, - ); - }) -} diff --git a/graphics/src/widget/canvas/path/arc.rs b/graphics/src/widget/canvas/path/arc.rs deleted file mode 100644 index b8e72daf..00000000 --- a/graphics/src/widget/canvas/path/arc.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! Build and draw curves. -use iced_native::{Point, Vector}; - -/// A segment of a differentiable curve. -#[derive(Debug, Clone, Copy)] -pub struct Arc { - /// The center of the arc. - pub center: Point, - /// The radius of the arc. - pub radius: f32, - /// The start of the segment's angle, clockwise rotation. - pub start_angle: f32, - /// The end of the segment's angle, clockwise rotation. - pub end_angle: f32, -} - -/// An elliptical [`Arc`]. -#[derive(Debug, Clone, Copy)] -pub struct Elliptical { - /// The center of the arc. - pub center: Point, - /// The radii of the arc's ellipse, defining its axes. - pub radii: Vector, - /// The rotation of the arc's ellipse. - pub rotation: f32, - /// The start of the segment's angle, clockwise rotation. - pub start_angle: f32, - /// The end of the segment's angle, clockwise rotation. - pub end_angle: f32, -} - -impl From for Elliptical { - fn from(arc: Arc) -> Elliptical { - Elliptical { - center: arc.center, - radii: Vector::new(arc.radius, arc.radius), - rotation: 0.0, - start_angle: arc.start_angle, - end_angle: arc.end_angle, - } - } -} diff --git a/graphics/src/widget/canvas/path/builder.rs b/graphics/src/widget/canvas/path/builder.rs deleted file mode 100644 index 5121aa68..00000000 --- a/graphics/src/widget/canvas/path/builder.rs +++ /dev/null @@ -1,198 +0,0 @@ -use crate::widget::canvas::path::{arc, Arc, Path}; - -use iced_native::{Point, Size}; -use lyon::path::builder::SvgPathBuilder; - -/// A [`Path`] builder. -/// -/// Once a [`Path`] is built, it can no longer be mutated. -#[allow(missing_debug_implementations)] -pub struct Builder { - raw: lyon::path::builder::WithSvg, -} - -impl Builder { - /// Creates a new [`Builder`]. - pub fn new() -> Builder { - Builder { - raw: lyon::path::Path::builder().with_svg(), - } - } - - /// Moves the starting point of a new sub-path to the given `Point`. - #[inline] - pub fn move_to(&mut self, point: Point) { - let _ = self.raw.move_to(lyon::math::Point::new(point.x, point.y)); - } - - /// Connects the last point in the [`Path`] to the given `Point` with a - /// straight line. - #[inline] - pub fn line_to(&mut self, point: Point) { - let _ = self.raw.line_to(lyon::math::Point::new(point.x, point.y)); - } - - /// Adds an [`Arc`] to the [`Path`] from `start_angle` to `end_angle` in - /// a clockwise direction. - #[inline] - pub fn arc(&mut self, arc: Arc) { - self.ellipse(arc.into()); - } - - /// Adds a circular arc to the [`Path`] with the given control points and - /// radius. - /// - /// This essentially draws a straight line segment from the current - /// position to `a`, but fits a circular arc of `radius` tangent to that - /// segment and tangent to the line between `a` and `b`. - /// - /// With another `.line_to(b)`, the result will be a path connecting the - /// starting point and `b` with straight line segments towards `a` and a - /// circular arc smoothing out the corner at `a`. - /// - /// See [the HTML5 specification of `arcTo`](https://html.spec.whatwg.org/multipage/canvas.html#building-paths:dom-context-2d-arcto) - /// for more details and examples. - pub fn arc_to(&mut self, a: Point, b: Point, radius: f32) { - use lyon::{math, path}; - - let start = self.raw.current_position(); - let mid = math::Point::new(a.x, a.y); - let end = math::Point::new(b.x, b.y); - - if start == mid || mid == end || radius == 0.0 { - let _ = self.raw.line_to(mid); - return; - } - - let double_area = start.x * (mid.y - end.y) - + mid.x * (end.y - start.y) - + end.x * (start.y - mid.y); - - if double_area == 0.0 { - let _ = self.raw.line_to(mid); - return; - } - - let to_start = (start - mid).normalize(); - let to_end = (end - mid).normalize(); - - let inner_angle = to_start.dot(to_end).acos(); - - let origin_angle = inner_angle / 2.0; - - let origin_adjacent = radius / origin_angle.tan(); - - let arc_start = mid + to_start * origin_adjacent; - let arc_end = mid + to_end * origin_adjacent; - - let sweep = to_start.cross(to_end) < 0.0; - - let _ = self.raw.line_to(arc_start); - - self.raw.arc_to( - math::Vector::new(radius, radius), - math::Angle::radians(0.0), - path::ArcFlags { - large_arc: false, - sweep, - }, - arc_end, - ); - } - - /// Adds an ellipse to the [`Path`] using a clockwise direction. - pub fn ellipse(&mut self, arc: arc::Elliptical) { - use lyon::{geom, math}; - - let arc = geom::Arc { - center: math::Point::new(arc.center.x, arc.center.y), - radii: math::Vector::new(arc.radii.x, arc.radii.y), - x_rotation: math::Angle::radians(arc.rotation), - start_angle: math::Angle::radians(arc.start_angle), - sweep_angle: math::Angle::radians(arc.end_angle - arc.start_angle), - }; - - let _ = self.raw.move_to(arc.sample(0.0)); - - arc.for_each_quadratic_bezier(&mut |curve| { - let _ = self.raw.quadratic_bezier_to(curve.ctrl, curve.to); - }); - } - - /// Adds a cubic Bézier curve to the [`Path`] given its two control points - /// and its end point. - #[inline] - pub fn bezier_curve_to( - &mut self, - control_a: Point, - control_b: Point, - to: Point, - ) { - use lyon::math; - - let _ = self.raw.cubic_bezier_to( - math::Point::new(control_a.x, control_a.y), - math::Point::new(control_b.x, control_b.y), - math::Point::new(to.x, to.y), - ); - } - - /// Adds a quadratic Bézier curve to the [`Path`] given its control point - /// and its end point. - #[inline] - pub fn quadratic_curve_to(&mut self, control: Point, to: Point) { - use lyon::math; - - let _ = self.raw.quadratic_bezier_to( - math::Point::new(control.x, control.y), - math::Point::new(to.x, to.y), - ); - } - - /// Adds a rectangle to the [`Path`] given its top-left corner coordinate - /// and its `Size`. - #[inline] - pub fn rectangle(&mut self, top_left: Point, size: Size) { - self.move_to(top_left); - self.line_to(Point::new(top_left.x + size.width, top_left.y)); - self.line_to(Point::new( - top_left.x + size.width, - top_left.y + size.height, - )); - self.line_to(Point::new(top_left.x, top_left.y + size.height)); - self.close(); - } - - /// Adds a circle to the [`Path`] given its center coordinate and its - /// radius. - #[inline] - pub fn circle(&mut self, center: Point, radius: f32) { - self.arc(Arc { - center, - radius, - start_angle: 0.0, - end_angle: 2.0 * std::f32::consts::PI, - }); - } - - /// Closes the current sub-path in the [`Path`] with a straight line to - /// the starting point. - #[inline] - pub fn close(&mut self) { - self.raw.close() - } - - /// Builds the [`Path`] of this [`Builder`]. - #[inline] - pub fn build(self) -> Path { - Path { - raw: self.raw.build(), - } - } -} - -impl Default for Builder { - fn default() -> Self { - Self::new() - } -} diff --git a/graphics/src/widget/canvas/program.rs b/graphics/src/widget/canvas/program.rs deleted file mode 100644 index 656dbfa6..00000000 --- a/graphics/src/widget/canvas/program.rs +++ /dev/null @@ -1,102 +0,0 @@ -use crate::widget::canvas::event::{self, Event}; -use crate::widget::canvas::mouse; -use crate::widget::canvas::{Cursor, Geometry}; -use crate::Rectangle; - -/// The state and logic of a [`Canvas`]. -/// -/// A [`Program`] can mutate internal state and produce messages for an -/// application. -/// -/// [`Canvas`]: crate::widget::Canvas -pub trait Program { - /// The internal state mutated by the [`Program`]. - type State: Default + 'static; - - /// Updates the [`State`](Self::State) of the [`Program`]. - /// - /// When a [`Program`] is used in a [`Canvas`], the runtime will call this - /// method for each [`Event`]. - /// - /// This method can optionally return a `Message` to notify an application - /// of any meaningful interactions. - /// - /// By default, this method does and returns nothing. - /// - /// [`Canvas`]: crate::widget::Canvas - fn update( - &self, - _state: &mut Self::State, - _event: Event, - _bounds: Rectangle, - _cursor: Cursor, - ) -> (event::Status, Option) { - (event::Status::Ignored, None) - } - - /// Draws the state of the [`Program`], producing a bunch of [`Geometry`]. - /// - /// [`Geometry`] can be easily generated with a [`Frame`] or stored in a - /// [`Cache`]. - /// - /// [`Frame`]: crate::widget::canvas::Frame - /// [`Cache`]: crate::widget::canvas::Cache - fn draw( - &self, - state: &Self::State, - theme: &Theme, - bounds: Rectangle, - cursor: Cursor, - ) -> Vec; - - /// Returns the current mouse interaction of the [`Program`]. - /// - /// The interaction returned will be in effect even if the cursor position - /// is out of bounds of the program's [`Canvas`]. - /// - /// [`Canvas`]: crate::widget::Canvas - fn mouse_interaction( - &self, - _state: &Self::State, - _bounds: Rectangle, - _cursor: Cursor, - ) -> mouse::Interaction { - mouse::Interaction::default() - } -} - -impl Program for &T -where - T: Program, -{ - type State = T::State; - - fn update( - &self, - state: &mut Self::State, - event: Event, - bounds: Rectangle, - cursor: Cursor, - ) -> (event::Status, Option) { - T::update(self, state, event, bounds, cursor) - } - - fn draw( - &self, - state: &Self::State, - theme: &Theme, - bounds: Rectangle, - cursor: Cursor, - ) -> Vec { - T::draw(self, state, theme, bounds, cursor) - } - - fn mouse_interaction( - &self, - state: &Self::State, - bounds: Rectangle, - cursor: Cursor, - ) -> mouse::Interaction { - T::mouse_interaction(self, state, bounds, cursor) - } -} diff --git a/graphics/src/widget/canvas/stroke.rs b/graphics/src/widget/canvas/stroke.rs deleted file mode 100644 index 4c19251d..00000000 --- a/graphics/src/widget/canvas/stroke.rs +++ /dev/null @@ -1,126 +0,0 @@ -//! Create lines from a [crate::widget::canvas::Path] and assigns them various attributes/styles. -pub use crate::widget::canvas::Style; - -use iced_native::Color; - -/// The style of a stroke. -#[derive(Debug, Clone)] -pub struct Stroke<'a> { - /// The color or gradient of the stroke. - /// - /// By default, it is set to a [`Style::Solid`] with [`Color::BLACK`]. - pub style: Style, - /// The distance between the two edges of the stroke. - pub width: f32, - /// The shape to be used at the end of open subpaths when they are stroked. - pub line_cap: LineCap, - /// The shape to be used at the corners of paths or basic shapes when they - /// are stroked. - pub line_join: LineJoin, - /// The dash pattern used when stroking the line. - pub line_dash: LineDash<'a>, -} - -impl<'a> Stroke<'a> { - /// Sets the color of the [`Stroke`]. - pub fn with_color(self, color: Color) -> Self { - Stroke { - style: Style::Solid(color), - ..self - } - } - - /// Sets the width of the [`Stroke`]. - pub fn with_width(self, width: f32) -> Self { - Stroke { width, ..self } - } - - /// Sets the [`LineCap`] of the [`Stroke`]. - pub fn with_line_cap(self, line_cap: LineCap) -> Self { - Stroke { line_cap, ..self } - } - - /// Sets the [`LineJoin`] of the [`Stroke`]. - pub fn with_line_join(self, line_join: LineJoin) -> Self { - Stroke { line_join, ..self } - } -} - -impl<'a> Default for Stroke<'a> { - fn default() -> Self { - Stroke { - style: Style::Solid(Color::BLACK), - width: 1.0, - line_cap: LineCap::default(), - line_join: LineJoin::default(), - line_dash: LineDash::default(), - } - } -} - -/// The shape used at the end of open subpaths when they are stroked. -#[derive(Debug, Clone, Copy)] -pub enum LineCap { - /// The stroke for each sub-path does not extend beyond its two endpoints. - Butt, - /// At the end of each sub-path, the shape representing the stroke will be - /// extended by a square. - Square, - /// At the end of each sub-path, the shape representing the stroke will be - /// extended by a semicircle. - Round, -} - -impl Default for LineCap { - fn default() -> LineCap { - LineCap::Butt - } -} - -impl From for lyon::tessellation::LineCap { - fn from(line_cap: LineCap) -> lyon::tessellation::LineCap { - match line_cap { - LineCap::Butt => lyon::tessellation::LineCap::Butt, - LineCap::Square => lyon::tessellation::LineCap::Square, - LineCap::Round => lyon::tessellation::LineCap::Round, - } - } -} - -/// The shape used at the corners of paths or basic shapes when they are -/// stroked. -#[derive(Debug, Clone, Copy)] -pub enum LineJoin { - /// A sharp corner. - Miter, - /// A round corner. - Round, - /// A bevelled corner. - Bevel, -} - -impl Default for LineJoin { - fn default() -> LineJoin { - LineJoin::Miter - } -} - -impl From for lyon::tessellation::LineJoin { - fn from(line_join: LineJoin) -> lyon::tessellation::LineJoin { - match line_join { - LineJoin::Miter => lyon::tessellation::LineJoin::Miter, - LineJoin::Round => lyon::tessellation::LineJoin::Round, - LineJoin::Bevel => lyon::tessellation::LineJoin::Bevel, - } - } -} - -/// The dash pattern used when stroking the line. -#[derive(Debug, Clone, Copy, Default)] -pub struct LineDash<'a> { - /// The alternating lengths of lines and gaps which describe the pattern. - pub segments: &'a [f32], - - /// The offset of [`LineDash::segments`] to start the pattern. - pub offset: usize, -} diff --git a/graphics/src/widget/canvas/style.rs b/graphics/src/widget/canvas/style.rs deleted file mode 100644 index 6794f2e7..00000000 --- a/graphics/src/widget/canvas/style.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::{Color, Gradient}; - -/// The coloring style of some drawing. -#[derive(Debug, Clone, PartialEq)] -pub enum Style { - /// A solid [`Color`]. - Solid(Color), - - /// A [`Gradient`] color. - Gradient(Gradient), -} - -impl From for Style { - fn from(color: Color) -> Self { - Self::Solid(color) - } -} - -impl From for Style { - fn from(gradient: Gradient) -> Self { - Self::Gradient(gradient) - } -} diff --git a/graphics/src/widget/canvas/text.rs b/graphics/src/widget/canvas/text.rs deleted file mode 100644 index 8c0b2dfb..00000000 --- a/graphics/src/widget/canvas/text.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::alignment; -use crate::{Color, Font, Point}; - -/// A bunch of text that can be drawn to a canvas -#[derive(Debug, Clone)] -pub struct Text { - /// The contents of the text - pub content: String, - /// The position of the text relative to the alignment properties. - /// By default, this position will be relative to the top-left corner coordinate meaning that - /// if the horizontal and vertical alignments are unchanged, this property will tell where the - /// top-left corner of the text should be placed. - /// By changing the horizontal_alignment and vertical_alignment properties, you are are able to - /// change what part of text is placed at this positions. - /// For example, when the horizontal_alignment and vertical_alignment are set to Center, the - /// center of the text will be placed at the given position NOT the top-left coordinate. - pub position: Point, - /// The color of the text - pub color: Color, - /// The size of the text - pub size: f32, - /// The font of the text - pub font: Font, - /// The horizontal alignment of the text - pub horizontal_alignment: alignment::Horizontal, - /// The vertical alignment of the text - pub vertical_alignment: alignment::Vertical, -} - -impl Default for Text { - fn default() -> Text { - Text { - content: String::new(), - position: Point::ORIGIN, - color: Color::BLACK, - size: 16.0, - font: Font::SansSerif, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, - } - } -} - -impl From for Text { - fn from(content: String) -> Text { - Text { - content, - ..Default::default() - } - } -} - -impl From<&str> for Text { - fn from(content: &str) -> Text { - String::from(content).into() - } -} diff --git a/graphics/src/widget/qr_code.rs b/graphics/src/widget/qr_code.rs deleted file mode 100644 index 12ce5b1f..00000000 --- a/graphics/src/widget/qr_code.rs +++ /dev/null @@ -1,301 +0,0 @@ -//! Encode and display information in a QR code. -use crate::renderer::{self, Renderer}; -use crate::widget::canvas; -use crate::Backend; - -use iced_native::layout; -use iced_native::widget::Tree; -use iced_native::{ - Color, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget, -}; -use thiserror::Error; - -const DEFAULT_CELL_SIZE: u16 = 4; -const QUIET_ZONE: usize = 2; - -/// A type of matrix barcode consisting of squares arranged in a grid which -/// can be read by an imaging device, such as a camera. -#[derive(Debug)] -pub struct QRCode<'a> { - state: &'a State, - dark: Color, - light: Color, - cell_size: u16, -} - -impl<'a> QRCode<'a> { - /// Creates a new [`QRCode`] with the provided [`State`]. - pub fn new(state: &'a State) -> Self { - Self { - cell_size: DEFAULT_CELL_SIZE, - dark: Color::BLACK, - light: Color::WHITE, - state, - } - } - - /// Sets both the dark and light [`Color`]s of the [`QRCode`]. - pub fn color(mut self, dark: Color, light: Color) -> Self { - self.dark = dark; - self.light = light; - self - } - - /// Sets the size of the squares of the grid cell of the [`QRCode`]. - pub fn cell_size(mut self, cell_size: u16) -> Self { - self.cell_size = cell_size; - self - } -} - -impl<'a, Message, B, T> Widget> for QRCode<'a> -where - B: Backend, -{ - fn width(&self) -> Length { - Length::Shrink - } - - fn height(&self) -> Length { - Length::Shrink - } - - fn layout( - &self, - _renderer: &Renderer, - _limits: &layout::Limits, - ) -> layout::Node { - let side_length = (self.state.width + 2 * QUIET_ZONE) as f32 - * f32::from(self.cell_size); - - layout::Node::new(Size::new(side_length, side_length)) - } - - fn draw( - &self, - _state: &Tree, - renderer: &mut Renderer, - _theme: &T, - _style: &renderer::Style, - layout: Layout<'_>, - _cursor_position: Point, - _viewport: &Rectangle, - ) { - use iced_native::Renderer as _; - - let bounds = layout.bounds(); - let side_length = self.state.width + 2 * QUIET_ZONE; - - // Reuse cache if possible - let geometry = self.state.cache.draw(bounds.size(), |frame| { - // Scale units to cell size - frame.scale(f32::from(self.cell_size)); - - // Draw background - frame.fill_rectangle( - Point::ORIGIN, - Size::new(side_length as f32, side_length as f32), - self.light, - ); - - // Avoid drawing on the quiet zone - frame.translate(Vector::new(QUIET_ZONE as f32, QUIET_ZONE as f32)); - - // Draw contents - self.state - .contents - .iter() - .enumerate() - .filter(|(_, value)| **value == qrcode::Color::Dark) - .for_each(|(index, _)| { - let row = index / self.state.width; - let column = index % self.state.width; - - frame.fill_rectangle( - Point::new(column as f32, row as f32), - Size::UNIT, - self.dark, - ); - }); - }); - - let translation = Vector::new(bounds.x, bounds.y); - - renderer.with_translation(translation, |renderer| { - renderer.draw_primitive(geometry.into_primitive()); - }); - } -} - -impl<'a, Message, B, T> From> - for Element<'a, Message, Renderer> -where - B: Backend, -{ - fn from(qr_code: QRCode<'a>) -> Self { - Self::new(qr_code) - } -} - -/// The state of a [`QRCode`]. -/// -/// It stores the data that will be displayed. -#[derive(Debug)] -pub struct State { - contents: Vec, - width: usize, - cache: canvas::Cache, -} - -impl State { - /// Creates a new [`State`] with the provided data. - /// - /// This method uses an [`ErrorCorrection::Medium`] and chooses the smallest - /// size to display the data. - pub fn new(data: impl AsRef<[u8]>) -> Result { - let encoded = qrcode::QrCode::new(data)?; - - Ok(Self::build(encoded)) - } - - /// Creates a new [`State`] with the provided [`ErrorCorrection`]. - pub fn with_error_correction( - data: impl AsRef<[u8]>, - error_correction: ErrorCorrection, - ) -> Result { - let encoded = qrcode::QrCode::with_error_correction_level( - data, - error_correction.into(), - )?; - - Ok(Self::build(encoded)) - } - - /// Creates a new [`State`] with the provided [`Version`] and - /// [`ErrorCorrection`]. - pub fn with_version( - data: impl AsRef<[u8]>, - version: Version, - error_correction: ErrorCorrection, - ) -> Result { - let encoded = qrcode::QrCode::with_version( - data, - version.into(), - error_correction.into(), - )?; - - Ok(Self::build(encoded)) - } - - fn build(encoded: qrcode::QrCode) -> Self { - let width = encoded.width(); - let contents = encoded.into_colors(); - - Self { - contents, - width, - cache: canvas::Cache::new(), - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -/// The size of a [`QRCode`]. -/// -/// The higher the version the larger the grid of cells, and therefore the more -/// information the [`QRCode`] can carry. -pub enum Version { - /// A normal QR code version. It should be between 1 and 40. - Normal(u8), - - /// A micro QR code version. It should be between 1 and 4. - Micro(u8), -} - -impl From for qrcode::Version { - fn from(version: Version) -> Self { - match version { - Version::Normal(v) => qrcode::Version::Normal(i16::from(v)), - Version::Micro(v) => qrcode::Version::Micro(i16::from(v)), - } - } -} - -/// The error correction level. -/// -/// It controls the amount of data that can be damaged while still being able -/// to recover the original information. -/// -/// A higher error correction level allows for more corrupted data. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ErrorCorrection { - /// Low error correction. 7% of the data can be restored. - Low, - /// Medium error correction. 15% of the data can be restored. - Medium, - /// Quartile error correction. 25% of the data can be restored. - Quartile, - /// High error correction. 30% of the data can be restored. - High, -} - -impl From for qrcode::EcLevel { - fn from(ec_level: ErrorCorrection) -> Self { - match ec_level { - ErrorCorrection::Low => qrcode::EcLevel::L, - ErrorCorrection::Medium => qrcode::EcLevel::M, - ErrorCorrection::Quartile => qrcode::EcLevel::Q, - ErrorCorrection::High => qrcode::EcLevel::H, - } - } -} - -/// An error that occurred when building a [`State`] for a [`QRCode`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] -pub enum Error { - /// The data is too long to encode in a QR code for the chosen [`Version`]. - #[error( - "The data is too long to encode in a QR code for the chosen version" - )] - DataTooLong, - - /// The chosen [`Version`] and [`ErrorCorrection`] combination is invalid. - #[error( - "The chosen version and error correction level combination is invalid." - )] - InvalidVersion, - - /// One or more characters in the provided data are not supported by the - /// chosen [`Version`]. - #[error( - "One or more characters in the provided data are not supported by the \ - chosen version" - )] - UnsupportedCharacterSet, - - /// The chosen ECI designator is invalid. A valid designator should be - /// between 0 and 999999. - #[error( - "The chosen ECI designator is invalid. A valid designator should be \ - between 0 and 999999." - )] - InvalidEciDesignator, - - /// A character that does not belong to the character set was found. - #[error("A character that does not belong to the character set was found")] - InvalidCharacter, -} - -impl From for Error { - fn from(error: qrcode::types::QrError) -> Self { - use qrcode::types::QrError; - - match error { - QrError::DataTooLong => Error::DataTooLong, - QrError::InvalidVersion => Error::InvalidVersion, - QrError::UnsupportedCharacterSet => Error::UnsupportedCharacterSet, - QrError::InvalidEciDesignator => Error::InvalidEciDesignator, - QrError::InvalidCharacter => Error::InvalidCharacter, - } - } -} diff --git a/native/Cargo.toml b/native/Cargo.toml index 3f92783e..23533e33 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -8,12 +8,14 @@ license = "MIT" repository = "https://github.com/iced-rs/iced" [features] +canvas = ["lyon_path"] debug = [] [dependencies] twox-hash = { version = "1.5", default-features = false } unicode-segmentation = "1.6" num-traits = "0.2" +thiserror = "1" [dependencies.iced_core] version = "0.8" @@ -27,3 +29,7 @@ features = ["thread-pool"] [dependencies.iced_style] version = "0.7" path = "../style" + +[dependencies.lyon_path] +version = "1" +optional = true diff --git a/native/src/lib.rs b/native/src/lib.rs index 27b6fc0d..c98827e7 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -33,7 +33,7 @@ )] #![deny( missing_debug_implementations, - missing_docs, + //missing_docs, unused_results, clippy::extra_unused_lifetimes, clippy::from_over_into, @@ -79,6 +79,7 @@ mod debug; mod debug; pub use iced_core::alignment; +pub use iced_core::gradient; pub use iced_core::time; pub use iced_core::{ color, Alignment, Background, Color, ContentFit, Length, Padding, Pixels, @@ -97,6 +98,7 @@ pub use debug::Debug; pub use element::Element; pub use event::Event; pub use font::Font; +pub use gradient::Gradient; pub use hasher::Hasher; pub use layout::Layout; pub use overlay::Overlay; diff --git a/native/src/widget.rs b/native/src/widget.rs index 2b3ca7be..27330894 100644 --- a/native/src/widget.rs +++ b/native/src/widget.rs @@ -83,6 +83,22 @@ pub use tree::Tree; #[doc(no_inline)] pub use vertical_slider::VerticalSlider; +#[cfg(feature = "canvas")] +#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] +pub mod canvas; + +#[cfg(feature = "canvas")] +#[doc(no_inline)] +pub use canvas::Canvas; + +#[cfg(feature = "qr_code")] +#[cfg_attr(docsrs, doc(cfg(feature = "qr_code")))] +pub mod qr_code; + +#[cfg(feature = "qr_code")] +#[doc(no_inline)] +pub use qr_code::QRCode; + pub use action::Action; pub use id::Id; pub use operation::Operation; diff --git a/native/src/widget/canvas.rs b/native/src/widget/canvas.rs new file mode 100644 index 00000000..8a9addd2 --- /dev/null +++ b/native/src/widget/canvas.rs @@ -0,0 +1,259 @@ +//! Draw 2D graphics for your users. +//! +//! A [`Canvas`] widget can be used to draw different kinds of 2D shapes in a +//! [`Frame`]. It can be used for animation, data visualization, game graphics, +//! and more! +pub mod event; +pub mod fill; +pub mod path; +pub mod stroke; + +mod cursor; +mod program; +mod style; +mod text; + +pub use crate::gradient::{self, Gradient}; +pub use cursor::Cursor; +pub use event::Event; +pub use fill::Fill; +pub use path::Path; +pub use program::Program; +pub use stroke::{LineCap, LineDash, LineJoin, Stroke}; +pub use style::Style; +pub use text::Text; + +use crate::layout::{self, Layout}; +use crate::mouse; +use crate::renderer; +use crate::widget::tree::{self, Tree}; +use crate::{ + Clipboard, Element, Length, Point, Rectangle, Shell, Size, Vector, Widget, +}; + +use std::marker::PhantomData; + +/// A widget capable of drawing 2D graphics. +/// +/// ## Drawing a simple circle +/// If you want to get a quick overview, here's how we can draw a simple circle: +/// +/// ```no_run +/// # mod iced { +/// # pub mod widget { +/// # pub use iced_graphics::widget::canvas; +/// # } +/// # pub use iced_native::{Color, Rectangle, Theme}; +/// # } +/// use iced::widget::canvas::{self, Canvas, Cursor, Fill, Frame, Geometry, Path, Program}; +/// use iced::{Color, Rectangle, Theme}; +/// +/// // First, we define the data we need for drawing +/// #[derive(Debug)] +/// struct Circle { +/// radius: f32, +/// } +/// +/// // Then, we implement the `Program` trait +/// impl Program<()> for Circle { +/// type State = (); +/// +/// fn draw(&self, _state: &(), _theme: &Theme, bounds: Rectangle, _cursor: Cursor) -> Vec{ +/// // We prepare a new `Frame` +/// let mut frame = Frame::new(bounds.size()); +/// +/// // We create a `Path` representing a simple circle +/// let circle = Path::circle(frame.center(), self.radius); +/// +/// // And fill it with some color +/// frame.fill(&circle, Color::BLACK); +/// +/// // Finally, we produce the geometry +/// vec![frame.into_geometry()] +/// } +/// } +/// +/// // Finally, we simply use our `Circle` to create the `Canvas`! +/// let canvas = Canvas::new(Circle { radius: 50.0 }); +/// ``` +#[derive(Debug)] +pub struct Canvas +where + Renderer: self::Renderer, + P: Program, +{ + width: Length, + height: Length, + program: P, + message_: PhantomData, + theme_: PhantomData, +} + +impl Canvas +where + Renderer: self::Renderer, + P: Program, +{ + const DEFAULT_SIZE: f32 = 100.0; + + /// Creates a new [`Canvas`]. + pub fn new(program: P) -> Self { + Canvas { + width: Length::Fixed(Self::DEFAULT_SIZE), + height: Length::Fixed(Self::DEFAULT_SIZE), + program, + message_: PhantomData, + theme_: PhantomData, + } + } + + /// Sets the width of the [`Canvas`]. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Canvas`]. + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } +} + +impl Widget + for Canvas +where + Renderer: self::Renderer, + P: Program, +{ + fn tag(&self) -> tree::Tag { + struct Tag(T); + tree::Tag::of::>() + } + + fn state(&self) -> tree::State { + tree::State::new(P::State::default()) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + let size = limits.resolve(Size::ZERO); + + layout::Node::new(size) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: crate::Event, + layout: Layout<'_>, + cursor_position: Point, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + let bounds = layout.bounds(); + + let canvas_event = match event { + crate::Event::Mouse(mouse_event) => Some(Event::Mouse(mouse_event)), + crate::Event::Touch(touch_event) => Some(Event::Touch(touch_event)), + crate::Event::Keyboard(keyboard_event) => { + Some(Event::Keyboard(keyboard_event)) + } + _ => None, + }; + + let cursor = Cursor::from_window_position(cursor_position); + + if let Some(canvas_event) = canvas_event { + let state = tree.state.downcast_mut::(); + + let (event_status, message) = + self.program.update(state, canvas_event, bounds, cursor); + + if let Some(message) = message { + shell.publish(message); + } + + return event_status; + } + + event::Status::Ignored + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + let bounds = layout.bounds(); + let cursor = Cursor::from_window_position(cursor_position); + let state = tree.state.downcast_ref::(); + + self.program.mouse_interaction(state, bounds, cursor) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + + if bounds.width < 1.0 || bounds.height < 1.0 { + return; + } + + let cursor = Cursor::from_window_position(cursor_position); + let state = tree.state.downcast_ref::(); + + renderer.with_translation( + Vector::new(bounds.x, bounds.y), + |renderer| { + renderer.draw( + self.program.draw(state, renderer, theme, bounds, cursor), + ); + }, + ); + } +} + +impl<'a, Message, Renderer, P> From> + for Element<'a, Message, Renderer> +where + Message: 'a, + Renderer: 'a + self::Renderer, + P: Program + 'a, +{ + fn from( + canvas: Canvas, + ) -> Element<'a, Message, Renderer> { + Element::new(canvas) + } +} + +pub trait Renderer: crate::Renderer { + type Geometry; + + fn draw(&mut self, geometry: Vec); +} diff --git a/native/src/widget/canvas/cursor.rs b/native/src/widget/canvas/cursor.rs new file mode 100644 index 00000000..ef6a7771 --- /dev/null +++ b/native/src/widget/canvas/cursor.rs @@ -0,0 +1,64 @@ +use crate::{Point, Rectangle}; + +/// The mouse cursor state. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Cursor { + /// The cursor has a defined position. + Available(Point), + + /// The cursor is currently unavailable (i.e. out of bounds or busy). + Unavailable, +} + +impl Cursor { + // TODO: Remove this once this type is used in `iced_native` to encode + // proper cursor availability + pub(crate) fn from_window_position(position: Point) -> Self { + if position.x < 0.0 || position.y < 0.0 { + Cursor::Unavailable + } else { + Cursor::Available(position) + } + } + + /// Returns the absolute position of the [`Cursor`], if available. + pub fn position(&self) -> Option { + match self { + Cursor::Available(position) => Some(*position), + Cursor::Unavailable => None, + } + } + + /// Returns the relative position of the [`Cursor`] inside the given bounds, + /// if available. + /// + /// If the [`Cursor`] is not over the provided bounds, this method will + /// return `None`. + pub fn position_in(&self, bounds: &Rectangle) -> Option { + if self.is_over(bounds) { + self.position_from(bounds.position()) + } else { + None + } + } + + /// Returns the relative position of the [`Cursor`] from the given origin, + /// if available. + pub fn position_from(&self, origin: Point) -> Option { + match self { + Cursor::Available(position) => { + Some(Point::new(position.x - origin.x, position.y - origin.y)) + } + Cursor::Unavailable => None, + } + } + + /// Returns whether the [`Cursor`] is currently over the provided bounds + /// or not. + pub fn is_over(&self, bounds: &Rectangle) -> bool { + match self { + Cursor::Available(position) => bounds.contains(*position), + Cursor::Unavailable => false, + } + } +} diff --git a/native/src/widget/canvas/event.rs b/native/src/widget/canvas/event.rs new file mode 100644 index 00000000..1d726577 --- /dev/null +++ b/native/src/widget/canvas/event.rs @@ -0,0 +1,21 @@ +//! Handle events of a canvas. +use crate::keyboard; +use crate::mouse; +use crate::touch; + +pub use crate::event::Status; + +/// A [`Canvas`] event. +/// +/// [`Canvas`]: crate::widget::Canvas +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Event { + /// A mouse event. + Mouse(mouse::Event), + + /// A touch event. + Touch(touch::Event), + + /// A keyboard event. + Keyboard(keyboard::Event), +} diff --git a/native/src/widget/canvas/fill.rs b/native/src/widget/canvas/fill.rs new file mode 100644 index 00000000..92b1e47e --- /dev/null +++ b/native/src/widget/canvas/fill.rs @@ -0,0 +1,64 @@ +//! Fill [crate::widget::canvas::Geometry] with a certain style. +use crate::widget::canvas::Gradient; +use crate::Color; + +pub use crate::widget::canvas::Style; + +/// The style used to fill geometry. +#[derive(Debug, Clone)] +pub struct Fill { + /// The color or gradient of the fill. + /// + /// By default, it is set to [`Style::Solid`] with [`Color::BLACK`]. + pub style: Style, + + /// The fill rule defines how to determine what is inside and what is + /// outside of a shape. + /// + /// See the [SVG specification][1] for more details. + /// + /// By default, it is set to `NonZero`. + /// + /// [1]: https://www.w3.org/TR/SVG/painting.html#FillRuleProperty + pub rule: Rule, +} + +impl Default for Fill { + fn default() -> Self { + Self { + style: Style::Solid(Color::BLACK), + rule: Rule::NonZero, + } + } +} + +impl From for Fill { + fn from(color: Color) -> Fill { + Fill { + style: Style::Solid(color), + ..Fill::default() + } + } +} + +impl From for Fill { + fn from(gradient: Gradient) -> Self { + Fill { + style: Style::Gradient(gradient), + ..Default::default() + } + } +} + +/// The fill rule defines how to determine what is inside and what is outside of +/// a shape. +/// +/// See the [SVG specification][1]. +/// +/// [1]: https://www.w3.org/TR/SVG/painting.html#FillRuleProperty +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(missing_docs)] +pub enum Rule { + NonZero, + EvenOdd, +} diff --git a/native/src/widget/canvas/path.rs b/native/src/widget/canvas/path.rs new file mode 100644 index 00000000..30c387c5 --- /dev/null +++ b/native/src/widget/canvas/path.rs @@ -0,0 +1,67 @@ +//! Build different kinds of 2D shapes. +pub mod arc; + +mod builder; + +#[doc(no_inline)] +pub use arc::Arc; +pub use builder::Builder; + +pub use lyon_path; + +use crate::{Point, Size}; + +/// An immutable set of points that may or may not be connected. +/// +/// A single [`Path`] can represent different kinds of 2D shapes! +#[derive(Debug, Clone)] +pub struct Path { + raw: lyon_path::Path, +} + +impl Path { + /// Creates a new [`Path`] with the provided closure. + /// + /// Use the [`Builder`] to configure your [`Path`]. + pub fn new(f: impl FnOnce(&mut Builder)) -> Self { + let mut builder = Builder::new(); + + // TODO: Make it pure instead of side-effect-based (?) + f(&mut builder); + + builder.build() + } + + /// Creates a new [`Path`] representing a line segment given its starting + /// and end points. + pub fn line(from: Point, to: Point) -> Self { + Self::new(|p| { + p.move_to(from); + p.line_to(to); + }) + } + + /// Creates a new [`Path`] representing a rectangle given its top-left + /// corner coordinate and its `Size`. + pub fn rectangle(top_left: Point, size: Size) -> Self { + Self::new(|p| p.rectangle(top_left, size)) + } + + /// Creates a new [`Path`] representing a circle given its center + /// coordinate and its radius. + pub fn circle(center: Point, radius: f32) -> Self { + Self::new(|p| p.circle(center, radius)) + } + + #[inline] + pub fn raw(&self) -> &lyon_path::Path { + &self.raw + } + + #[inline] + pub fn transform(&self, transform: &lyon_path::math::Transform) -> Path { + Path { + raw: self.raw.clone().transformed(transform), + } + } +} diff --git a/native/src/widget/canvas/path/arc.rs b/native/src/widget/canvas/path/arc.rs new file mode 100644 index 00000000..e0747d3e --- /dev/null +++ b/native/src/widget/canvas/path/arc.rs @@ -0,0 +1,42 @@ +//! Build and draw curves. +use crate::{Point, Vector}; + +/// A segment of a differentiable curve. +#[derive(Debug, Clone, Copy)] +pub struct Arc { + /// The center of the arc. + pub center: Point, + /// The radius of the arc. + pub radius: f32, + /// The start of the segment's angle, clockwise rotation. + pub start_angle: f32, + /// The end of the segment's angle, clockwise rotation. + pub end_angle: f32, +} + +/// An elliptical [`Arc`]. +#[derive(Debug, Clone, Copy)] +pub struct Elliptical { + /// The center of the arc. + pub center: Point, + /// The radii of the arc's ellipse, defining its axes. + pub radii: Vector, + /// The rotation of the arc's ellipse. + pub rotation: f32, + /// The start of the segment's angle, clockwise rotation. + pub start_angle: f32, + /// The end of the segment's angle, clockwise rotation. + pub end_angle: f32, +} + +impl From for Elliptical { + fn from(arc: Arc) -> Elliptical { + Elliptical { + center: arc.center, + radii: Vector::new(arc.radius, arc.radius), + rotation: 0.0, + start_angle: arc.start_angle, + end_angle: arc.end_angle, + } + } +} diff --git a/native/src/widget/canvas/path/builder.rs b/native/src/widget/canvas/path/builder.rs new file mode 100644 index 00000000..84fda052 --- /dev/null +++ b/native/src/widget/canvas/path/builder.rs @@ -0,0 +1,192 @@ +use crate::widget::canvas::path::{arc, Arc, Path}; +use crate::{Point, Size}; + +use lyon_path::builder::{self, SvgPathBuilder}; +use lyon_path::geom; +use lyon_path::math; + +/// A [`Path`] builder. +/// +/// Once a [`Path`] is built, it can no longer be mutated. +#[allow(missing_debug_implementations)] +pub struct Builder { + raw: builder::WithSvg, +} + +impl Builder { + /// Creates a new [`Builder`]. + pub fn new() -> Builder { + Builder { + raw: lyon_path::Path::builder().with_svg(), + } + } + + /// Moves the starting point of a new sub-path to the given `Point`. + #[inline] + pub fn move_to(&mut self, point: Point) { + let _ = self.raw.move_to(math::Point::new(point.x, point.y)); + } + + /// Connects the last point in the [`Path`] to the given `Point` with a + /// straight line. + #[inline] + pub fn line_to(&mut self, point: Point) { + let _ = self.raw.line_to(math::Point::new(point.x, point.y)); + } + + /// Adds an [`Arc`] to the [`Path`] from `start_angle` to `end_angle` in + /// a clockwise direction. + #[inline] + pub fn arc(&mut self, arc: Arc) { + self.ellipse(arc.into()); + } + + /// Adds a circular arc to the [`Path`] with the given control points and + /// radius. + /// + /// This essentially draws a straight line segment from the current + /// position to `a`, but fits a circular arc of `radius` tangent to that + /// segment and tangent to the line between `a` and `b`. + /// + /// With another `.line_to(b)`, the result will be a path connecting the + /// starting point and `b` with straight line segments towards `a` and a + /// circular arc smoothing out the corner at `a`. + /// + /// See [the HTML5 specification of `arcTo`](https://html.spec.whatwg.org/multipage/canvas.html#building-paths:dom-context-2d-arcto) + /// for more details and examples. + pub fn arc_to(&mut self, a: Point, b: Point, radius: f32) { + let start = self.raw.current_position(); + let mid = math::Point::new(a.x, a.y); + let end = math::Point::new(b.x, b.y); + + if start == mid || mid == end || radius == 0.0 { + let _ = self.raw.line_to(mid); + return; + } + + let double_area = start.x * (mid.y - end.y) + + mid.x * (end.y - start.y) + + end.x * (start.y - mid.y); + + if double_area == 0.0 { + let _ = self.raw.line_to(mid); + return; + } + + let to_start = (start - mid).normalize(); + let to_end = (end - mid).normalize(); + + let inner_angle = to_start.dot(to_end).acos(); + + let origin_angle = inner_angle / 2.0; + + let origin_adjacent = radius / origin_angle.tan(); + + let arc_start = mid + to_start * origin_adjacent; + let arc_end = mid + to_end * origin_adjacent; + + let sweep = to_start.cross(to_end) < 0.0; + + let _ = self.raw.line_to(arc_start); + + self.raw.arc_to( + math::Vector::new(radius, radius), + math::Angle::radians(0.0), + lyon_path::ArcFlags { + large_arc: false, + sweep, + }, + arc_end, + ); + } + + /// Adds an ellipse to the [`Path`] using a clockwise direction. + pub fn ellipse(&mut self, arc: arc::Elliptical) { + let arc = geom::Arc { + center: math::Point::new(arc.center.x, arc.center.y), + radii: math::Vector::new(arc.radii.x, arc.radii.y), + x_rotation: math::Angle::radians(arc.rotation), + start_angle: math::Angle::radians(arc.start_angle), + sweep_angle: math::Angle::radians(arc.end_angle - arc.start_angle), + }; + + let _ = self.raw.move_to(arc.sample(0.0)); + + arc.for_each_quadratic_bezier(&mut |curve| { + let _ = self.raw.quadratic_bezier_to(curve.ctrl, curve.to); + }); + } + + /// Adds a cubic Bézier curve to the [`Path`] given its two control points + /// and its end point. + #[inline] + pub fn bezier_curve_to( + &mut self, + control_a: Point, + control_b: Point, + to: Point, + ) { + let _ = self.raw.cubic_bezier_to( + math::Point::new(control_a.x, control_a.y), + math::Point::new(control_b.x, control_b.y), + math::Point::new(to.x, to.y), + ); + } + + /// Adds a quadratic Bézier curve to the [`Path`] given its control point + /// and its end point. + #[inline] + pub fn quadratic_curve_to(&mut self, control: Point, to: Point) { + let _ = self.raw.quadratic_bezier_to( + math::Point::new(control.x, control.y), + math::Point::new(to.x, to.y), + ); + } + + /// Adds a rectangle to the [`Path`] given its top-left corner coordinate + /// and its `Size`. + #[inline] + pub fn rectangle(&mut self, top_left: Point, size: Size) { + self.move_to(top_left); + self.line_to(Point::new(top_left.x + size.width, top_left.y)); + self.line_to(Point::new( + top_left.x + size.width, + top_left.y + size.height, + )); + self.line_to(Point::new(top_left.x, top_left.y + size.height)); + self.close(); + } + + /// Adds a circle to the [`Path`] given its center coordinate and its + /// radius. + #[inline] + pub fn circle(&mut self, center: Point, radius: f32) { + self.arc(Arc { + center, + radius, + start_angle: 0.0, + end_angle: 2.0 * std::f32::consts::PI, + }); + } + + /// Closes the current sub-path in the [`Path`] with a straight line to + /// the starting point. + #[inline] + pub fn close(&mut self) { + self.raw.close() + } + + /// Builds the [`Path`] of this [`Builder`]. + #[inline] + pub fn build(self) -> Path { + Path { + raw: self.raw.build(), + } + } +} + +impl Default for Builder { + fn default() -> Self { + Self::new() + } +} diff --git a/native/src/widget/canvas/program.rs b/native/src/widget/canvas/program.rs new file mode 100644 index 00000000..17a5a137 --- /dev/null +++ b/native/src/widget/canvas/program.rs @@ -0,0 +1,108 @@ +use crate::widget::canvas::event::{self, Event}; +use crate::widget::canvas::mouse; +use crate::widget::canvas::{Cursor, Renderer}; +use crate::Rectangle; + +/// The state and logic of a [`Canvas`]. +/// +/// A [`Program`] can mutate internal state and produce messages for an +/// application. +/// +/// [`Canvas`]: crate::widget::Canvas +pub trait Program +where + Renderer: self::Renderer, +{ + /// The internal state mutated by the [`Program`]. + type State: Default + 'static; + + /// Updates the [`State`](Self::State) of the [`Program`]. + /// + /// When a [`Program`] is used in a [`Canvas`], the runtime will call this + /// method for each [`Event`]. + /// + /// This method can optionally return a `Message` to notify an application + /// of any meaningful interactions. + /// + /// By default, this method does and returns nothing. + /// + /// [`Canvas`]: crate::widget::Canvas + fn update( + &self, + _state: &mut Self::State, + _event: Event, + _bounds: Rectangle, + _cursor: Cursor, + ) -> (event::Status, Option) { + (event::Status::Ignored, None) + } + + /// Draws the state of the [`Program`], producing a bunch of [`Geometry`]. + /// + /// [`Geometry`] can be easily generated with a [`Frame`] or stored in a + /// [`Cache`]. + /// + /// [`Frame`]: crate::widget::canvas::Frame + /// [`Cache`]: crate::widget::canvas::Cache + fn draw( + &self, + state: &Self::State, + renderer: &Renderer, + theme: &Renderer::Theme, + bounds: Rectangle, + cursor: Cursor, + ) -> Vec; + + /// Returns the current mouse interaction of the [`Program`]. + /// + /// The interaction returned will be in effect even if the cursor position + /// is out of bounds of the program's [`Canvas`]. + /// + /// [`Canvas`]: crate::widget::Canvas + fn mouse_interaction( + &self, + _state: &Self::State, + _bounds: Rectangle, + _cursor: Cursor, + ) -> mouse::Interaction { + mouse::Interaction::default() + } +} + +impl Program for &T +where + Renderer: self::Renderer, + T: Program, +{ + type State = T::State; + + fn update( + &self, + state: &mut Self::State, + event: Event, + bounds: Rectangle, + cursor: Cursor, + ) -> (event::Status, Option) { + T::update(self, state, event, bounds, cursor) + } + + fn draw( + &self, + state: &Self::State, + renderer: &Renderer, + theme: &Renderer::Theme, + bounds: Rectangle, + cursor: Cursor, + ) -> Vec { + T::draw(self, state, renderer, theme, bounds, cursor) + } + + fn mouse_interaction( + &self, + state: &Self::State, + bounds: Rectangle, + cursor: Cursor, + ) -> mouse::Interaction { + T::mouse_interaction(self, state, bounds, cursor) + } +} diff --git a/native/src/widget/canvas/stroke.rs b/native/src/widget/canvas/stroke.rs new file mode 100644 index 00000000..ab4727b2 --- /dev/null +++ b/native/src/widget/canvas/stroke.rs @@ -0,0 +1,106 @@ +//! Create lines from a [crate::widget::canvas::Path] and assigns them various attributes/styles. +pub use crate::widget::canvas::Style; + +use crate::Color; + +/// The style of a stroke. +#[derive(Debug, Clone)] +pub struct Stroke<'a> { + /// The color or gradient of the stroke. + /// + /// By default, it is set to a [`Style::Solid`] with [`Color::BLACK`]. + pub style: Style, + /// The distance between the two edges of the stroke. + pub width: f32, + /// The shape to be used at the end of open subpaths when they are stroked. + pub line_cap: LineCap, + /// The shape to be used at the corners of paths or basic shapes when they + /// are stroked. + pub line_join: LineJoin, + /// The dash pattern used when stroking the line. + pub line_dash: LineDash<'a>, +} + +impl<'a> Stroke<'a> { + /// Sets the color of the [`Stroke`]. + pub fn with_color(self, color: Color) -> Self { + Stroke { + style: Style::Solid(color), + ..self + } + } + + /// Sets the width of the [`Stroke`]. + pub fn with_width(self, width: f32) -> Self { + Stroke { width, ..self } + } + + /// Sets the [`LineCap`] of the [`Stroke`]. + pub fn with_line_cap(self, line_cap: LineCap) -> Self { + Stroke { line_cap, ..self } + } + + /// Sets the [`LineJoin`] of the [`Stroke`]. + pub fn with_line_join(self, line_join: LineJoin) -> Self { + Stroke { line_join, ..self } + } +} + +impl<'a> Default for Stroke<'a> { + fn default() -> Self { + Stroke { + style: Style::Solid(Color::BLACK), + width: 1.0, + line_cap: LineCap::default(), + line_join: LineJoin::default(), + line_dash: LineDash::default(), + } + } +} + +/// The shape used at the end of open subpaths when they are stroked. +#[derive(Debug, Clone, Copy)] +pub enum LineCap { + /// The stroke for each sub-path does not extend beyond its two endpoints. + Butt, + /// At the end of each sub-path, the shape representing the stroke will be + /// extended by a square. + Square, + /// At the end of each sub-path, the shape representing the stroke will be + /// extended by a semicircle. + Round, +} + +impl Default for LineCap { + fn default() -> LineCap { + LineCap::Butt + } +} + +/// The shape used at the corners of paths or basic shapes when they are +/// stroked. +#[derive(Debug, Clone, Copy)] +pub enum LineJoin { + /// A sharp corner. + Miter, + /// A round corner. + Round, + /// A bevelled corner. + Bevel, +} + +impl Default for LineJoin { + fn default() -> LineJoin { + LineJoin::Miter + } +} + +/// The dash pattern used when stroking the line. +#[derive(Debug, Clone, Copy, Default)] +pub struct LineDash<'a> { + /// The alternating lengths of lines and gaps which describe the pattern. + pub segments: &'a [f32], + + /// The offset of [`LineDash::segments`] to start the pattern. + pub offset: usize, +} diff --git a/native/src/widget/canvas/style.rs b/native/src/widget/canvas/style.rs new file mode 100644 index 00000000..2642fdb8 --- /dev/null +++ b/native/src/widget/canvas/style.rs @@ -0,0 +1,24 @@ +use crate::widget::canvas::Gradient; +use crate::Color; + +/// The coloring style of some drawing. +#[derive(Debug, Clone, PartialEq)] +pub enum Style { + /// A solid [`Color`]. + Solid(Color), + + /// A [`Gradient`] color. + Gradient(Gradient), +} + +impl From for Style { + fn from(color: Color) -> Self { + Self::Solid(color) + } +} + +impl From for Style { + fn from(gradient: Gradient) -> Self { + Self::Gradient(gradient) + } +} diff --git a/native/src/widget/canvas/text.rs b/native/src/widget/canvas/text.rs new file mode 100644 index 00000000..8c0b2dfb --- /dev/null +++ b/native/src/widget/canvas/text.rs @@ -0,0 +1,57 @@ +use crate::alignment; +use crate::{Color, Font, Point}; + +/// A bunch of text that can be drawn to a canvas +#[derive(Debug, Clone)] +pub struct Text { + /// The contents of the text + pub content: String, + /// The position of the text relative to the alignment properties. + /// By default, this position will be relative to the top-left corner coordinate meaning that + /// if the horizontal and vertical alignments are unchanged, this property will tell where the + /// top-left corner of the text should be placed. + /// By changing the horizontal_alignment and vertical_alignment properties, you are are able to + /// change what part of text is placed at this positions. + /// For example, when the horizontal_alignment and vertical_alignment are set to Center, the + /// center of the text will be placed at the given position NOT the top-left coordinate. + pub position: Point, + /// The color of the text + pub color: Color, + /// The size of the text + pub size: f32, + /// The font of the text + pub font: Font, + /// The horizontal alignment of the text + pub horizontal_alignment: alignment::Horizontal, + /// The vertical alignment of the text + pub vertical_alignment: alignment::Vertical, +} + +impl Default for Text { + fn default() -> Text { + Text { + content: String::new(), + position: Point::ORIGIN, + color: Color::BLACK, + size: 16.0, + font: Font::SansSerif, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + } + } +} + +impl From for Text { + fn from(content: String) -> Text { + Text { + content, + ..Default::default() + } + } +} + +impl From<&str> for Text { + fn from(content: &str) -> Text { + String::from(content).into() + } +} diff --git a/renderer/Cargo.toml b/renderer/Cargo.toml index 1f21f06b..429b55c2 100644 --- a/renderer/Cargo.toml +++ b/renderer/Cargo.toml @@ -6,12 +6,13 @@ edition = "2021" [features] image = ["iced_wgpu/image", "iced_tiny_skia/image"] svg = ["iced_wgpu/svg", "iced_tiny_skia/svg"] -canvas = ["iced_graphics/canvas"] -qr_code = ["iced_graphics/qr_code"] +canvas = ["iced_wgpu/canvas", "iced_tiny_skia/canvas"] +qr_code = ["canvas", "qrcode"] tracing = ["iced_wgpu/tracing"] [dependencies] raw-window-handle = "0.5" +thiserror = "1" [dependencies.iced_native] version = "0.9" @@ -30,3 +31,8 @@ iced_wgpu = { version = "0.9", path = "../wgpu" } [target.'cfg(target_arch = "wasm32")'.dependencies] iced_wgpu = { version = "0.9", path = "../wgpu", features = ["webgl"] } + +[dependencies.qrcode] +version = "0.12" +optional = true +default-features = false diff --git a/renderer/src/backend.rs b/renderer/src/backend.rs index b0a409dc..6c0b4e5c 100644 --- a/renderer/src/backend.rs +++ b/renderer/src/backend.rs @@ -1,4 +1,4 @@ -use crate::{Font, Point, Size}; +use crate::{Font, Geometry, Point, Size}; use iced_graphics::backend; use iced_graphics::text; @@ -12,6 +12,8 @@ pub enum Backend { } impl iced_graphics::Backend for Backend { + type Geometry = Geometry; + fn trim_measurements(&mut self) { match self { Self::Wgpu(backend) => backend.trim_measurements(), diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index f9bfc373..d9c85e82 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -4,11 +4,14 @@ pub mod window; mod backend; mod settings; +pub use iced_graphics::primitive; + pub use backend::Backend; +pub use primitive::Primitive; pub use settings::Settings; pub use iced_graphics::{ - Antialiasing, Color, Error, Font, Point, Size, Viewport, + Antialiasing, Color, Error, Font, Point, Rectangle, Size, Vector, Viewport, }; /// The default graphics renderer for [`iced`]. @@ -16,3 +19,12 @@ pub use iced_graphics::{ /// [`iced`]: https://github.com/iced-rs/iced pub type Renderer = iced_graphics::Renderer; + +#[derive(Debug, Clone)] +pub struct Geometry(pub(crate) Primitive); + +impl From for Primitive { + fn from(geometry: Geometry) -> Self { + geometry.0 + } +} diff --git a/renderer/src/widget.rs b/renderer/src/widget.rs index 417cc06f..6c0c2a83 100644 --- a/renderer/src/widget.rs +++ b/renderer/src/widget.rs @@ -1,5 +1,11 @@ -#[cfg(feature = "qr_code")] -pub use iced_graphics::widget::qr_code; +#[cfg(feature = "canvas")] +pub mod canvas; #[cfg(feature = "canvas")] -pub use iced_graphics::widget::canvas; +pub use canvas::Canvas; + +#[cfg(feature = "qr_code")] +pub mod qr_code; + +#[cfg(feature = "qr_code")] +pub use qr_code::QRCode; diff --git a/renderer/src/widget/canvas.rs b/renderer/src/widget/canvas.rs new file mode 100644 index 00000000..f40a1097 --- /dev/null +++ b/renderer/src/widget/canvas.rs @@ -0,0 +1,177 @@ +mod cache; + +pub use cache::Cache; + +pub use iced_native::widget::canvas::event::{self, Event}; +pub use iced_native::widget::canvas::fill::{self, Fill}; +pub use iced_native::widget::canvas::gradient::{self, Gradient}; +pub use iced_native::widget::canvas::path::{self, Path}; +pub use iced_native::widget::canvas::stroke::{self, Stroke}; +pub use iced_native::widget::canvas::{ + Canvas, Cursor, LineCap, LineDash, LineJoin, Program, Renderer, Style, Text, +}; + +use crate::{Backend, Point, Rectangle, Size, Vector}; + +pub use crate::Geometry; + +pub enum Frame { + Wgpu(iced_wgpu::widget::canvas::Frame), + TinySkia(iced_tiny_skia::canvas::Frame), +} + +macro_rules! delegate { + ($frame:expr, $name:ident => $body:expr) => { + match $frame { + Self::Wgpu($name) => $body, + Self::TinySkia($name) => $body, + } + }; +} + +impl Frame { + pub fn new(renderer: &crate::Renderer, size: Size) -> Self { + match renderer.backend() { + Backend::Wgpu(_) => { + Frame::Wgpu(iced_wgpu::widget::canvas::Frame::new(size)) + } + Backend::TinySkia(_) => { + Frame::TinySkia(iced_tiny_skia::canvas::Frame::new(size)) + } + } + } + + /// Returns the width of the [`Frame`]. + #[inline] + pub fn width(&self) -> f32 { + delegate!(self, frame => frame.width()) + } + + /// Returns the height of the [`Frame`]. + #[inline] + pub fn height(&self) -> f32 { + delegate!(self, frame => frame.height()) + } + + /// Returns the dimensions of the [`Frame`]. + #[inline] + pub fn size(&self) -> Size { + delegate!(self, frame => frame.size()) + } + + /// Returns the coordinate of the center of the [`Frame`]. + #[inline] + pub fn center(&self) -> Point { + delegate!(self, frame => frame.center()) + } + + /// Draws the given [`Path`] on the [`Frame`] by filling it with the + /// provided style. + pub fn fill(&mut self, path: &Path, fill: impl Into) { + delegate!(self, frame => frame.fill(path, fill)); + } + + /// Draws an axis-aligned rectangle given its top-left corner coordinate and + /// its `Size` on the [`Frame`] by filling it with the provided style. + pub fn fill_rectangle( + &mut self, + top_left: Point, + size: Size, + fill: impl Into, + ) { + delegate!(self, frame => frame.fill_rectangle(top_left, size, fill)); + } + + /// Draws the stroke of the given [`Path`] on the [`Frame`] with the + /// provided style. + pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { + delegate!(self, frame => frame.stroke(path, stroke)); + } + + /// Draws the characters of the given [`Text`] on the [`Frame`], filling + /// them with the given color. + /// + /// __Warning:__ Text currently does not work well with rotations and scale + /// transforms! The position will be correctly transformed, but the + /// resulting glyphs will not be rotated or scaled properly. + /// + /// Additionally, all text will be rendered on top of all the layers of + /// a [`Canvas`]. Therefore, it is currently only meant to be used for + /// overlays, which is the most common use case. + /// + /// Support for vectorial text is planned, and should address all these + /// limitations. + /// + /// [`Canvas`]: crate::widget::Canvas + pub fn fill_text(&mut self, text: impl Into) { + delegate!(self, frame => frame.fill_text(text)); + } + + /// Stores the current transform of the [`Frame`] and executes the given + /// drawing operations, restoring the transform afterwards. + /// + /// This method is useful to compose transforms and perform drawing + /// operations in different coordinate systems. + #[inline] + pub fn with_save(&mut self, f: impl FnOnce(&mut Frame)) { + delegate!(self, frame => frame.push_transform()); + + f(self); + + delegate!(self, frame => frame.pop_transform()); + } + + /// Executes the given drawing operations within a [`Rectangle`] region, + /// clipping any geometry that overflows its bounds. Any transformations + /// performed are local to the provided closure. + /// + /// This method is useful to perform drawing operations that need to be + /// clipped. + #[inline] + pub fn with_clip(&mut self, region: Rectangle, f: impl FnOnce(&mut Frame)) { + let mut frame = match self { + Self::Wgpu(_) => { + Self::Wgpu(iced_wgpu::widget::canvas::Frame::new(region.size())) + } + Self::TinySkia(_) => Self::TinySkia( + iced_tiny_skia::canvas::Frame::new(region.size()), + ), + }; + + f(&mut frame); + + let translation = Vector::new(region.x, region.y); + + match (self, frame) { + (Self::Wgpu(target), Self::Wgpu(frame)) => { + target.clip(frame, translation); + } + (Self::TinySkia(target), Self::TinySkia(frame)) => { + target.clip(frame, translation); + } + _ => unreachable!(), + }; + } + + /// Applies a translation to the current transform of the [`Frame`]. + #[inline] + pub fn translate(&mut self, translation: Vector) { + delegate!(self, frame => frame.translate(translation)); + } + + /// Applies a rotation in radians to the current transform of the [`Frame`]. + #[inline] + pub fn rotate(&mut self, angle: f32) { + delegate!(self, frame => frame.rotate(angle)); + } + + /// Applies a scaling to the current transform of the [`Frame`]. + #[inline] + pub fn scale(&mut self, scale: f32) { + delegate!(self, frame => frame.scale(scale)); + } + + pub fn into_geometry(self) -> Geometry { + Geometry(delegate!(self, frame => frame.into_primitive())) + } +} diff --git a/renderer/src/widget/canvas/cache.rs b/renderer/src/widget/canvas/cache.rs new file mode 100644 index 00000000..7d6b4811 --- /dev/null +++ b/renderer/src/widget/canvas/cache.rs @@ -0,0 +1,85 @@ +use crate::widget::canvas::{Frame, Geometry}; +use crate::{Primitive, Renderer, Size}; + +use std::cell::RefCell; +use std::sync::Arc; + +/// A simple cache that stores generated [`Geometry`] to avoid recomputation. +/// +/// A [`Cache`] will not redraw its geometry unless the dimensions of its layer +/// change or it is explicitly cleared. +#[derive(Debug, Default)] +pub struct Cache { + state: RefCell, +} + +#[derive(Debug, Default)] +enum State { + #[default] + Empty, + Filled { + bounds: Size, + primitive: Arc, + }, +} + +impl Cache { + /// Creates a new empty [`Cache`]. + pub fn new() -> Self { + Cache { + state: Default::default(), + } + } + + /// Clears the [`Cache`], forcing a redraw the next time it is used. + pub fn clear(&self) { + *self.state.borrow_mut() = State::Empty; + } + + /// Draws [`Geometry`] using the provided closure and stores it in the + /// [`Cache`]. + /// + /// The closure will only be called when + /// - the bounds have changed since the previous draw call. + /// - the [`Cache`] is empty or has been explicitly cleared. + /// + /// Otherwise, the previously stored [`Geometry`] will be returned. The + /// [`Cache`] is not cleared in this case. In other words, it will keep + /// returning the stored [`Geometry`] if needed. + pub fn draw( + &self, + renderer: &Renderer, + bounds: Size, + draw_fn: impl FnOnce(&mut Frame), + ) -> Geometry { + use std::ops::Deref; + + if let State::Filled { + bounds: cached_bounds, + primitive, + } = self.state.borrow().deref() + { + if *cached_bounds == bounds { + return Geometry(Primitive::Cache { + content: primitive.clone(), + }); + } + } + + let mut frame = Frame::new(renderer, bounds); + draw_fn(&mut frame); + + let primitive = { + let geometry = frame.into_geometry(); + + Arc::new(geometry.0) + }; + + *self.state.borrow_mut() = State::Filled { + bounds, + primitive: primitive.clone(), + }; + + Geometry(Primitive::Cache { content: primitive }) + } +} diff --git a/renderer/src/widget/qr_code.rs b/renderer/src/widget/qr_code.rs new file mode 100644 index 00000000..aae4ec88 --- /dev/null +++ b/renderer/src/widget/qr_code.rs @@ -0,0 +1,301 @@ +//! Encode and display information in a QR code. +use crate::widget::canvas; +use crate::Renderer; + +use iced_graphics::renderer; + +use iced_native::layout; +use iced_native::widget::Tree; +use iced_native::{ + Color, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget, +}; +use thiserror::Error; + +const DEFAULT_CELL_SIZE: u16 = 4; +const QUIET_ZONE: usize = 2; + +/// A type of matrix barcode consisting of squares arranged in a grid which +/// can be read by an imaging device, such as a camera. +#[derive(Debug)] +pub struct QRCode<'a> { + state: &'a State, + dark: Color, + light: Color, + cell_size: u16, +} + +impl<'a> QRCode<'a> { + /// Creates a new [`QRCode`] with the provided [`State`]. + pub fn new(state: &'a State) -> Self { + Self { + cell_size: DEFAULT_CELL_SIZE, + dark: Color::BLACK, + light: Color::WHITE, + state, + } + } + + /// Sets both the dark and light [`Color`]s of the [`QRCode`]. + pub fn color(mut self, dark: Color, light: Color) -> Self { + self.dark = dark; + self.light = light; + self + } + + /// Sets the size of the squares of the grid cell of the [`QRCode`]. + pub fn cell_size(mut self, cell_size: u16) -> Self { + self.cell_size = cell_size; + self + } +} + +impl<'a, Message, Theme> Widget> for QRCode<'a> { + fn width(&self) -> Length { + Length::Shrink + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + _renderer: &Renderer, + _limits: &layout::Limits, + ) -> layout::Node { + let side_length = (self.state.width + 2 * QUIET_ZONE) as f32 + * f32::from(self.cell_size); + + layout::Node::new(Size::new(side_length, side_length)) + } + + fn draw( + &self, + _state: &Tree, + renderer: &mut Renderer, + _theme: &Theme, + _style: &renderer::Style, + layout: Layout<'_>, + _cursor_position: Point, + _viewport: &Rectangle, + ) { + use iced_native::Renderer as _; + + let bounds = layout.bounds(); + let side_length = self.state.width + 2 * QUIET_ZONE; + + // Reuse cache if possible + let geometry = + self.state.cache.draw(renderer, bounds.size(), |frame| { + // Scale units to cell size + frame.scale(f32::from(self.cell_size)); + + // Draw background + frame.fill_rectangle( + Point::ORIGIN, + Size::new(side_length as f32, side_length as f32), + self.light, + ); + + // Avoid drawing on the quiet zone + frame.translate(Vector::new( + QUIET_ZONE as f32, + QUIET_ZONE as f32, + )); + + // Draw contents + self.state + .contents + .iter() + .enumerate() + .filter(|(_, value)| **value == qrcode::Color::Dark) + .for_each(|(index, _)| { + let row = index / self.state.width; + let column = index % self.state.width; + + frame.fill_rectangle( + Point::new(column as f32, row as f32), + Size::UNIT, + self.dark, + ); + }); + }); + + let translation = Vector::new(bounds.x, bounds.y); + + renderer.with_translation(translation, |renderer| { + renderer.draw_primitive(geometry.0); + }); + } +} + +impl<'a, Message, Theme> From> + for Element<'a, Message, Renderer> +{ + fn from(qr_code: QRCode<'a>) -> Self { + Self::new(qr_code) + } +} + +/// The state of a [`QRCode`]. +/// +/// It stores the data that will be displayed. +#[derive(Debug)] +pub struct State { + contents: Vec, + width: usize, + cache: canvas::Cache, +} + +impl State { + /// Creates a new [`State`] with the provided data. + /// + /// This method uses an [`ErrorCorrection::Medium`] and chooses the smallest + /// size to display the data. + pub fn new(data: impl AsRef<[u8]>) -> Result { + let encoded = qrcode::QrCode::new(data)?; + + Ok(Self::build(encoded)) + } + + /// Creates a new [`State`] with the provided [`ErrorCorrection`]. + pub fn with_error_correction( + data: impl AsRef<[u8]>, + error_correction: ErrorCorrection, + ) -> Result { + let encoded = qrcode::QrCode::with_error_correction_level( + data, + error_correction.into(), + )?; + + Ok(Self::build(encoded)) + } + + /// Creates a new [`State`] with the provided [`Version`] and + /// [`ErrorCorrection`]. + pub fn with_version( + data: impl AsRef<[u8]>, + version: Version, + error_correction: ErrorCorrection, + ) -> Result { + let encoded = qrcode::QrCode::with_version( + data, + version.into(), + error_correction.into(), + )?; + + Ok(Self::build(encoded)) + } + + fn build(encoded: qrcode::QrCode) -> Self { + let width = encoded.width(); + let contents = encoded.into_colors(); + + Self { + contents, + width, + cache: canvas::Cache::new(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// The size of a [`QRCode`]. +/// +/// The higher the version the larger the grid of cells, and therefore the more +/// information the [`QRCode`] can carry. +pub enum Version { + /// A normal QR code version. It should be between 1 and 40. + Normal(u8), + + /// A micro QR code version. It should be between 1 and 4. + Micro(u8), +} + +impl From for qrcode::Version { + fn from(version: Version) -> Self { + match version { + Version::Normal(v) => qrcode::Version::Normal(i16::from(v)), + Version::Micro(v) => qrcode::Version::Micro(i16::from(v)), + } + } +} + +/// The error correction level. +/// +/// It controls the amount of data that can be damaged while still being able +/// to recover the original information. +/// +/// A higher error correction level allows for more corrupted data. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ErrorCorrection { + /// Low error correction. 7% of the data can be restored. + Low, + /// Medium error correction. 15% of the data can be restored. + Medium, + /// Quartile error correction. 25% of the data can be restored. + Quartile, + /// High error correction. 30% of the data can be restored. + High, +} + +impl From for qrcode::EcLevel { + fn from(ec_level: ErrorCorrection) -> Self { + match ec_level { + ErrorCorrection::Low => qrcode::EcLevel::L, + ErrorCorrection::Medium => qrcode::EcLevel::M, + ErrorCorrection::Quartile => qrcode::EcLevel::Q, + ErrorCorrection::High => qrcode::EcLevel::H, + } + } +} + +/// An error that occurred when building a [`State`] for a [`QRCode`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] +pub enum Error { + /// The data is too long to encode in a QR code for the chosen [`Version`]. + #[error( + "The data is too long to encode in a QR code for the chosen version" + )] + DataTooLong, + + /// The chosen [`Version`] and [`ErrorCorrection`] combination is invalid. + #[error( + "The chosen version and error correction level combination is invalid." + )] + InvalidVersion, + + /// One or more characters in the provided data are not supported by the + /// chosen [`Version`]. + #[error( + "One or more characters in the provided data are not supported by the \ + chosen version" + )] + UnsupportedCharacterSet, + + /// The chosen ECI designator is invalid. A valid designator should be + /// between 0 and 999999. + #[error( + "The chosen ECI designator is invalid. A valid designator should be \ + between 0 and 999999." + )] + InvalidEciDesignator, + + /// A character that does not belong to the character set was found. + #[error("A character that does not belong to the character set was found")] + InvalidCharacter, +} + +impl From for Error { + fn from(error: qrcode::types::QrError) -> Self { + use qrcode::types::QrError; + + match error { + QrError::DataTooLong => Error::DataTooLong, + QrError::InvalidVersion => Error::InvalidVersion, + QrError::UnsupportedCharacterSet => Error::UnsupportedCharacterSet, + QrError::InvalidEciDesignator => Error::InvalidEciDesignator, + QrError::InvalidCharacter => Error::InvalidCharacter, + } + } +} diff --git a/src/widget.rs b/src/widget.rs index 04cb0e50..f3a66101 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -170,9 +170,10 @@ pub use iced_renderer::widget::canvas; #[cfg(feature = "canvas")] #[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] /// Creates a new [`Canvas`]. -pub fn canvas(program: P) -> Canvas +pub fn canvas(program: P) -> Canvas where - P: canvas::Program, + Renderer: canvas::Renderer, + P: canvas::Program, { Canvas::new(program) } diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml index 781e7d34..5f39fce2 100644 --- a/tiny_skia/Cargo.toml +++ b/tiny_skia/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [features] image = [] svg = [] +canvas = ["iced_native/canvas"] [dependencies] raw-window-handle = "0.5" diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 66d83221..e08cede7 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -1,9 +1,9 @@ -use crate::{Color, Font, Settings, Size, Viewport}; +use crate::{Color, Font, Primitive, Settings, Size, Viewport}; use iced_graphics::alignment; use iced_graphics::backend; use iced_graphics::text; -use iced_graphics::{Background, Primitive, Rectangle, Vector}; +use iced_graphics::{Background, Rectangle, Vector}; use std::borrow::Cow; @@ -81,7 +81,6 @@ impl Backend { translation: Vector, ) { match primitive { - Primitive::None => {} Primitive::Quad { bounds, background, @@ -161,6 +160,38 @@ impl Backend { Primitive::Svg { .. } => { // TODO } + Primitive::Fill { + path, + paint, + rule, + transform, + } => { + pixels.fill_path( + path, + paint, + *rule, + transform + .post_translate(translation.x, translation.y) + .post_scale(scale_factor, scale_factor), + clip_mask, + ); + } + Primitive::Stroke { + path, + paint, + stroke, + transform, + } => { + pixels.stroke_path( + path, + paint, + stroke, + transform + .post_translate(translation.x, translation.y) + .post_scale(scale_factor, scale_factor), + clip_mask, + ); + } Primitive::Group { primitives } => { for primitive in primitives { self.draw_primitive( @@ -196,16 +227,19 @@ impl Backend { translation, ); } - Primitive::Cached { cache } => { + Primitive::Cache { content } => { self.draw_primitive( - cache, + content, pixels, clip_mask, scale_factor, translation, ); } - Primitive::SolidMesh { .. } | Primitive::GradientMesh { .. } => {} + Primitive::SolidMesh { .. } | Primitive::GradientMesh { .. } => { + // Not supported! + // TODO: Draw a placeholder (?) / Log it (?) + } } } } @@ -386,6 +420,8 @@ fn rectangular_clip_mask( } impl iced_graphics::Backend for Backend { + type Geometry = (); + fn trim_measurements(&mut self) { self.text_pipeline.trim_measurement_cache(); } diff --git a/tiny_skia/src/canvas.rs b/tiny_skia/src/canvas.rs new file mode 100644 index 00000000..c3b8b316 --- /dev/null +++ b/tiny_skia/src/canvas.rs @@ -0,0 +1,276 @@ +use crate::{Point, Primitive, Rectangle, Size, Vector}; + +use iced_native::widget::canvas::fill::{self, Fill}; +use iced_native::widget::canvas::stroke::{self, Stroke}; +use iced_native::widget::canvas::{Path, Style, Text}; +use iced_native::Gradient; + +pub struct Frame { + size: Size, + transform: tiny_skia::Transform, + stack: Vec, + primitives: Vec, +} + +impl Frame { + pub fn new(size: Size) -> Self { + Self { + size, + transform: tiny_skia::Transform::identity(), + stack: Vec::new(), + primitives: Vec::new(), + } + } + + pub fn width(&self) -> f32 { + self.size.width + } + + pub fn height(&self) -> f32 { + self.size.height + } + + pub fn size(&self) -> Size { + self.size + } + + pub fn center(&self) -> Point { + Point::new(self.size.width / 2.0, self.size.height / 2.0) + } + + pub fn fill(&mut self, path: &Path, fill: impl Into) { + let path = convert_path(path); + let fill = fill.into(); + + self.primitives.push(Primitive::Fill { + path, + paint: into_paint(fill.style), + rule: into_fill_rule(fill.rule), + transform: self.transform, + }); + } + + pub fn fill_rectangle( + &mut self, + top_left: Point, + size: Size, + fill: impl Into, + ) { + self.fill(&Path::rectangle(top_left, size), fill); + } + + pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { + let path = convert_path(path); + let stroke = stroke.into(); + let skia_stroke = into_stroke(&stroke); + + self.primitives.push(Primitive::Stroke { + path, + paint: into_paint(stroke.style), + stroke: skia_stroke, + transform: self.transform, + }); + } + + pub fn fill_text(&mut self, text: impl Into) { + let text = text.into(); + + let position = if self.transform.is_identity() { + text.position + } else { + let mut transformed = [tiny_skia::Point { + x: text.position.x, + y: text.position.y, + }]; + + self.transform.map_points(&mut transformed); + + Point::new(transformed[0].x, transformed[0].y) + }; + + // TODO: Use vectorial text instead of primitive + self.primitives.push(Primitive::Text { + content: text.content, + bounds: Rectangle { + x: position.x, + y: position.y, + width: f32::INFINITY, + height: f32::INFINITY, + }, + color: text.color, + size: text.size, + font: text.font, + horizontal_alignment: text.horizontal_alignment, + vertical_alignment: text.vertical_alignment, + }); + } + + pub fn push_transform(&mut self) { + self.stack.push(self.transform); + } + + pub fn pop_transform(&mut self) { + self.transform = self.stack.pop().expect("Pop transform"); + } + + pub fn clip(&mut self, _frame: Self, _translation: Vector) {} + + pub fn translate(&mut self, translation: Vector) { + self.transform = + self.transform.pre_translate(translation.x, translation.y); + } + + pub fn rotate(&mut self, angle: f32) { + self.transform = self + .transform + .pre_concat(tiny_skia::Transform::from_rotate(angle.to_degrees())); + } + + pub fn scale(&mut self, scale: f32) { + self.transform = self.transform.pre_scale(scale, scale); + } + + pub fn into_primitive(self) -> Primitive { + Primitive::Clip { + bounds: Rectangle::new(Point::ORIGIN, self.size), + content: Box::new(Primitive::Group { + primitives: self.primitives, + }), + } + } +} + +fn convert_path(path: &Path) -> tiny_skia::Path { + use iced_native::widget::canvas::path::lyon_path; + + let mut builder = tiny_skia::PathBuilder::new(); + let mut last_point = Default::default(); + + for event in path.raw().iter() { + match event { + lyon_path::Event::Begin { at } => { + builder.move_to(at.x, at.y); + + last_point = at; + } + lyon_path::Event::Line { from, to } => { + if last_point != from { + builder.move_to(from.x, from.y); + } + + builder.line_to(to.x, to.y); + + last_point = to; + } + lyon_path::Event::Quadratic { from, ctrl, to } => { + if last_point != from { + builder.move_to(from.x, from.y); + } + + builder.quad_to(ctrl.x, ctrl.y, to.x, to.y); + + last_point = to; + } + lyon_path::Event::Cubic { + from, + ctrl1, + ctrl2, + to, + } => { + if last_point != from { + builder.move_to(from.x, from.y); + } + + builder + .cubic_to(ctrl1.x, ctrl1.y, ctrl2.x, ctrl2.y, to.x, to.y); + + last_point = to; + } + lyon_path::Event::End { close, .. } => { + if close { + builder.close(); + } + } + } + } + + builder + .finish() + .expect("Convert lyon path to tiny_skia path") +} + +pub fn into_paint(style: Style) -> tiny_skia::Paint<'static> { + tiny_skia::Paint { + shader: match style { + Style::Solid(color) => tiny_skia::Shader::SolidColor( + tiny_skia::Color::from_rgba(color.b, color.g, color.r, color.a) + .expect("Create color"), + ), + Style::Gradient(gradient) => match gradient { + Gradient::Linear(linear) => tiny_skia::LinearGradient::new( + tiny_skia::Point { + x: linear.start.x, + y: linear.start.y, + }, + tiny_skia::Point { + x: linear.end.x, + y: linear.end.y, + }, + linear + .color_stops + .into_iter() + .map(|stop| { + tiny_skia::GradientStop::new( + stop.offset, + tiny_skia::Color::from_rgba( + stop.color.b, + stop.color.g, + stop.color.r, + stop.color.a, + ) + .expect("Create color"), + ) + }) + .collect(), + tiny_skia::SpreadMode::Pad, + tiny_skia::Transform::identity(), + ) + .expect("Create linear gradient"), + }, + }, + anti_alias: true, + ..Default::default() + } +} + +pub fn into_fill_rule(rule: fill::Rule) -> tiny_skia::FillRule { + match rule { + fill::Rule::EvenOdd => tiny_skia::FillRule::EvenOdd, + fill::Rule::NonZero => tiny_skia::FillRule::Winding, + } +} + +pub fn into_stroke(stroke: &Stroke) -> tiny_skia::Stroke { + tiny_skia::Stroke { + width: stroke.width, + line_cap: match stroke.line_cap { + stroke::LineCap::Butt => tiny_skia::LineCap::Butt, + stroke::LineCap::Square => tiny_skia::LineCap::Square, + stroke::LineCap::Round => tiny_skia::LineCap::Round, + }, + line_join: match stroke.line_join { + stroke::LineJoin::Miter => tiny_skia::LineJoin::Miter, + stroke::LineJoin::Round => tiny_skia::LineJoin::Round, + stroke::LineJoin::Bevel => tiny_skia::LineJoin::Bevel, + }, + dash: if stroke.line_dash.segments.is_empty() { + None + } else { + tiny_skia::StrokeDash::new( + stroke.line_dash.segments.into(), + stroke.line_dash.offset as f32, + ) + }, + ..Default::default() + } +} diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index 420a1ffb..e66e6412 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -4,10 +4,18 @@ mod backend; mod settings; mod text; +#[cfg(feature = "canvas")] +pub mod canvas; + +pub use iced_graphics::primitive; + pub use backend::Backend; +pub use primitive::Primitive; pub use settings::Settings; -pub use iced_graphics::{Color, Error, Font, Point, Size, Vector, Viewport}; +pub use iced_graphics::{ + Color, Error, Font, Point, Rectangle, Size, Vector, Viewport, +}; /// A [`tiny-skia`] graphics renderer for [`iced`]. /// diff --git a/tiny_skia/src/primitive.rs b/tiny_skia/src/primitive.rs new file mode 100644 index 00000000..22daaedc --- /dev/null +++ b/tiny_skia/src/primitive.rs @@ -0,0 +1,82 @@ +use crate::{Rectangle, Vector}; + +use std::sync::Arc; + +#[derive(Debug, Clone)] +pub enum Primitive { + /// A group of primitives + Group { + /// The primitives of the group + primitives: Vec, + }, + /// A clip primitive + Clip { + /// The bounds of the clip + bounds: Rectangle, + /// The content of the clip + content: Box, + }, + /// A primitive that applies a translation + Translate { + /// The translation vector + translation: Vector, + + /// The primitive to translate + content: Box, + }, + /// A cached primitive. + /// + /// This can be useful if you are implementing a widget where primitive + /// generation is expensive. + Cached { + /// The cached primitive + cache: Arc, + }, + /// A basic primitive. + Basic(iced_graphics::Primitive), +} + +impl iced_graphics::backend::Primitive for Primitive { + fn translate(self, translation: Vector) -> Self { + Self::Translate { + translation, + content: Box::new(self), + } + } + + fn clip(self, bounds: Rectangle) -> Self { + Self::Clip { + bounds, + content: Box::new(self), + } + } +} + +#[derive(Debug, Clone, Default)] +pub struct Recording(pub(crate) Vec); + +impl iced_graphics::backend::Recording for Recording { + type Primitive = Primitive; + + fn push(&mut self, primitive: Primitive) { + self.0.push(primitive); + } + + fn push_basic(&mut self, basic: iced_graphics::Primitive) { + self.0.push(Primitive::Basic(basic)); + } + + fn group(self) -> Self::Primitive { + Primitive::Group { primitives: self.0 } + } + + fn clear(&mut self) { + self.0.clear(); + } +} + +impl Recording { + pub fn primitives(&self) -> &[Primitive] { + &self.0 + } +} diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 2bd5831e..08159cd8 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -1,7 +1,6 @@ -use crate::{Backend, Color, Error, Renderer, Settings, Viewport}; +use crate::{Backend, Color, Error, Primitive, Renderer, Settings, Viewport}; use iced_graphics::window::compositor::{self, Information, SurfaceError}; -use iced_graphics::Primitive; use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; use std::marker::PhantomData; diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 632873a3..0bcef71c 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -21,8 +21,7 @@ bmp = ["iced_graphics/bmp"] hdr = ["iced_graphics/hdr"] dds = ["iced_graphics/dds"] farbfeld = ["iced_graphics/farbfeld"] -canvas = ["iced_graphics/canvas"] -qr_code = ["iced_graphics/qr_code"] +canvas = ["iced_graphics/canvas", "lyon"] spirv = ["wgpu/spirv"] webgl = ["wgpu/webgl"] @@ -62,10 +61,6 @@ version = "0.2" git = "https://github.com/hecrj/glyphon.git" rev = "65b481d758f50fd13fc21af2cc5ef62ddee64955" -[dependencies.tracing] -version = "0.1.6" -optional = true - [dependencies.encase] version = "0.3.0" features = ["glam"] @@ -73,6 +68,14 @@ features = ["glam"] [dependencies.glam] version = "0.21.3" +[dependencies.lyon] +version = "1.0" +optional = true + +[dependencies.tracing] +version = "0.1.6" +optional = true + [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] all-features = true diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index e650d9a5..10dc5b4f 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -1,11 +1,10 @@ use crate::quad; use crate::text; use crate::triangle; -use crate::{Settings, Transformation}; +use crate::{Layer, Primitive, Settings, Transformation}; use iced_graphics::backend; -use iced_graphics::layer::Layer; -use iced_graphics::{Color, Font, Primitive, Size, Viewport}; +use iced_graphics::{Color, Font, Size, Viewport}; #[cfg(feature = "tracing")] use tracing::info_span; @@ -330,6 +329,8 @@ impl Backend { } impl iced_graphics::Backend for Backend { + type Geometry = (); + fn trim_measurements(&mut self) { self.text_pipeline.trim_measurement_cache() } diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs index db05d2ff..2159a3ec 100644 --- a/wgpu/src/image.rs +++ b/wgpu/src/image.rs @@ -6,10 +6,10 @@ use iced_graphics::image::raster; #[cfg(feature = "svg")] use iced_graphics::image::vector; +use crate::layer; use crate::{Buffer, Transformation}; use atlas::Atlas; -use iced_graphics::layer; use iced_native::{Rectangle, Size}; use std::cell::RefCell; diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs new file mode 100644 index 00000000..0840555a --- /dev/null +++ b/wgpu/src/layer.rs @@ -0,0 +1,274 @@ +//! Organize rendering primitives into a flattened list of layers. +mod image; +mod quad; +mod text; + +pub mod mesh; + +pub use image::Image; +pub use mesh::Mesh; +pub use quad::Quad; +pub use text::Text; + +use crate::Primitive; + +use iced_graphics::alignment; +use iced_graphics::{ + Background, Color, Font, Point, Rectangle, Size, Vector, Viewport, +}; + +/// A group of primitives that should be clipped together. +#[derive(Debug)] +pub struct Layer<'a> { + /// The clipping bounds of the [`Layer`]. + pub bounds: Rectangle, + + /// The quads of the [`Layer`]. + pub quads: Vec, + + /// The triangle meshes of the [`Layer`]. + pub meshes: Vec>, + + /// The text of the [`Layer`]. + pub text: Vec>, + + /// The images of the [`Layer`]. + pub images: Vec, +} + +impl<'a> Layer<'a> { + /// Creates a new [`Layer`] with the given clipping bounds. + pub fn new(bounds: Rectangle) -> Self { + Self { + bounds, + quads: Vec::new(), + meshes: Vec::new(), + text: Vec::new(), + images: Vec::new(), + } + } + + /// Creates a new [`Layer`] for the provided overlay text. + /// + /// This can be useful for displaying debug information. + pub fn overlay(lines: &'a [impl AsRef], viewport: &Viewport) -> Self { + let mut overlay = + Layer::new(Rectangle::with_size(viewport.logical_size())); + + for (i, line) in lines.iter().enumerate() { + let text = Text { + content: line.as_ref(), + bounds: Rectangle::new( + Point::new(11.0, 11.0 + 25.0 * i as f32), + Size::INFINITY, + ), + color: Color::new(0.9, 0.9, 0.9, 1.0), + size: 20.0, + font: Font::Monospace, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + }; + + overlay.text.push(text); + + overlay.text.push(Text { + bounds: text.bounds + Vector::new(-1.0, -1.0), + color: Color::BLACK, + ..text + }); + } + + overlay + } + + /// Distributes the given [`Primitive`] and generates a list of layers based + /// on its contents. + pub fn generate( + primitives: &'a [Primitive], + viewport: &Viewport, + ) -> Vec { + let first_layer = + Layer::new(Rectangle::with_size(viewport.logical_size())); + + let mut layers = vec![first_layer]; + + for primitive in primitives { + Self::process_primitive( + &mut layers, + Vector::new(0.0, 0.0), + primitive, + 0, + ); + } + + layers + } + + fn process_primitive( + layers: &mut Vec, + translation: Vector, + primitive: &'a Primitive, + current_layer: usize, + ) { + match primitive { + Primitive::Text { + content, + bounds, + size, + color, + font, + horizontal_alignment, + vertical_alignment, + } => { + let layer = &mut layers[current_layer]; + + layer.text.push(Text { + content, + bounds: *bounds + translation, + size: *size, + color: *color, + font: *font, + horizontal_alignment: *horizontal_alignment, + vertical_alignment: *vertical_alignment, + }); + } + Primitive::Quad { + bounds, + background, + border_radius, + border_width, + border_color, + } => { + let layer = &mut layers[current_layer]; + + // TODO: Move some of these computations to the GPU (?) + layer.quads.push(Quad { + position: [ + bounds.x + translation.x, + bounds.y + translation.y, + ], + size: [bounds.width, bounds.height], + color: match background { + Background::Color(color) => color.into_linear(), + }, + border_radius: *border_radius, + border_width: *border_width, + border_color: border_color.into_linear(), + }); + } + Primitive::Image { handle, bounds } => { + let layer = &mut layers[current_layer]; + + layer.images.push(Image::Raster { + handle: handle.clone(), + bounds: *bounds + translation, + }); + } + Primitive::Svg { + handle, + color, + bounds, + } => { + let layer = &mut layers[current_layer]; + + layer.images.push(Image::Vector { + handle: handle.clone(), + color: *color, + bounds: *bounds + translation, + }); + } + Primitive::SolidMesh { buffers, size } => { + let layer = &mut layers[current_layer]; + + let bounds = Rectangle::new( + Point::new(translation.x, translation.y), + *size, + ); + + // Only draw visible content + if let Some(clip_bounds) = layer.bounds.intersection(&bounds) { + layer.meshes.push(Mesh::Solid { + origin: Point::new(translation.x, translation.y), + buffers, + clip_bounds, + }); + } + } + Primitive::GradientMesh { + buffers, + size, + gradient, + } => { + let layer = &mut layers[current_layer]; + + let bounds = Rectangle::new( + Point::new(translation.x, translation.y), + *size, + ); + + // Only draw visible content + if let Some(clip_bounds) = layer.bounds.intersection(&bounds) { + layer.meshes.push(Mesh::Gradient { + origin: Point::new(translation.x, translation.y), + buffers, + clip_bounds, + gradient, + }); + } + } + Primitive::Group { primitives } => { + // TODO: Inspect a bit and regroup (?) + for primitive in primitives { + Self::process_primitive( + layers, + translation, + primitive, + current_layer, + ) + } + } + Primitive::Clip { bounds, content } => { + let layer = &mut layers[current_layer]; + let translated_bounds = *bounds + translation; + + // Only draw visible content + if let Some(clip_bounds) = + layer.bounds.intersection(&translated_bounds) + { + let clip_layer = Layer::new(clip_bounds); + layers.push(clip_layer); + + Self::process_primitive( + layers, + translation, + content, + layers.len() - 1, + ); + } + } + Primitive::Translate { + translation: new_translation, + content, + } => { + Self::process_primitive( + layers, + translation + *new_translation, + content, + current_layer, + ); + } + Primitive::Cache { content } => { + Self::process_primitive( + layers, + translation, + content, + current_layer, + ); + } + Primitive::Fill { .. } | Primitive::Stroke { .. } => { + // Unsupported! + // TODO: Draw a placeholder (?) + } + } + } +} diff --git a/wgpu/src/layer/image.rs b/wgpu/src/layer/image.rs new file mode 100644 index 00000000..3eff2397 --- /dev/null +++ b/wgpu/src/layer/image.rs @@ -0,0 +1,27 @@ +use crate::{Color, Rectangle}; + +use iced_native::{image, svg}; + +/// A raster or vector image. +#[derive(Debug, Clone)] +pub enum Image { + /// A raster image. + Raster { + /// The handle of a raster image. + handle: image::Handle, + + /// The bounds of the image. + bounds: Rectangle, + }, + /// A vector image. + Vector { + /// The handle of a vector image. + handle: svg::Handle, + + /// The [`Color`] filter + color: Option, + + /// The bounds of the image. + bounds: Rectangle, + }, +} diff --git a/wgpu/src/layer/mesh.rs b/wgpu/src/layer/mesh.rs new file mode 100644 index 00000000..5c1e41ad --- /dev/null +++ b/wgpu/src/layer/mesh.rs @@ -0,0 +1,93 @@ +//! A collection of triangle primitives. +use crate::primitive; +use crate::{Gradient, Point, Rectangle}; + +/// A mesh of triangles. +#[derive(Debug, Clone, Copy)] +pub enum Mesh<'a> { + /// A mesh of triangles with a solid color. + Solid { + /// The origin of the vertices of the [`Mesh`]. + origin: Point, + + /// The vertex and index buffers of the [`Mesh`]. + buffers: &'a primitive::Mesh2D, + + /// The clipping bounds of the [`Mesh`]. + clip_bounds: Rectangle, + }, + /// A mesh of triangles with a gradient color. + Gradient { + /// The origin of the vertices of the [`Mesh`]. + origin: Point, + + /// The vertex and index buffers of the [`Mesh`]. + buffers: &'a primitive::Mesh2D, + + /// The clipping bounds of the [`Mesh`]. + clip_bounds: Rectangle, + + /// The gradient to apply to the [`Mesh`]. + gradient: &'a Gradient, + }, +} + +impl Mesh<'_> { + /// Returns the origin of the [`Mesh`]. + pub fn origin(&self) -> Point { + match self { + Self::Solid { origin, .. } | Self::Gradient { origin, .. } => { + *origin + } + } + } + + /// Returns the indices of the [`Mesh`]. + pub fn indices(&self) -> &[u32] { + match self { + Self::Solid { buffers, .. } => &buffers.indices, + Self::Gradient { buffers, .. } => &buffers.indices, + } + } + + /// Returns the clip bounds of the [`Mesh`]. + pub fn clip_bounds(&self) -> Rectangle { + match self { + Self::Solid { clip_bounds, .. } + | Self::Gradient { clip_bounds, .. } => *clip_bounds, + } + } +} + +/// The result of counting the attributes of a set of meshes. +#[derive(Debug, Clone, Copy, Default)] +pub struct AttributeCount { + /// The total amount of solid vertices. + pub solid_vertices: usize, + + /// The total amount of gradient vertices. + pub gradient_vertices: usize, + + /// The total amount of indices. + pub indices: usize, +} + +/// Returns the number of total vertices & total indices of all [`Mesh`]es. +pub fn attribute_count_of<'a>(meshes: &'a [Mesh<'a>]) -> AttributeCount { + meshes + .iter() + .fold(AttributeCount::default(), |mut count, mesh| { + match mesh { + Mesh::Solid { buffers, .. } => { + count.solid_vertices += buffers.vertices.len(); + count.indices += buffers.indices.len(); + } + Mesh::Gradient { buffers, .. } => { + count.gradient_vertices += buffers.vertices.len(); + count.indices += buffers.indices.len(); + } + } + + count + }) +} diff --git a/wgpu/src/layer/quad.rs b/wgpu/src/layer/quad.rs new file mode 100644 index 00000000..0d8bde9d --- /dev/null +++ b/wgpu/src/layer/quad.rs @@ -0,0 +1,30 @@ +/// A colored rectangle with a border. +/// +/// This type can be directly uploaded to GPU memory. +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct Quad { + /// The position of the [`Quad`]. + pub position: [f32; 2], + + /// The size of the [`Quad`]. + pub size: [f32; 2], + + /// The color of the [`Quad`], in __linear RGB__. + pub color: [f32; 4], + + /// The border color of the [`Quad`], in __linear RGB__. + pub border_color: [f32; 4], + + /// The border radius of the [`Quad`]. + pub border_radius: [f32; 4], + + /// The border width of the [`Quad`]. + pub border_width: f32, +} + +#[allow(unsafe_code)] +unsafe impl bytemuck::Zeroable for Quad {} + +#[allow(unsafe_code)] +unsafe impl bytemuck::Pod for Quad {} diff --git a/wgpu/src/layer/text.rs b/wgpu/src/layer/text.rs new file mode 100644 index 00000000..38d62616 --- /dev/null +++ b/wgpu/src/layer/text.rs @@ -0,0 +1,26 @@ +use crate::{alignment, Color, Font, Rectangle}; + +/// A paragraph of text. +#[derive(Debug, Clone, Copy)] +pub struct Text<'a> { + /// The content of the [`Text`]. + pub content: &'a str, + + /// The layout bounds of the [`Text`]. + pub bounds: Rectangle, + + /// The color of the [`Text`], in __linear RGB_. + pub color: Color, + + /// The size of the [`Text`]. + pub size: f32, + + /// The font of the [`Text`]. + pub font: Font, + + /// The horizontal alignment of the [`Text`]. + pub horizontal_alignment: alignment::Horizontal, + + /// The vertical alignment of the [`Text`]. + pub vertical_alignment: alignment::Vertical, +} diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 9da40572..8be12602 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -25,7 +25,7 @@ )] #![deny( missing_debug_implementations, - missing_docs, + //missing_docs, unsafe_code, unused_results, clippy::extra_unused_lifetimes, @@ -38,7 +38,9 @@ #![allow(clippy::inherent_to_string, clippy::type_complexity)] #![cfg_attr(docsrs, feature(doc_cfg))] +pub mod layer; pub mod settings; +pub mod widget; pub mod window; mod backend; @@ -47,16 +49,23 @@ mod quad; mod text; mod triangle; +pub use iced_graphics::primitive; pub use iced_graphics::{ - Antialiasing, Color, Error, Font, Primitive, Viewport, + Antialiasing, Color, Error, Font, Gradient, Point, Rectangle, Size, Vector, + Viewport, }; +pub use iced_native::alignment; + pub use iced_native::Theme; pub use wgpu; pub use backend::Backend; +pub use layer::Layer; +pub use primitive::Primitive; pub use settings::Settings; -use crate::buffer::Buffer; +use buffer::Buffer; + use iced_graphics::Transformation; #[cfg(any(feature = "image", feature = "svg"))] diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs index 246cc5e1..8a568968 100644 --- a/wgpu/src/quad.rs +++ b/wgpu/src/quad.rs @@ -1,5 +1,6 @@ +use crate::layer; use crate::{Buffer, Transformation}; -use iced_graphics::layer; + use iced_native::Rectangle; use bytemuck::{Pod, Zeroable}; diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index dea6ab18..0dc8a64c 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -1,6 +1,7 @@ +use crate::layer::Text; + pub use iced_native::text::Hit; -use iced_graphics::layer::Text; use iced_native::alignment; use iced_native::{Color, Font, Rectangle, Size}; diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index 4b4fa16d..706e4282 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -2,12 +2,12 @@ mod msaa; use crate::buffer::r#static::Buffer; +use crate::layer::mesh::{self, Mesh}; use crate::settings; use crate::Transformation; -use iced_graphics::layer::mesh::{self, Mesh}; -use iced_graphics::triangle::ColoredVertex2D; use iced_graphics::Size; + #[cfg(feature = "tracing")] use tracing::info_span; @@ -468,6 +468,7 @@ mod solid { use crate::settings; use crate::triangle; use encase::ShaderType; + use iced_graphics::primitive; use iced_graphics::Transformation; #[derive(Debug)] @@ -478,7 +479,7 @@ mod solid { #[derive(Debug)] pub struct Layer { - pub vertices: Buffer, + pub vertices: Buffer, pub uniforms: dynamic::Buffer, pub constants: wgpu::BindGroup, } @@ -596,7 +597,7 @@ mod solid { entry_point: "vs_main", buffers: &[wgpu::VertexBufferLayout { array_stride: std::mem::size_of::< - triangle::ColoredVertex2D, + primitive::ColoredVertex2D, >() as u64, step_mode: wgpu::VertexStepMode::Vertex, @@ -637,7 +638,7 @@ mod gradient { use encase::ShaderType; use glam::{IVec4, Vec4}; - use iced_graphics::triangle::Vertex2D; + use iced_graphics::primitive; #[derive(Debug)] pub struct Pipeline { @@ -647,7 +648,7 @@ mod gradient { #[derive(Debug)] pub struct Layer { - pub vertices: Buffer, + pub vertices: Buffer, pub uniforms: dynamic::Buffer, pub storage: dynamic::Buffer, pub constants: wgpu::BindGroup, @@ -810,34 +811,38 @@ mod gradient { ), }); - let pipeline = device.create_render_pipeline( - &wgpu::RenderPipelineDescriptor { - label: Some("iced_wgpu::triangle::gradient pipeline"), - layout: Some(&layout), - vertex: wgpu::VertexState { - module: &shader, - entry_point: "vs_main", - buffers: &[wgpu::VertexBufferLayout { - array_stride: std::mem::size_of::() - as u64, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &wgpu::vertex_attr_array!( - // Position - 0 => Float32x2, - ), - }], + let pipeline = + device.create_render_pipeline( + &wgpu::RenderPipelineDescriptor { + label: Some("iced_wgpu::triangle::gradient pipeline"), + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "vs_main", + buffers: &[wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::< + primitive::Vertex2D, + >( + ) + as u64, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &wgpu::vertex_attr_array!( + // Position + 0 => Float32x2, + ), + }], + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs_main", + targets: &[triangle::fragment_target(format)], + }), + primitive: triangle::primitive_state(), + depth_stencil: None, + multisample: triangle::multisample_state(antialiasing), + multiview: None, }, - fragment: Some(wgpu::FragmentState { - module: &shader, - entry_point: "fs_main", - targets: &[triangle::fragment_target(format)], - }), - primitive: triangle::primitive_state(), - depth_stencil: None, - multisample: triangle::multisample_state(antialiasing), - multiview: None, - }, - ); + ); Self { pipeline, diff --git a/wgpu/src/widget.rs b/wgpu/src/widget.rs new file mode 100644 index 00000000..8d05041e --- /dev/null +++ b/wgpu/src/widget.rs @@ -0,0 +1,9 @@ +//! Use the graphical widgets supported out-of-the-box. + +#[cfg(feature = "canvas")] +#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] +pub mod canvas; + +#[cfg(feature = "canvas")] +#[doc(no_inline)] +pub use canvas::Canvas; diff --git a/wgpu/src/widget/canvas.rs b/wgpu/src/widget/canvas.rs new file mode 100644 index 00000000..41444fcf --- /dev/null +++ b/wgpu/src/widget/canvas.rs @@ -0,0 +1,16 @@ +mod cache; +mod frame; +mod geometry; + +pub use cache::Cache; +pub use frame::Frame; +pub use geometry::Geometry; + +pub use iced_native::widget::canvas::event::{self, Event}; +pub use iced_native::widget::canvas::fill::{self, Fill}; +pub use iced_native::widget::canvas::gradient::{self, Gradient}; +pub use iced_native::widget::canvas::path::{self, Path}; +pub use iced_native::widget::canvas::stroke::{self, Stroke}; +pub use iced_native::widget::canvas::{ + Canvas, Cursor, LineCap, LineDash, LineJoin, Program, Renderer, Style, Text, +}; diff --git a/wgpu/src/widget/canvas/cache.rs b/wgpu/src/widget/canvas/cache.rs new file mode 100644 index 00000000..09b26b90 --- /dev/null +++ b/wgpu/src/widget/canvas/cache.rs @@ -0,0 +1,93 @@ +use crate::widget::canvas::{Frame, Geometry}; +use crate::Primitive; + +use iced_native::Size; +use std::{cell::RefCell, sync::Arc}; + +#[derive(Default)] +enum State { + #[default] + Empty, + Filled { + bounds: Size, + primitive: Arc, + }, +} + +/// A simple cache that stores generated [`Geometry`] to avoid recomputation. +/// +/// A [`Cache`] will not redraw its geometry unless the dimensions of its layer +/// change or it is explicitly cleared. +#[derive(Debug, Default)] +pub struct Cache { + state: RefCell, +} + +impl Cache { + /// Creates a new empty [`Cache`]. + pub fn new() -> Self { + Cache { + state: Default::default(), + } + } + + /// Clears the [`Cache`], forcing a redraw the next time it is used. + pub fn clear(&self) { + *self.state.borrow_mut() = State::Empty; + } + + /// Draws [`Geometry`] using the provided closure and stores it in the + /// [`Cache`]. + /// + /// The closure will only be called when + /// - the bounds have changed since the previous draw call. + /// - the [`Cache`] is empty or has been explicitly cleared. + /// + /// Otherwise, the previously stored [`Geometry`] will be returned. The + /// [`Cache`] is not cleared in this case. In other words, it will keep + /// returning the stored [`Geometry`] if needed. + pub fn draw( + &self, + bounds: Size, + draw_fn: impl FnOnce(&mut Frame), + ) -> Geometry { + use std::ops::Deref; + + if let State::Filled { + bounds: cached_bounds, + primitive, + } = self.state.borrow().deref() + { + if *cached_bounds == bounds { + return Geometry::from_primitive(Primitive::Cache { + content: primitive.clone(), + }); + } + } + + let mut frame = Frame::new(bounds); + draw_fn(&mut frame); + + let primitive = Arc::new(frame.into_primitive()); + + *self.state.borrow_mut() = State::Filled { + bounds, + primitive: primitive.clone(), + }; + + Geometry::from_primitive(Primitive::Cache { content: primitive }) + } +} + +impl std::fmt::Debug for State { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + State::Empty => write!(f, "Empty"), + State::Filled { primitive, bounds } => f + .debug_struct("Filled") + .field("primitive", primitive) + .field("bounds", bounds) + .finish(), + } + } +} diff --git a/wgpu/src/widget/canvas/frame.rs b/wgpu/src/widget/canvas/frame.rs new file mode 100644 index 00000000..987570ec --- /dev/null +++ b/wgpu/src/widget/canvas/frame.rs @@ -0,0 +1,609 @@ +use crate::primitive::{self, Primitive}; +use crate::widget::canvas::fill::{self, Fill}; +use crate::widget::canvas::{ + LineCap, LineDash, LineJoin, Path, Stroke, Style, Text, +}; + +use iced_native::{Gradient, Point, Rectangle, Size, Vector}; + +use lyon::geom::euclid; +use lyon::tessellation; +use std::borrow::Cow; + +/// The frame of a [`Canvas`]. +/// +/// [`Canvas`]: crate::widget::Canvas +#[allow(missing_debug_implementations)] +pub struct Frame { + size: Size, + buffers: BufferStack, + primitives: Vec, + transforms: Transforms, + fill_tessellator: tessellation::FillTessellator, + stroke_tessellator: tessellation::StrokeTessellator, +} + +enum Buffer { + Solid(tessellation::VertexBuffers), + Gradient( + tessellation::VertexBuffers, + Gradient, + ), +} + +struct BufferStack { + stack: Vec, +} + +impl BufferStack { + fn new() -> Self { + Self { stack: Vec::new() } + } + + fn get_mut(&mut self, style: &Style) -> &mut Buffer { + match style { + Style::Solid(_) => match self.stack.last() { + Some(Buffer::Solid(_)) => {} + _ => { + self.stack.push(Buffer::Solid( + tessellation::VertexBuffers::new(), + )); + } + }, + Style::Gradient(gradient) => match self.stack.last() { + Some(Buffer::Gradient(_, last)) if gradient == last => {} + _ => { + self.stack.push(Buffer::Gradient( + tessellation::VertexBuffers::new(), + gradient.clone(), + )); + } + }, + } + + self.stack.last_mut().unwrap() + } + + fn get_fill<'a>( + &'a mut self, + style: &Style, + ) -> Box { + match (style, self.get_mut(style)) { + (Style::Solid(color), Buffer::Solid(buffer)) => { + Box::new(tessellation::BuffersBuilder::new( + buffer, + TriangleVertex2DBuilder(color.into_linear()), + )) + } + (Style::Gradient(_), Buffer::Gradient(buffer, _)) => Box::new( + tessellation::BuffersBuilder::new(buffer, Vertex2DBuilder), + ), + _ => unreachable!(), + } + } + + fn get_stroke<'a>( + &'a mut self, + style: &Style, + ) -> Box { + match (style, self.get_mut(style)) { + (Style::Solid(color), Buffer::Solid(buffer)) => { + Box::new(tessellation::BuffersBuilder::new( + buffer, + TriangleVertex2DBuilder(color.into_linear()), + )) + } + (Style::Gradient(_), Buffer::Gradient(buffer, _)) => Box::new( + tessellation::BuffersBuilder::new(buffer, Vertex2DBuilder), + ), + _ => unreachable!(), + } + } +} + +#[derive(Debug)] +struct Transforms { + previous: Vec, + current: Transform, +} + +#[derive(Debug, Clone, Copy)] +struct Transform { + raw: lyon::math::Transform, + is_identity: bool, +} + +impl Transform { + /// Transforms the given [Point] by the transformation matrix. + fn transform_point(&self, point: &mut Point) { + let transformed = self + .raw + .transform_point(euclid::Point2D::new(point.x, point.y)); + point.x = transformed.x; + point.y = transformed.y; + } + + fn transform_style(&self, style: Style) -> Style { + match style { + Style::Solid(color) => Style::Solid(color), + Style::Gradient(gradient) => { + Style::Gradient(self.transform_gradient(gradient)) + } + } + } + + fn transform_gradient(&self, mut gradient: Gradient) -> Gradient { + let (start, end) = match &mut gradient { + Gradient::Linear(linear) => (&mut linear.start, &mut linear.end), + }; + self.transform_point(start); + self.transform_point(end); + gradient + } +} + +impl Frame { + /// Creates a new empty [`Frame`] with the given dimensions. + /// + /// The default coordinate system of a [`Frame`] has its origin at the + /// top-left corner of its bounds. + pub fn new(size: Size) -> Frame { + Frame { + size, + buffers: BufferStack::new(), + primitives: Vec::new(), + transforms: Transforms { + previous: Vec::new(), + current: Transform { + raw: lyon::math::Transform::identity(), + is_identity: true, + }, + }, + fill_tessellator: tessellation::FillTessellator::new(), + stroke_tessellator: tessellation::StrokeTessellator::new(), + } + } + + /// Returns the width of the [`Frame`]. + #[inline] + pub fn width(&self) -> f32 { + self.size.width + } + + /// Returns the height of the [`Frame`]. + #[inline] + pub fn height(&self) -> f32 { + self.size.height + } + + /// Returns the dimensions of the [`Frame`]. + #[inline] + pub fn size(&self) -> Size { + self.size + } + + /// Returns the coordinate of the center of the [`Frame`]. + #[inline] + pub fn center(&self) -> Point { + Point::new(self.size.width / 2.0, self.size.height / 2.0) + } + + /// Draws the given [`Path`] on the [`Frame`] by filling it with the + /// provided style. + pub fn fill(&mut self, path: &Path, fill: impl Into) { + let Fill { style, rule } = fill.into(); + + let mut buffer = self + .buffers + .get_fill(&self.transforms.current.transform_style(style)); + + let options = tessellation::FillOptions::default() + .with_fill_rule(into_fill_rule(rule)); + + if self.transforms.current.is_identity { + self.fill_tessellator.tessellate_path( + path.raw(), + &options, + buffer.as_mut(), + ) + } else { + let path = path.transform(&self.transforms.current.raw); + + self.fill_tessellator.tessellate_path( + path.raw(), + &options, + buffer.as_mut(), + ) + } + .expect("Tessellate path."); + } + + /// Draws an axis-aligned rectangle given its top-left corner coordinate and + /// its `Size` on the [`Frame`] by filling it with the provided style. + pub fn fill_rectangle( + &mut self, + top_left: Point, + size: Size, + fill: impl Into, + ) { + let Fill { style, rule } = fill.into(); + + let mut buffer = self + .buffers + .get_fill(&self.transforms.current.transform_style(style)); + + let top_left = + self.transforms.current.raw.transform_point( + lyon::math::Point::new(top_left.x, top_left.y), + ); + + let size = + self.transforms.current.raw.transform_vector( + lyon::math::Vector::new(size.width, size.height), + ); + + let options = tessellation::FillOptions::default() + .with_fill_rule(into_fill_rule(rule)); + + self.fill_tessellator + .tessellate_rectangle( + &lyon::math::Box2D::new(top_left, top_left + size), + &options, + buffer.as_mut(), + ) + .expect("Fill rectangle"); + } + + /// Draws the stroke of the given [`Path`] on the [`Frame`] with the + /// provided style. + pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { + let stroke = stroke.into(); + + let mut buffer = self + .buffers + .get_stroke(&self.transforms.current.transform_style(stroke.style)); + + let mut options = tessellation::StrokeOptions::default(); + options.line_width = stroke.width; + options.start_cap = into_line_cap(stroke.line_cap); + options.end_cap = into_line_cap(stroke.line_cap); + options.line_join = into_line_join(stroke.line_join); + + let path = if stroke.line_dash.segments.is_empty() { + Cow::Borrowed(path) + } else { + Cow::Owned(dashed(path, stroke.line_dash)) + }; + + if self.transforms.current.is_identity { + self.stroke_tessellator.tessellate_path( + path.raw(), + &options, + buffer.as_mut(), + ) + } else { + let path = path.transform(&self.transforms.current.raw); + + self.stroke_tessellator.tessellate_path( + path.raw(), + &options, + buffer.as_mut(), + ) + } + .expect("Stroke path"); + } + + /// Draws the characters of the given [`Text`] on the [`Frame`], filling + /// them with the given color. + /// + /// __Warning:__ Text currently does not work well with rotations and scale + /// transforms! The position will be correctly transformed, but the + /// resulting glyphs will not be rotated or scaled properly. + /// + /// Additionally, all text will be rendered on top of all the layers of + /// a [`Canvas`]. Therefore, it is currently only meant to be used for + /// overlays, which is the most common use case. + /// + /// Support for vectorial text is planned, and should address all these + /// limitations. + /// + /// [`Canvas`]: crate::widget::Canvas + pub fn fill_text(&mut self, text: impl Into) { + let text = text.into(); + + let position = if self.transforms.current.is_identity { + text.position + } else { + let transformed = self.transforms.current.raw.transform_point( + lyon::math::Point::new(text.position.x, text.position.y), + ); + + Point::new(transformed.x, transformed.y) + }; + + // TODO: Use vectorial text instead of primitive + self.primitives.push(Primitive::Text { + content: text.content, + bounds: Rectangle { + x: position.x, + y: position.y, + width: f32::INFINITY, + height: f32::INFINITY, + }, + color: text.color, + size: text.size, + font: text.font, + horizontal_alignment: text.horizontal_alignment, + vertical_alignment: text.vertical_alignment, + }); + } + + /// Stores the current transform of the [`Frame`] and executes the given + /// drawing operations, restoring the transform afterwards. + /// + /// This method is useful to compose transforms and perform drawing + /// operations in different coordinate systems. + #[inline] + pub fn with_save(&mut self, f: impl FnOnce(&mut Frame)) { + self.push_transform(); + + f(self); + + self.pop_transform(); + } + + pub fn push_transform(&mut self) { + self.transforms.previous.push(self.transforms.current); + } + + pub fn pop_transform(&mut self) { + self.transforms.current = self.transforms.previous.pop().unwrap(); + } + + /// Executes the given drawing operations within a [`Rectangle`] region, + /// clipping any geometry that overflows its bounds. Any transformations + /// performed are local to the provided closure. + /// + /// This method is useful to perform drawing operations that need to be + /// clipped. + #[inline] + pub fn with_clip(&mut self, region: Rectangle, f: impl FnOnce(&mut Frame)) { + let mut frame = Frame::new(region.size()); + + f(&mut frame); + + let translation = Vector::new(region.x, region.y); + + self.clip(frame, translation); + } + + pub fn clip(&mut self, frame: Frame, translation: Vector) { + let size = frame.size(); + let primitives = frame.into_primitives(); + + let (text, meshes) = primitives + .into_iter() + .partition(|primitive| matches!(primitive, Primitive::Text { .. })); + + self.primitives.push(Primitive::Group { + primitives: vec![ + Primitive::Translate { + translation, + content: Box::new(Primitive::Group { primitives: meshes }), + }, + Primitive::Translate { + translation, + content: Box::new(Primitive::Clip { + bounds: Rectangle::with_size(size), + content: Box::new(Primitive::Group { + primitives: text, + }), + }), + }, + ], + }); + } + + /// Applies a translation to the current transform of the [`Frame`]. + #[inline] + pub fn translate(&mut self, translation: Vector) { + self.transforms.current.raw = self + .transforms + .current + .raw + .pre_translate(lyon::math::Vector::new( + translation.x, + translation.y, + )); + self.transforms.current.is_identity = false; + } + + /// Applies a rotation in radians to the current transform of the [`Frame`]. + #[inline] + pub fn rotate(&mut self, angle: f32) { + self.transforms.current.raw = self + .transforms + .current + .raw + .pre_rotate(lyon::math::Angle::radians(angle)); + self.transforms.current.is_identity = false; + } + + /// Applies a scaling to the current transform of the [`Frame`]. + #[inline] + pub fn scale(&mut self, scale: f32) { + self.transforms.current.raw = + self.transforms.current.raw.pre_scale(scale, scale); + self.transforms.current.is_identity = false; + } + + /// Produces the [`Primitive`] representing everything drawn on the [`Frame`]. + pub fn into_primitive(self) -> Primitive { + Primitive::Group { + primitives: self.into_primitives(), + } + } + + fn into_primitives(mut self) -> Vec { + for buffer in self.buffers.stack { + match buffer { + Buffer::Solid(buffer) => { + if !buffer.indices.is_empty() { + self.primitives.push(Primitive::SolidMesh { + buffers: primitive::Mesh2D { + vertices: buffer.vertices, + indices: buffer.indices, + }, + size: self.size, + }) + } + } + Buffer::Gradient(buffer, gradient) => { + if !buffer.indices.is_empty() { + self.primitives.push(Primitive::GradientMesh { + buffers: primitive::Mesh2D { + vertices: buffer.vertices, + indices: buffer.indices, + }, + size: self.size, + gradient, + }) + } + } + } + } + + self.primitives + } +} + +struct Vertex2DBuilder; + +impl tessellation::FillVertexConstructor + for Vertex2DBuilder +{ + fn new_vertex( + &mut self, + vertex: tessellation::FillVertex<'_>, + ) -> primitive::Vertex2D { + let position = vertex.position(); + + primitive::Vertex2D { + position: [position.x, position.y], + } + } +} + +impl tessellation::StrokeVertexConstructor + for Vertex2DBuilder +{ + fn new_vertex( + &mut self, + vertex: tessellation::StrokeVertex<'_, '_>, + ) -> primitive::Vertex2D { + let position = vertex.position(); + + primitive::Vertex2D { + position: [position.x, position.y], + } + } +} + +struct TriangleVertex2DBuilder([f32; 4]); + +impl tessellation::FillVertexConstructor + for TriangleVertex2DBuilder +{ + fn new_vertex( + &mut self, + vertex: tessellation::FillVertex<'_>, + ) -> primitive::ColoredVertex2D { + let position = vertex.position(); + + primitive::ColoredVertex2D { + position: [position.x, position.y], + color: self.0, + } + } +} + +impl tessellation::StrokeVertexConstructor + for TriangleVertex2DBuilder +{ + fn new_vertex( + &mut self, + vertex: tessellation::StrokeVertex<'_, '_>, + ) -> primitive::ColoredVertex2D { + let position = vertex.position(); + + primitive::ColoredVertex2D { + position: [position.x, position.y], + color: self.0, + } + } +} + +fn into_line_join(line_join: LineJoin) -> lyon::tessellation::LineJoin { + match line_join { + LineJoin::Miter => lyon::tessellation::LineJoin::Miter, + LineJoin::Round => lyon::tessellation::LineJoin::Round, + LineJoin::Bevel => lyon::tessellation::LineJoin::Bevel, + } +} + +fn into_line_cap(line_cap: LineCap) -> lyon::tessellation::LineCap { + match line_cap { + LineCap::Butt => lyon::tessellation::LineCap::Butt, + LineCap::Square => lyon::tessellation::LineCap::Square, + LineCap::Round => lyon::tessellation::LineCap::Round, + } +} + +fn into_fill_rule(rule: fill::Rule) -> lyon::tessellation::FillRule { + match rule { + fill::Rule::NonZero => lyon::tessellation::FillRule::NonZero, + fill::Rule::EvenOdd => lyon::tessellation::FillRule::EvenOdd, + } +} + +pub(super) fn dashed(path: &Path, line_dash: LineDash<'_>) -> Path { + use lyon::algorithms::walk::{ + walk_along_path, RepeatedPattern, WalkerEvent, + }; + use lyon::path::iterator::PathIterator; + + Path::new(|builder| { + let segments_odd = (line_dash.segments.len() % 2 == 1) + .then(|| [line_dash.segments, line_dash.segments].concat()); + + let mut draw_line = false; + + walk_along_path( + path.raw().iter().flattened(0.01), + 0.0, + lyon::tessellation::StrokeOptions::DEFAULT_TOLERANCE, + &mut RepeatedPattern { + callback: |event: WalkerEvent<'_>| { + let point = Point { + x: event.position.x, + y: event.position.y, + }; + + if draw_line { + builder.line_to(point); + } else { + builder.move_to(point); + } + + draw_line = !draw_line; + + true + }, + index: line_dash.offset, + intervals: segments_odd + .as_deref() + .unwrap_or(line_dash.segments), + }, + ); + }) +} diff --git a/wgpu/src/widget/canvas/geometry.rs b/wgpu/src/widget/canvas/geometry.rs new file mode 100644 index 00000000..e8ac621d --- /dev/null +++ b/wgpu/src/widget/canvas/geometry.rs @@ -0,0 +1,24 @@ +use crate::Primitive; + +/// A bunch of shapes that can be drawn. +/// +/// [`Geometry`] can be easily generated with a [`Frame`] or stored in a +/// [`Cache`]. +/// +/// [`Frame`]: crate::widget::canvas::Frame +/// [`Cache`]: crate::widget::canvas::Cache +#[derive(Debug, Clone)] +pub struct Geometry(Primitive); + +impl Geometry { + pub(crate) fn from_primitive(primitive: Primitive) -> Self { + Self(primitive) + } + + /// Turns the [`Geometry`] into a [`Primitive`]. + /// + /// This can be useful if you are building a custom widget. + pub fn into_primitive(self) -> Primitive { + self.0 + } +} -- cgit From 5c0427edbb4358896412c736af2f441c12601d1b Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 1 Mar 2023 21:41:32 +0100 Subject: Fix `Clip` primitive translation in `iced_tiny_skia` --- examples/game_of_life/src/main.rs | 2 +- tiny_skia/src/backend.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 494f71a6..cdb33aca 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -145,7 +145,7 @@ impl Application for GameOfLife { self.grid .view() .map(move |message| Message::Grid(message, version)), - controls + controls, ]; container(content) diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index e08cede7..838426f5 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -221,7 +221,7 @@ impl Backend { pixels, Some(&rectangular_clip_mask( pixels, - *bounds * scale_factor, + (*bounds + translation) * scale_factor, )), scale_factor, translation, -- cgit From 838fd96212b14f20fe2224c4844904a8995f2db7 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 1 Mar 2023 21:47:15 +0100 Subject: Disable `anti_alias` for `Frame::fill_rectangle` in `iced_tiny_skia` --- tiny_skia/src/canvas.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/tiny_skia/src/canvas.rs b/tiny_skia/src/canvas.rs index c3b8b316..59765f59 100644 --- a/tiny_skia/src/canvas.rs +++ b/tiny_skia/src/canvas.rs @@ -44,7 +44,7 @@ impl Frame { self.primitives.push(Primitive::Fill { path, - paint: into_paint(fill.style), + paint: into_paint(fill.style, true), rule: into_fill_rule(fill.rule), transform: self.transform, }); @@ -56,7 +56,15 @@ impl Frame { size: Size, fill: impl Into, ) { - self.fill(&Path::rectangle(top_left, size), fill); + let path = convert_path(&Path::rectangle(top_left, size)); + let fill = fill.into(); + + self.primitives.push(Primitive::Fill { + path, + paint: into_paint(fill.style, false), + rule: into_fill_rule(fill.rule), + transform: self.transform, + }); } pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { @@ -66,7 +74,7 @@ impl Frame { self.primitives.push(Primitive::Stroke { path, - paint: into_paint(stroke.style), + paint: into_paint(stroke.style, true), stroke: skia_stroke, transform: self.transform, }); @@ -199,7 +207,7 @@ fn convert_path(path: &Path) -> tiny_skia::Path { .expect("Convert lyon path to tiny_skia path") } -pub fn into_paint(style: Style) -> tiny_skia::Paint<'static> { +pub fn into_paint(style: Style, anti_alias: bool) -> tiny_skia::Paint<'static> { tiny_skia::Paint { shader: match style { Style::Solid(color) => tiny_skia::Shader::SolidColor( @@ -238,7 +246,7 @@ pub fn into_paint(style: Style) -> tiny_skia::Paint<'static> { .expect("Create linear gradient"), }, }, - anti_alias: true, + anti_alias, ..Default::default() } } -- cgit From 119cf2ecd10f70471199439acb1c4f9d96a57ced Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 1 Mar 2023 21:48:27 +0100 Subject: Remove magic boolean in `into_paint` --- tiny_skia/src/canvas.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tiny_skia/src/canvas.rs b/tiny_skia/src/canvas.rs index 59765f59..958063d2 100644 --- a/tiny_skia/src/canvas.rs +++ b/tiny_skia/src/canvas.rs @@ -44,7 +44,7 @@ impl Frame { self.primitives.push(Primitive::Fill { path, - paint: into_paint(fill.style, true), + paint: into_paint(fill.style), rule: into_fill_rule(fill.rule), transform: self.transform, }); @@ -61,7 +61,10 @@ impl Frame { self.primitives.push(Primitive::Fill { path, - paint: into_paint(fill.style, false), + paint: tiny_skia::Paint { + anti_alias: false, + ..into_paint(fill.style) + }, rule: into_fill_rule(fill.rule), transform: self.transform, }); @@ -74,7 +77,7 @@ impl Frame { self.primitives.push(Primitive::Stroke { path, - paint: into_paint(stroke.style, true), + paint: into_paint(stroke.style), stroke: skia_stroke, transform: self.transform, }); @@ -207,7 +210,7 @@ fn convert_path(path: &Path) -> tiny_skia::Path { .expect("Convert lyon path to tiny_skia path") } -pub fn into_paint(style: Style, anti_alias: bool) -> tiny_skia::Paint<'static> { +pub fn into_paint(style: Style) -> tiny_skia::Paint<'static> { tiny_skia::Paint { shader: match style { Style::Solid(color) => tiny_skia::Shader::SolidColor( @@ -246,7 +249,7 @@ pub fn into_paint(style: Style, anti_alias: bool) -> tiny_skia::Paint<'static> { .expect("Create linear gradient"), }, }, - anti_alias, + anti_alias: true, ..Default::default() } } -- cgit From 350427e82c3a49367da65086c20a307e9b864a23 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 1 Mar 2023 21:52:12 +0100 Subject: Fix missing `qr_code` module in `iced_native` --- native/src/widget.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/native/src/widget.rs b/native/src/widget.rs index 27330894..f107cd69 100644 --- a/native/src/widget.rs +++ b/native/src/widget.rs @@ -91,14 +91,6 @@ pub mod canvas; #[doc(no_inline)] pub use canvas::Canvas; -#[cfg(feature = "qr_code")] -#[cfg_attr(docsrs, doc(cfg(feature = "qr_code")))] -pub mod qr_code; - -#[cfg(feature = "qr_code")] -#[doc(no_inline)] -pub use qr_code::QRCode; - pub use action::Action; pub use id::Id; pub use operation::Operation; -- cgit From 868f79d22e2be82e98b06d66da3b4cbc6139d7c7 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 2 Mar 2023 00:40:36 +0100 Subject: Reuse `ClipMask` in `iced_tiny_skia` --- tiny_skia/src/backend.rs | 41 +++++++++++++++++++++++++------------- tiny_skia/src/window/compositor.rs | 7 +++++-- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 838426f5..2e4663ea 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -25,6 +25,7 @@ impl Backend { pub fn draw>( &mut self, pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: &mut tiny_skia::ClipMask, primitives: &[Primitive], viewport: &Viewport, background_color: Color, @@ -38,6 +39,7 @@ impl Backend { self.draw_primitive( primitive, pixels, + clip_mask, None, scale_factor, Vector::ZERO, @@ -63,6 +65,7 @@ impl Backend { vertical_alignment: alignment::Vertical::Top, }, pixels, + clip_mask, None, scale_factor, Vector::ZERO, @@ -76,7 +79,8 @@ impl Backend { &mut self, primitive: &Primitive, pixels: &mut tiny_skia::PixmapMut<'_>, - clip_mask: Option<&tiny_skia::ClipMask>, + clip_mask: &mut tiny_skia::ClipMask, + clip_bounds: Option, scale_factor: f32, translation: Vector, ) { @@ -95,6 +99,7 @@ impl Backend { .post_scale(scale_factor, scale_factor); let path = rounded_rectangle(*bounds, *border_radius); + let clip_mask = clip_bounds.map(|_| clip_mask as &_); pixels.fill_path( &path, @@ -151,7 +156,7 @@ impl Backend { *horizontal_alignment, *vertical_alignment, pixels, - clip_mask, + clip_bounds.map(|_| clip_mask as &_), ); } Primitive::Image { .. } => { @@ -173,7 +178,7 @@ impl Backend { transform .post_translate(translation.x, translation.y) .post_scale(scale_factor, scale_factor), - clip_mask, + clip_bounds.map(|_| clip_mask as &_), ); } Primitive::Stroke { @@ -189,7 +194,7 @@ impl Backend { transform .post_translate(translation.x, translation.y) .post_scale(scale_factor, scale_factor), - clip_mask, + clip_bounds.map(|_| clip_mask as &_), ); } Primitive::Group { primitives } => { @@ -198,6 +203,7 @@ impl Backend { primitive, pixels, clip_mask, + clip_bounds, scale_factor, translation, ); @@ -211,27 +217,37 @@ impl Backend { content, pixels, clip_mask, + clip_bounds, scale_factor, translation + *offset, ); } Primitive::Clip { bounds, content } => { + let bounds = (*bounds + translation) * scale_factor; + + adjust_clip_mask(clip_mask, pixels, bounds); + self.draw_primitive( content, pixels, - Some(&rectangular_clip_mask( - pixels, - (*bounds + translation) * scale_factor, - )), + clip_mask, + Some(bounds), scale_factor, translation, ); + + if let Some(bounds) = clip_bounds { + adjust_clip_mask(clip_mask, pixels, bounds); + } else { + clip_mask.clear(); + } } Primitive::Cache { content } => { self.draw_primitive( content, pixels, clip_mask, + clip_bounds, scale_factor, translation, ); @@ -393,12 +409,11 @@ fn arc_to( } } -fn rectangular_clip_mask( +fn adjust_clip_mask( + clip_mask: &mut tiny_skia::ClipMask, pixels: &tiny_skia::PixmapMut<'_>, bounds: Rectangle, -) -> tiny_skia::ClipMask { - let mut clip_mask = tiny_skia::ClipMask::new(); - +) { let path = { let mut builder = tiny_skia::PathBuilder::new(); builder.push_rect(bounds.x, bounds.y, bounds.width, bounds.height); @@ -415,8 +430,6 @@ fn rectangular_clip_mask( true, ) .expect("Set path of clipping area"); - - clip_mask } impl iced_graphics::Backend for Backend { diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 08159cd8..76f371e1 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -6,6 +6,7 @@ use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; use std::marker::PhantomData; pub struct Compositor { + clip_mask: tiny_skia::ClipMask, _theme: PhantomData, } @@ -83,9 +84,10 @@ impl iced_graphics::window::Compositor for Compositor { } pub fn new(settings: Settings) -> (Compositor, Backend) { - // TODO + // TOD ( Compositor { + clip_mask: tiny_skia::ClipMask::new(), _theme: PhantomData, }, Backend::new(settings), @@ -93,7 +95,7 @@ pub fn new(settings: Settings) -> (Compositor, Backend) { } pub fn present>( - _compositor: &mut Compositor, + compositor: &mut Compositor, backend: &mut Backend, surface: &mut Surface, primitives: &[Primitive], @@ -110,6 +112,7 @@ pub fn present>( physical_size.height, ) .expect("Create pixel map"), + &mut compositor.clip_mask, primitives, viewport, background_color, -- cgit From b2a9a1e73cb7b2026b2eeaa2be2c04a61c5efb21 Mon Sep 17 00:00:00 2001 From: Bingus Date: Thu, 2 Mar 2023 08:31:39 -0800 Subject: Fixed fullscreen only being possible on primary monitor. --- winit/src/application.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winit/src/application.rs b/winit/src/application.rs index 9781a453..b13b7214 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -762,7 +762,7 @@ pub fn run_command( window::Action::ChangeMode(mode) => { window.set_visible(conversion::visible(mode)); window.set_fullscreen(conversion::fullscreen( - window.primary_monitor(), + window.current_monitor(), mode, )); } -- cgit From a9ca89ca55157d7e94dc6422b4842826139ca2db Mon Sep 17 00:00:00 2001 From: Bingus Date: Thu, 2 Mar 2023 08:43:58 -0800 Subject: Added example of toggling fullscreen to TODOs. --- examples/todos/src/main.rs | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 6408f09c..0f5bfe30 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -1,6 +1,6 @@ use iced::alignment::{self, Alignment}; use iced::event::{self, Event}; -use iced::keyboard; +use iced::keyboard::{self, KeyCode, Modifiers}; use iced::subscription; use iced::theme::{self, Theme}; use iced::widget::{ @@ -8,6 +8,8 @@ use iced::widget::{ text_input, Text, }; use iced::window; +#[cfg(not(target_arch = "wasm32"))] +use iced::window::Mode; use iced::{Application, Element}; use iced::{Color, Command, Font, Length, Settings, Subscription}; @@ -49,7 +51,11 @@ enum Message { CreateTask, FilterChanged(Filter), TaskMessage(usize, TaskMessage), - TabPressed { shift: bool }, + TabPressed { + shift: bool, + }, + #[cfg(not(target_arch = "wasm32"))] + ToggleFullscreen(Mode), } impl Application for Todos { @@ -156,6 +162,10 @@ impl Application for Todos { widget::focus_next() } } + #[cfg(not(target_arch = "wasm32"))] + Message::ToggleFullscreen(mode) => { + window::change_mode(mode) + } _ => Command::none(), }; @@ -266,6 +276,22 @@ impl Application for Todos { ) => Some(Message::TabPressed { shift: modifiers.shift(), }), + #[cfg(not(target_arch = "wasm32"))] + ( + Event::Keyboard(keyboard::Event::KeyPressed { + key_code, + modifiers: Modifiers::SHIFT, + }), + event::Status::Ignored, + ) => match key_code { + KeyCode::Up => { + Some(Message::ToggleFullscreen(Mode::Fullscreen)) + } + KeyCode::Down => { + Some(Message::ToggleFullscreen(Mode::Windowed)) + } + _ => None, + }, _ => None, }) } -- cgit From bbeaf10c04a922af5c1c3b898f0c4301d23feab0 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 3 Mar 2023 03:55:07 +0100 Subject: Mark `Primitive` as `non-exhaustive` in `iced_graphics` --- graphics/Cargo.toml | 5 ++++- graphics/src/primitive.rs | 3 +++ tiny_skia/Cargo.toml | 1 + wgpu/src/layer.rs | 3 +-- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index 36d8a516..62e67cf8 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -34,7 +34,6 @@ log = "0.4" raw-window-handle = "0.5" thiserror = "1.0" bitflags = "1.2" -tiny-skia = "0.8" [dependencies.bytemuck] version = "1.4" @@ -48,6 +47,10 @@ path = "../native" version = "0.7" path = "../style" +[dependencies.tiny-skia] +version = "0.8" +optional = true + [dependencies.image_rs] version = "0.24" package = "image" diff --git a/graphics/src/primitive.rs b/graphics/src/primitive.rs index e4826591..5a48639d 100644 --- a/graphics/src/primitive.rs +++ b/graphics/src/primitive.rs @@ -9,6 +9,7 @@ use std::sync::Arc; /// A rendering primitive. #[derive(Debug, Clone)] +#[non_exhaustive] pub enum Primitive { /// A text primitive Text { @@ -85,12 +86,14 @@ pub enum Primitive { /// The [`Gradient`] to apply to the mesh. gradient: Gradient, }, + #[cfg(feature = "tiny_skia")] Fill { path: tiny_skia::Path, paint: tiny_skia::Paint<'static>, rule: tiny_skia::FillRule, transform: tiny_skia::Transform, }, + #[cfg(feature = "tiny_skia")] Stroke { path: tiny_skia::Path, paint: tiny_skia::Paint<'static>, diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml index 5f39fce2..72181735 100644 --- a/tiny_skia/Cargo.toml +++ b/tiny_skia/Cargo.toml @@ -24,6 +24,7 @@ path = "../native" [dependencies.iced_graphics] version = "0.7" path = "../graphics" +features = ["tiny-skia"] [dependencies.cosmic-text] features = ["std", "swash"] diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index 0840555a..69fcf899 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -265,9 +265,8 @@ impl<'a> Layer<'a> { current_layer, ); } - Primitive::Fill { .. } | Primitive::Stroke { .. } => { + _ => { // Unsupported! - // TODO: Draw a placeholder (?) } } } -- cgit From d13d19ba3569560edd67f20b48f37548d10ceee9 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 3 Mar 2023 04:00:44 +0100 Subject: Rename `canvas::frame` to `canvas` in `iced_wgpu` --- graphics/src/primitive.rs | 4 +- renderer/src/widget/canvas.rs | 6 +- tiny_skia/src/backend.rs | 3 + wgpu/src/canvas.rs | 608 ++++++++++++++++++++++++++++++++++++ wgpu/src/lib.rs | 5 +- wgpu/src/widget.rs | 9 - wgpu/src/widget/canvas.rs | 16 - wgpu/src/widget/canvas/cache.rs | 93 ------ wgpu/src/widget/canvas/frame.rs | 609 ------------------------------------- wgpu/src/widget/canvas/geometry.rs | 24 -- 10 files changed, 619 insertions(+), 758 deletions(-) create mode 100644 wgpu/src/canvas.rs delete mode 100644 wgpu/src/widget.rs delete mode 100644 wgpu/src/widget/canvas.rs delete mode 100644 wgpu/src/widget/canvas/cache.rs delete mode 100644 wgpu/src/widget/canvas/frame.rs delete mode 100644 wgpu/src/widget/canvas/geometry.rs diff --git a/graphics/src/primitive.rs b/graphics/src/primitive.rs index 5a48639d..f900b3fd 100644 --- a/graphics/src/primitive.rs +++ b/graphics/src/primitive.rs @@ -86,14 +86,14 @@ pub enum Primitive { /// The [`Gradient`] to apply to the mesh. gradient: Gradient, }, - #[cfg(feature = "tiny_skia")] + #[cfg(feature = "tiny-skia")] Fill { path: tiny_skia::Path, paint: tiny_skia::Paint<'static>, rule: tiny_skia::FillRule, transform: tiny_skia::Transform, }, - #[cfg(feature = "tiny_skia")] + #[cfg(feature = "tiny-skia")] Stroke { path: tiny_skia::Path, paint: tiny_skia::Paint<'static>, diff --git a/renderer/src/widget/canvas.rs b/renderer/src/widget/canvas.rs index f40a1097..35c8fff9 100644 --- a/renderer/src/widget/canvas.rs +++ b/renderer/src/widget/canvas.rs @@ -16,7 +16,7 @@ use crate::{Backend, Point, Rectangle, Size, Vector}; pub use crate::Geometry; pub enum Frame { - Wgpu(iced_wgpu::widget::canvas::Frame), + Wgpu(iced_wgpu::canvas::Frame), TinySkia(iced_tiny_skia::canvas::Frame), } @@ -33,7 +33,7 @@ impl Frame { pub fn new(renderer: &crate::Renderer, size: Size) -> Self { match renderer.backend() { Backend::Wgpu(_) => { - Frame::Wgpu(iced_wgpu::widget::canvas::Frame::new(size)) + Frame::Wgpu(iced_wgpu::canvas::Frame::new(size)) } Backend::TinySkia(_) => { Frame::TinySkia(iced_tiny_skia::canvas::Frame::new(size)) @@ -131,7 +131,7 @@ impl Frame { pub fn with_clip(&mut self, region: Rectangle, f: impl FnOnce(&mut Frame)) { let mut frame = match self { Self::Wgpu(_) => { - Self::Wgpu(iced_wgpu::widget::canvas::Frame::new(region.size())) + Self::Wgpu(iced_wgpu::canvas::Frame::new(region.size())) } Self::TinySkia(_) => Self::TinySkia( iced_tiny_skia::canvas::Frame::new(region.size()), diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 2e4663ea..6883a953 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -256,6 +256,9 @@ impl Backend { // Not supported! // TODO: Draw a placeholder (?) / Log it (?) } + _ => { + // Not supported! + } } } } diff --git a/wgpu/src/canvas.rs b/wgpu/src/canvas.rs new file mode 100644 index 00000000..e8d540c3 --- /dev/null +++ b/wgpu/src/canvas.rs @@ -0,0 +1,608 @@ +use iced_graphics::primitive::{self, Primitive}; +use iced_native::widget::canvas::fill::{self, Fill}; +use iced_native::widget::canvas::{ + LineCap, LineDash, LineJoin, Path, Stroke, Style, Text, +}; +use iced_native::{Gradient, Point, Rectangle, Size, Vector}; + +use lyon::geom::euclid; +use lyon::tessellation; +use std::borrow::Cow; + +/// The frame of a [`Canvas`]. +/// +/// [`Canvas`]: crate::widget::Canvas +#[allow(missing_debug_implementations)] +pub struct Frame { + size: Size, + buffers: BufferStack, + primitives: Vec, + transforms: Transforms, + fill_tessellator: tessellation::FillTessellator, + stroke_tessellator: tessellation::StrokeTessellator, +} + +enum Buffer { + Solid(tessellation::VertexBuffers), + Gradient( + tessellation::VertexBuffers, + Gradient, + ), +} + +struct BufferStack { + stack: Vec, +} + +impl BufferStack { + fn new() -> Self { + Self { stack: Vec::new() } + } + + fn get_mut(&mut self, style: &Style) -> &mut Buffer { + match style { + Style::Solid(_) => match self.stack.last() { + Some(Buffer::Solid(_)) => {} + _ => { + self.stack.push(Buffer::Solid( + tessellation::VertexBuffers::new(), + )); + } + }, + Style::Gradient(gradient) => match self.stack.last() { + Some(Buffer::Gradient(_, last)) if gradient == last => {} + _ => { + self.stack.push(Buffer::Gradient( + tessellation::VertexBuffers::new(), + gradient.clone(), + )); + } + }, + } + + self.stack.last_mut().unwrap() + } + + fn get_fill<'a>( + &'a mut self, + style: &Style, + ) -> Box { + match (style, self.get_mut(style)) { + (Style::Solid(color), Buffer::Solid(buffer)) => { + Box::new(tessellation::BuffersBuilder::new( + buffer, + TriangleVertex2DBuilder(color.into_linear()), + )) + } + (Style::Gradient(_), Buffer::Gradient(buffer, _)) => Box::new( + tessellation::BuffersBuilder::new(buffer, Vertex2DBuilder), + ), + _ => unreachable!(), + } + } + + fn get_stroke<'a>( + &'a mut self, + style: &Style, + ) -> Box { + match (style, self.get_mut(style)) { + (Style::Solid(color), Buffer::Solid(buffer)) => { + Box::new(tessellation::BuffersBuilder::new( + buffer, + TriangleVertex2DBuilder(color.into_linear()), + )) + } + (Style::Gradient(_), Buffer::Gradient(buffer, _)) => Box::new( + tessellation::BuffersBuilder::new(buffer, Vertex2DBuilder), + ), + _ => unreachable!(), + } + } +} + +#[derive(Debug)] +struct Transforms { + previous: Vec, + current: Transform, +} + +#[derive(Debug, Clone, Copy)] +struct Transform { + raw: lyon::math::Transform, + is_identity: bool, +} + +impl Transform { + /// Transforms the given [Point] by the transformation matrix. + fn transform_point(&self, point: &mut Point) { + let transformed = self + .raw + .transform_point(euclid::Point2D::new(point.x, point.y)); + point.x = transformed.x; + point.y = transformed.y; + } + + fn transform_style(&self, style: Style) -> Style { + match style { + Style::Solid(color) => Style::Solid(color), + Style::Gradient(gradient) => { + Style::Gradient(self.transform_gradient(gradient)) + } + } + } + + fn transform_gradient(&self, mut gradient: Gradient) -> Gradient { + let (start, end) = match &mut gradient { + Gradient::Linear(linear) => (&mut linear.start, &mut linear.end), + }; + self.transform_point(start); + self.transform_point(end); + gradient + } +} + +impl Frame { + /// Creates a new empty [`Frame`] with the given dimensions. + /// + /// The default coordinate system of a [`Frame`] has its origin at the + /// top-left corner of its bounds. + pub fn new(size: Size) -> Frame { + Frame { + size, + buffers: BufferStack::new(), + primitives: Vec::new(), + transforms: Transforms { + previous: Vec::new(), + current: Transform { + raw: lyon::math::Transform::identity(), + is_identity: true, + }, + }, + fill_tessellator: tessellation::FillTessellator::new(), + stroke_tessellator: tessellation::StrokeTessellator::new(), + } + } + + /// Returns the width of the [`Frame`]. + #[inline] + pub fn width(&self) -> f32 { + self.size.width + } + + /// Returns the height of the [`Frame`]. + #[inline] + pub fn height(&self) -> f32 { + self.size.height + } + + /// Returns the dimensions of the [`Frame`]. + #[inline] + pub fn size(&self) -> Size { + self.size + } + + /// Returns the coordinate of the center of the [`Frame`]. + #[inline] + pub fn center(&self) -> Point { + Point::new(self.size.width / 2.0, self.size.height / 2.0) + } + + /// Draws the given [`Path`] on the [`Frame`] by filling it with the + /// provided style. + pub fn fill(&mut self, path: &Path, fill: impl Into) { + let Fill { style, rule } = fill.into(); + + let mut buffer = self + .buffers + .get_fill(&self.transforms.current.transform_style(style)); + + let options = tessellation::FillOptions::default() + .with_fill_rule(into_fill_rule(rule)); + + if self.transforms.current.is_identity { + self.fill_tessellator.tessellate_path( + path.raw(), + &options, + buffer.as_mut(), + ) + } else { + let path = path.transform(&self.transforms.current.raw); + + self.fill_tessellator.tessellate_path( + path.raw(), + &options, + buffer.as_mut(), + ) + } + .expect("Tessellate path."); + } + + /// Draws an axis-aligned rectangle given its top-left corner coordinate and + /// its `Size` on the [`Frame`] by filling it with the provided style. + pub fn fill_rectangle( + &mut self, + top_left: Point, + size: Size, + fill: impl Into, + ) { + let Fill { style, rule } = fill.into(); + + let mut buffer = self + .buffers + .get_fill(&self.transforms.current.transform_style(style)); + + let top_left = + self.transforms.current.raw.transform_point( + lyon::math::Point::new(top_left.x, top_left.y), + ); + + let size = + self.transforms.current.raw.transform_vector( + lyon::math::Vector::new(size.width, size.height), + ); + + let options = tessellation::FillOptions::default() + .with_fill_rule(into_fill_rule(rule)); + + self.fill_tessellator + .tessellate_rectangle( + &lyon::math::Box2D::new(top_left, top_left + size), + &options, + buffer.as_mut(), + ) + .expect("Fill rectangle"); + } + + /// Draws the stroke of the given [`Path`] on the [`Frame`] with the + /// provided style. + pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { + let stroke = stroke.into(); + + let mut buffer = self + .buffers + .get_stroke(&self.transforms.current.transform_style(stroke.style)); + + let mut options = tessellation::StrokeOptions::default(); + options.line_width = stroke.width; + options.start_cap = into_line_cap(stroke.line_cap); + options.end_cap = into_line_cap(stroke.line_cap); + options.line_join = into_line_join(stroke.line_join); + + let path = if stroke.line_dash.segments.is_empty() { + Cow::Borrowed(path) + } else { + Cow::Owned(dashed(path, stroke.line_dash)) + }; + + if self.transforms.current.is_identity { + self.stroke_tessellator.tessellate_path( + path.raw(), + &options, + buffer.as_mut(), + ) + } else { + let path = path.transform(&self.transforms.current.raw); + + self.stroke_tessellator.tessellate_path( + path.raw(), + &options, + buffer.as_mut(), + ) + } + .expect("Stroke path"); + } + + /// Draws the characters of the given [`Text`] on the [`Frame`], filling + /// them with the given color. + /// + /// __Warning:__ Text currently does not work well with rotations and scale + /// transforms! The position will be correctly transformed, but the + /// resulting glyphs will not be rotated or scaled properly. + /// + /// Additionally, all text will be rendered on top of all the layers of + /// a [`Canvas`]. Therefore, it is currently only meant to be used for + /// overlays, which is the most common use case. + /// + /// Support for vectorial text is planned, and should address all these + /// limitations. + /// + /// [`Canvas`]: crate::widget::Canvas + pub fn fill_text(&mut self, text: impl Into) { + let text = text.into(); + + let position = if self.transforms.current.is_identity { + text.position + } else { + let transformed = self.transforms.current.raw.transform_point( + lyon::math::Point::new(text.position.x, text.position.y), + ); + + Point::new(transformed.x, transformed.y) + }; + + // TODO: Use vectorial text instead of primitive + self.primitives.push(Primitive::Text { + content: text.content, + bounds: Rectangle { + x: position.x, + y: position.y, + width: f32::INFINITY, + height: f32::INFINITY, + }, + color: text.color, + size: text.size, + font: text.font, + horizontal_alignment: text.horizontal_alignment, + vertical_alignment: text.vertical_alignment, + }); + } + + /// Stores the current transform of the [`Frame`] and executes the given + /// drawing operations, restoring the transform afterwards. + /// + /// This method is useful to compose transforms and perform drawing + /// operations in different coordinate systems. + #[inline] + pub fn with_save(&mut self, f: impl FnOnce(&mut Frame)) { + self.push_transform(); + + f(self); + + self.pop_transform(); + } + + pub fn push_transform(&mut self) { + self.transforms.previous.push(self.transforms.current); + } + + pub fn pop_transform(&mut self) { + self.transforms.current = self.transforms.previous.pop().unwrap(); + } + + /// Executes the given drawing operations within a [`Rectangle`] region, + /// clipping any geometry that overflows its bounds. Any transformations + /// performed are local to the provided closure. + /// + /// This method is useful to perform drawing operations that need to be + /// clipped. + #[inline] + pub fn with_clip(&mut self, region: Rectangle, f: impl FnOnce(&mut Frame)) { + let mut frame = Frame::new(region.size()); + + f(&mut frame); + + let translation = Vector::new(region.x, region.y); + + self.clip(frame, translation); + } + + pub fn clip(&mut self, frame: Frame, translation: Vector) { + let size = frame.size(); + let primitives = frame.into_primitives(); + + let (text, meshes) = primitives + .into_iter() + .partition(|primitive| matches!(primitive, Primitive::Text { .. })); + + self.primitives.push(Primitive::Group { + primitives: vec![ + Primitive::Translate { + translation, + content: Box::new(Primitive::Group { primitives: meshes }), + }, + Primitive::Translate { + translation, + content: Box::new(Primitive::Clip { + bounds: Rectangle::with_size(size), + content: Box::new(Primitive::Group { + primitives: text, + }), + }), + }, + ], + }); + } + + /// Applies a translation to the current transform of the [`Frame`]. + #[inline] + pub fn translate(&mut self, translation: Vector) { + self.transforms.current.raw = self + .transforms + .current + .raw + .pre_translate(lyon::math::Vector::new( + translation.x, + translation.y, + )); + self.transforms.current.is_identity = false; + } + + /// Applies a rotation in radians to the current transform of the [`Frame`]. + #[inline] + pub fn rotate(&mut self, angle: f32) { + self.transforms.current.raw = self + .transforms + .current + .raw + .pre_rotate(lyon::math::Angle::radians(angle)); + self.transforms.current.is_identity = false; + } + + /// Applies a scaling to the current transform of the [`Frame`]. + #[inline] + pub fn scale(&mut self, scale: f32) { + self.transforms.current.raw = + self.transforms.current.raw.pre_scale(scale, scale); + self.transforms.current.is_identity = false; + } + + /// Produces the [`Primitive`] representing everything drawn on the [`Frame`]. + pub fn into_primitive(self) -> Primitive { + Primitive::Group { + primitives: self.into_primitives(), + } + } + + fn into_primitives(mut self) -> Vec { + for buffer in self.buffers.stack { + match buffer { + Buffer::Solid(buffer) => { + if !buffer.indices.is_empty() { + self.primitives.push(Primitive::SolidMesh { + buffers: primitive::Mesh2D { + vertices: buffer.vertices, + indices: buffer.indices, + }, + size: self.size, + }) + } + } + Buffer::Gradient(buffer, gradient) => { + if !buffer.indices.is_empty() { + self.primitives.push(Primitive::GradientMesh { + buffers: primitive::Mesh2D { + vertices: buffer.vertices, + indices: buffer.indices, + }, + size: self.size, + gradient, + }) + } + } + } + } + + self.primitives + } +} + +struct Vertex2DBuilder; + +impl tessellation::FillVertexConstructor + for Vertex2DBuilder +{ + fn new_vertex( + &mut self, + vertex: tessellation::FillVertex<'_>, + ) -> primitive::Vertex2D { + let position = vertex.position(); + + primitive::Vertex2D { + position: [position.x, position.y], + } + } +} + +impl tessellation::StrokeVertexConstructor + for Vertex2DBuilder +{ + fn new_vertex( + &mut self, + vertex: tessellation::StrokeVertex<'_, '_>, + ) -> primitive::Vertex2D { + let position = vertex.position(); + + primitive::Vertex2D { + position: [position.x, position.y], + } + } +} + +struct TriangleVertex2DBuilder([f32; 4]); + +impl tessellation::FillVertexConstructor + for TriangleVertex2DBuilder +{ + fn new_vertex( + &mut self, + vertex: tessellation::FillVertex<'_>, + ) -> primitive::ColoredVertex2D { + let position = vertex.position(); + + primitive::ColoredVertex2D { + position: [position.x, position.y], + color: self.0, + } + } +} + +impl tessellation::StrokeVertexConstructor + for TriangleVertex2DBuilder +{ + fn new_vertex( + &mut self, + vertex: tessellation::StrokeVertex<'_, '_>, + ) -> primitive::ColoredVertex2D { + let position = vertex.position(); + + primitive::ColoredVertex2D { + position: [position.x, position.y], + color: self.0, + } + } +} + +fn into_line_join(line_join: LineJoin) -> lyon::tessellation::LineJoin { + match line_join { + LineJoin::Miter => lyon::tessellation::LineJoin::Miter, + LineJoin::Round => lyon::tessellation::LineJoin::Round, + LineJoin::Bevel => lyon::tessellation::LineJoin::Bevel, + } +} + +fn into_line_cap(line_cap: LineCap) -> lyon::tessellation::LineCap { + match line_cap { + LineCap::Butt => lyon::tessellation::LineCap::Butt, + LineCap::Square => lyon::tessellation::LineCap::Square, + LineCap::Round => lyon::tessellation::LineCap::Round, + } +} + +fn into_fill_rule(rule: fill::Rule) -> lyon::tessellation::FillRule { + match rule { + fill::Rule::NonZero => lyon::tessellation::FillRule::NonZero, + fill::Rule::EvenOdd => lyon::tessellation::FillRule::EvenOdd, + } +} + +pub(super) fn dashed(path: &Path, line_dash: LineDash<'_>) -> Path { + use lyon::algorithms::walk::{ + walk_along_path, RepeatedPattern, WalkerEvent, + }; + use lyon::path::iterator::PathIterator; + + Path::new(|builder| { + let segments_odd = (line_dash.segments.len() % 2 == 1) + .then(|| [line_dash.segments, line_dash.segments].concat()); + + let mut draw_line = false; + + walk_along_path( + path.raw().iter().flattened(0.01), + 0.0, + lyon::tessellation::StrokeOptions::DEFAULT_TOLERANCE, + &mut RepeatedPattern { + callback: |event: WalkerEvent<'_>| { + let point = Point { + x: event.position.x, + y: event.position.y, + }; + + if draw_line { + builder.line_to(point); + } else { + builder.move_to(point); + } + + draw_line = !draw_line; + + true + }, + index: line_dash.offset, + intervals: segments_odd + .as_deref() + .unwrap_or(line_dash.segments), + }, + ); + }) +} diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 8be12602..31db16a8 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -37,12 +37,13 @@ #![forbid(rust_2018_idioms)] #![allow(clippy::inherent_to_string, clippy::type_complexity)] #![cfg_attr(docsrs, feature(doc_cfg))] - pub mod layer; pub mod settings; -pub mod widget; pub mod window; +#[cfg(feature = "canvas")] +pub mod canvas; + mod backend; mod buffer; mod quad; diff --git a/wgpu/src/widget.rs b/wgpu/src/widget.rs deleted file mode 100644 index 8d05041e..00000000 --- a/wgpu/src/widget.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! Use the graphical widgets supported out-of-the-box. - -#[cfg(feature = "canvas")] -#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] -pub mod canvas; - -#[cfg(feature = "canvas")] -#[doc(no_inline)] -pub use canvas::Canvas; diff --git a/wgpu/src/widget/canvas.rs b/wgpu/src/widget/canvas.rs deleted file mode 100644 index 41444fcf..00000000 --- a/wgpu/src/widget/canvas.rs +++ /dev/null @@ -1,16 +0,0 @@ -mod cache; -mod frame; -mod geometry; - -pub use cache::Cache; -pub use frame::Frame; -pub use geometry::Geometry; - -pub use iced_native::widget::canvas::event::{self, Event}; -pub use iced_native::widget::canvas::fill::{self, Fill}; -pub use iced_native::widget::canvas::gradient::{self, Gradient}; -pub use iced_native::widget::canvas::path::{self, Path}; -pub use iced_native::widget::canvas::stroke::{self, Stroke}; -pub use iced_native::widget::canvas::{ - Canvas, Cursor, LineCap, LineDash, LineJoin, Program, Renderer, Style, Text, -}; diff --git a/wgpu/src/widget/canvas/cache.rs b/wgpu/src/widget/canvas/cache.rs deleted file mode 100644 index 09b26b90..00000000 --- a/wgpu/src/widget/canvas/cache.rs +++ /dev/null @@ -1,93 +0,0 @@ -use crate::widget::canvas::{Frame, Geometry}; -use crate::Primitive; - -use iced_native::Size; -use std::{cell::RefCell, sync::Arc}; - -#[derive(Default)] -enum State { - #[default] - Empty, - Filled { - bounds: Size, - primitive: Arc, - }, -} - -/// A simple cache that stores generated [`Geometry`] to avoid recomputation. -/// -/// A [`Cache`] will not redraw its geometry unless the dimensions of its layer -/// change or it is explicitly cleared. -#[derive(Debug, Default)] -pub struct Cache { - state: RefCell, -} - -impl Cache { - /// Creates a new empty [`Cache`]. - pub fn new() -> Self { - Cache { - state: Default::default(), - } - } - - /// Clears the [`Cache`], forcing a redraw the next time it is used. - pub fn clear(&self) { - *self.state.borrow_mut() = State::Empty; - } - - /// Draws [`Geometry`] using the provided closure and stores it in the - /// [`Cache`]. - /// - /// The closure will only be called when - /// - the bounds have changed since the previous draw call. - /// - the [`Cache`] is empty or has been explicitly cleared. - /// - /// Otherwise, the previously stored [`Geometry`] will be returned. The - /// [`Cache`] is not cleared in this case. In other words, it will keep - /// returning the stored [`Geometry`] if needed. - pub fn draw( - &self, - bounds: Size, - draw_fn: impl FnOnce(&mut Frame), - ) -> Geometry { - use std::ops::Deref; - - if let State::Filled { - bounds: cached_bounds, - primitive, - } = self.state.borrow().deref() - { - if *cached_bounds == bounds { - return Geometry::from_primitive(Primitive::Cache { - content: primitive.clone(), - }); - } - } - - let mut frame = Frame::new(bounds); - draw_fn(&mut frame); - - let primitive = Arc::new(frame.into_primitive()); - - *self.state.borrow_mut() = State::Filled { - bounds, - primitive: primitive.clone(), - }; - - Geometry::from_primitive(Primitive::Cache { content: primitive }) - } -} - -impl std::fmt::Debug for State { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - State::Empty => write!(f, "Empty"), - State::Filled { primitive, bounds } => f - .debug_struct("Filled") - .field("primitive", primitive) - .field("bounds", bounds) - .finish(), - } - } -} diff --git a/wgpu/src/widget/canvas/frame.rs b/wgpu/src/widget/canvas/frame.rs deleted file mode 100644 index 987570ec..00000000 --- a/wgpu/src/widget/canvas/frame.rs +++ /dev/null @@ -1,609 +0,0 @@ -use crate::primitive::{self, Primitive}; -use crate::widget::canvas::fill::{self, Fill}; -use crate::widget::canvas::{ - LineCap, LineDash, LineJoin, Path, Stroke, Style, Text, -}; - -use iced_native::{Gradient, Point, Rectangle, Size, Vector}; - -use lyon::geom::euclid; -use lyon::tessellation; -use std::borrow::Cow; - -/// The frame of a [`Canvas`]. -/// -/// [`Canvas`]: crate::widget::Canvas -#[allow(missing_debug_implementations)] -pub struct Frame { - size: Size, - buffers: BufferStack, - primitives: Vec, - transforms: Transforms, - fill_tessellator: tessellation::FillTessellator, - stroke_tessellator: tessellation::StrokeTessellator, -} - -enum Buffer { - Solid(tessellation::VertexBuffers), - Gradient( - tessellation::VertexBuffers, - Gradient, - ), -} - -struct BufferStack { - stack: Vec, -} - -impl BufferStack { - fn new() -> Self { - Self { stack: Vec::new() } - } - - fn get_mut(&mut self, style: &Style) -> &mut Buffer { - match style { - Style::Solid(_) => match self.stack.last() { - Some(Buffer::Solid(_)) => {} - _ => { - self.stack.push(Buffer::Solid( - tessellation::VertexBuffers::new(), - )); - } - }, - Style::Gradient(gradient) => match self.stack.last() { - Some(Buffer::Gradient(_, last)) if gradient == last => {} - _ => { - self.stack.push(Buffer::Gradient( - tessellation::VertexBuffers::new(), - gradient.clone(), - )); - } - }, - } - - self.stack.last_mut().unwrap() - } - - fn get_fill<'a>( - &'a mut self, - style: &Style, - ) -> Box { - match (style, self.get_mut(style)) { - (Style::Solid(color), Buffer::Solid(buffer)) => { - Box::new(tessellation::BuffersBuilder::new( - buffer, - TriangleVertex2DBuilder(color.into_linear()), - )) - } - (Style::Gradient(_), Buffer::Gradient(buffer, _)) => Box::new( - tessellation::BuffersBuilder::new(buffer, Vertex2DBuilder), - ), - _ => unreachable!(), - } - } - - fn get_stroke<'a>( - &'a mut self, - style: &Style, - ) -> Box { - match (style, self.get_mut(style)) { - (Style::Solid(color), Buffer::Solid(buffer)) => { - Box::new(tessellation::BuffersBuilder::new( - buffer, - TriangleVertex2DBuilder(color.into_linear()), - )) - } - (Style::Gradient(_), Buffer::Gradient(buffer, _)) => Box::new( - tessellation::BuffersBuilder::new(buffer, Vertex2DBuilder), - ), - _ => unreachable!(), - } - } -} - -#[derive(Debug)] -struct Transforms { - previous: Vec, - current: Transform, -} - -#[derive(Debug, Clone, Copy)] -struct Transform { - raw: lyon::math::Transform, - is_identity: bool, -} - -impl Transform { - /// Transforms the given [Point] by the transformation matrix. - fn transform_point(&self, point: &mut Point) { - let transformed = self - .raw - .transform_point(euclid::Point2D::new(point.x, point.y)); - point.x = transformed.x; - point.y = transformed.y; - } - - fn transform_style(&self, style: Style) -> Style { - match style { - Style::Solid(color) => Style::Solid(color), - Style::Gradient(gradient) => { - Style::Gradient(self.transform_gradient(gradient)) - } - } - } - - fn transform_gradient(&self, mut gradient: Gradient) -> Gradient { - let (start, end) = match &mut gradient { - Gradient::Linear(linear) => (&mut linear.start, &mut linear.end), - }; - self.transform_point(start); - self.transform_point(end); - gradient - } -} - -impl Frame { - /// Creates a new empty [`Frame`] with the given dimensions. - /// - /// The default coordinate system of a [`Frame`] has its origin at the - /// top-left corner of its bounds. - pub fn new(size: Size) -> Frame { - Frame { - size, - buffers: BufferStack::new(), - primitives: Vec::new(), - transforms: Transforms { - previous: Vec::new(), - current: Transform { - raw: lyon::math::Transform::identity(), - is_identity: true, - }, - }, - fill_tessellator: tessellation::FillTessellator::new(), - stroke_tessellator: tessellation::StrokeTessellator::new(), - } - } - - /// Returns the width of the [`Frame`]. - #[inline] - pub fn width(&self) -> f32 { - self.size.width - } - - /// Returns the height of the [`Frame`]. - #[inline] - pub fn height(&self) -> f32 { - self.size.height - } - - /// Returns the dimensions of the [`Frame`]. - #[inline] - pub fn size(&self) -> Size { - self.size - } - - /// Returns the coordinate of the center of the [`Frame`]. - #[inline] - pub fn center(&self) -> Point { - Point::new(self.size.width / 2.0, self.size.height / 2.0) - } - - /// Draws the given [`Path`] on the [`Frame`] by filling it with the - /// provided style. - pub fn fill(&mut self, path: &Path, fill: impl Into) { - let Fill { style, rule } = fill.into(); - - let mut buffer = self - .buffers - .get_fill(&self.transforms.current.transform_style(style)); - - let options = tessellation::FillOptions::default() - .with_fill_rule(into_fill_rule(rule)); - - if self.transforms.current.is_identity { - self.fill_tessellator.tessellate_path( - path.raw(), - &options, - buffer.as_mut(), - ) - } else { - let path = path.transform(&self.transforms.current.raw); - - self.fill_tessellator.tessellate_path( - path.raw(), - &options, - buffer.as_mut(), - ) - } - .expect("Tessellate path."); - } - - /// Draws an axis-aligned rectangle given its top-left corner coordinate and - /// its `Size` on the [`Frame`] by filling it with the provided style. - pub fn fill_rectangle( - &mut self, - top_left: Point, - size: Size, - fill: impl Into, - ) { - let Fill { style, rule } = fill.into(); - - let mut buffer = self - .buffers - .get_fill(&self.transforms.current.transform_style(style)); - - let top_left = - self.transforms.current.raw.transform_point( - lyon::math::Point::new(top_left.x, top_left.y), - ); - - let size = - self.transforms.current.raw.transform_vector( - lyon::math::Vector::new(size.width, size.height), - ); - - let options = tessellation::FillOptions::default() - .with_fill_rule(into_fill_rule(rule)); - - self.fill_tessellator - .tessellate_rectangle( - &lyon::math::Box2D::new(top_left, top_left + size), - &options, - buffer.as_mut(), - ) - .expect("Fill rectangle"); - } - - /// Draws the stroke of the given [`Path`] on the [`Frame`] with the - /// provided style. - pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { - let stroke = stroke.into(); - - let mut buffer = self - .buffers - .get_stroke(&self.transforms.current.transform_style(stroke.style)); - - let mut options = tessellation::StrokeOptions::default(); - options.line_width = stroke.width; - options.start_cap = into_line_cap(stroke.line_cap); - options.end_cap = into_line_cap(stroke.line_cap); - options.line_join = into_line_join(stroke.line_join); - - let path = if stroke.line_dash.segments.is_empty() { - Cow::Borrowed(path) - } else { - Cow::Owned(dashed(path, stroke.line_dash)) - }; - - if self.transforms.current.is_identity { - self.stroke_tessellator.tessellate_path( - path.raw(), - &options, - buffer.as_mut(), - ) - } else { - let path = path.transform(&self.transforms.current.raw); - - self.stroke_tessellator.tessellate_path( - path.raw(), - &options, - buffer.as_mut(), - ) - } - .expect("Stroke path"); - } - - /// Draws the characters of the given [`Text`] on the [`Frame`], filling - /// them with the given color. - /// - /// __Warning:__ Text currently does not work well with rotations and scale - /// transforms! The position will be correctly transformed, but the - /// resulting glyphs will not be rotated or scaled properly. - /// - /// Additionally, all text will be rendered on top of all the layers of - /// a [`Canvas`]. Therefore, it is currently only meant to be used for - /// overlays, which is the most common use case. - /// - /// Support for vectorial text is planned, and should address all these - /// limitations. - /// - /// [`Canvas`]: crate::widget::Canvas - pub fn fill_text(&mut self, text: impl Into) { - let text = text.into(); - - let position = if self.transforms.current.is_identity { - text.position - } else { - let transformed = self.transforms.current.raw.transform_point( - lyon::math::Point::new(text.position.x, text.position.y), - ); - - Point::new(transformed.x, transformed.y) - }; - - // TODO: Use vectorial text instead of primitive - self.primitives.push(Primitive::Text { - content: text.content, - bounds: Rectangle { - x: position.x, - y: position.y, - width: f32::INFINITY, - height: f32::INFINITY, - }, - color: text.color, - size: text.size, - font: text.font, - horizontal_alignment: text.horizontal_alignment, - vertical_alignment: text.vertical_alignment, - }); - } - - /// Stores the current transform of the [`Frame`] and executes the given - /// drawing operations, restoring the transform afterwards. - /// - /// This method is useful to compose transforms and perform drawing - /// operations in different coordinate systems. - #[inline] - pub fn with_save(&mut self, f: impl FnOnce(&mut Frame)) { - self.push_transform(); - - f(self); - - self.pop_transform(); - } - - pub fn push_transform(&mut self) { - self.transforms.previous.push(self.transforms.current); - } - - pub fn pop_transform(&mut self) { - self.transforms.current = self.transforms.previous.pop().unwrap(); - } - - /// Executes the given drawing operations within a [`Rectangle`] region, - /// clipping any geometry that overflows its bounds. Any transformations - /// performed are local to the provided closure. - /// - /// This method is useful to perform drawing operations that need to be - /// clipped. - #[inline] - pub fn with_clip(&mut self, region: Rectangle, f: impl FnOnce(&mut Frame)) { - let mut frame = Frame::new(region.size()); - - f(&mut frame); - - let translation = Vector::new(region.x, region.y); - - self.clip(frame, translation); - } - - pub fn clip(&mut self, frame: Frame, translation: Vector) { - let size = frame.size(); - let primitives = frame.into_primitives(); - - let (text, meshes) = primitives - .into_iter() - .partition(|primitive| matches!(primitive, Primitive::Text { .. })); - - self.primitives.push(Primitive::Group { - primitives: vec![ - Primitive::Translate { - translation, - content: Box::new(Primitive::Group { primitives: meshes }), - }, - Primitive::Translate { - translation, - content: Box::new(Primitive::Clip { - bounds: Rectangle::with_size(size), - content: Box::new(Primitive::Group { - primitives: text, - }), - }), - }, - ], - }); - } - - /// Applies a translation to the current transform of the [`Frame`]. - #[inline] - pub fn translate(&mut self, translation: Vector) { - self.transforms.current.raw = self - .transforms - .current - .raw - .pre_translate(lyon::math::Vector::new( - translation.x, - translation.y, - )); - self.transforms.current.is_identity = false; - } - - /// Applies a rotation in radians to the current transform of the [`Frame`]. - #[inline] - pub fn rotate(&mut self, angle: f32) { - self.transforms.current.raw = self - .transforms - .current - .raw - .pre_rotate(lyon::math::Angle::radians(angle)); - self.transforms.current.is_identity = false; - } - - /// Applies a scaling to the current transform of the [`Frame`]. - #[inline] - pub fn scale(&mut self, scale: f32) { - self.transforms.current.raw = - self.transforms.current.raw.pre_scale(scale, scale); - self.transforms.current.is_identity = false; - } - - /// Produces the [`Primitive`] representing everything drawn on the [`Frame`]. - pub fn into_primitive(self) -> Primitive { - Primitive::Group { - primitives: self.into_primitives(), - } - } - - fn into_primitives(mut self) -> Vec { - for buffer in self.buffers.stack { - match buffer { - Buffer::Solid(buffer) => { - if !buffer.indices.is_empty() { - self.primitives.push(Primitive::SolidMesh { - buffers: primitive::Mesh2D { - vertices: buffer.vertices, - indices: buffer.indices, - }, - size: self.size, - }) - } - } - Buffer::Gradient(buffer, gradient) => { - if !buffer.indices.is_empty() { - self.primitives.push(Primitive::GradientMesh { - buffers: primitive::Mesh2D { - vertices: buffer.vertices, - indices: buffer.indices, - }, - size: self.size, - gradient, - }) - } - } - } - } - - self.primitives - } -} - -struct Vertex2DBuilder; - -impl tessellation::FillVertexConstructor - for Vertex2DBuilder -{ - fn new_vertex( - &mut self, - vertex: tessellation::FillVertex<'_>, - ) -> primitive::Vertex2D { - let position = vertex.position(); - - primitive::Vertex2D { - position: [position.x, position.y], - } - } -} - -impl tessellation::StrokeVertexConstructor - for Vertex2DBuilder -{ - fn new_vertex( - &mut self, - vertex: tessellation::StrokeVertex<'_, '_>, - ) -> primitive::Vertex2D { - let position = vertex.position(); - - primitive::Vertex2D { - position: [position.x, position.y], - } - } -} - -struct TriangleVertex2DBuilder([f32; 4]); - -impl tessellation::FillVertexConstructor - for TriangleVertex2DBuilder -{ - fn new_vertex( - &mut self, - vertex: tessellation::FillVertex<'_>, - ) -> primitive::ColoredVertex2D { - let position = vertex.position(); - - primitive::ColoredVertex2D { - position: [position.x, position.y], - color: self.0, - } - } -} - -impl tessellation::StrokeVertexConstructor - for TriangleVertex2DBuilder -{ - fn new_vertex( - &mut self, - vertex: tessellation::StrokeVertex<'_, '_>, - ) -> primitive::ColoredVertex2D { - let position = vertex.position(); - - primitive::ColoredVertex2D { - position: [position.x, position.y], - color: self.0, - } - } -} - -fn into_line_join(line_join: LineJoin) -> lyon::tessellation::LineJoin { - match line_join { - LineJoin::Miter => lyon::tessellation::LineJoin::Miter, - LineJoin::Round => lyon::tessellation::LineJoin::Round, - LineJoin::Bevel => lyon::tessellation::LineJoin::Bevel, - } -} - -fn into_line_cap(line_cap: LineCap) -> lyon::tessellation::LineCap { - match line_cap { - LineCap::Butt => lyon::tessellation::LineCap::Butt, - LineCap::Square => lyon::tessellation::LineCap::Square, - LineCap::Round => lyon::tessellation::LineCap::Round, - } -} - -fn into_fill_rule(rule: fill::Rule) -> lyon::tessellation::FillRule { - match rule { - fill::Rule::NonZero => lyon::tessellation::FillRule::NonZero, - fill::Rule::EvenOdd => lyon::tessellation::FillRule::EvenOdd, - } -} - -pub(super) fn dashed(path: &Path, line_dash: LineDash<'_>) -> Path { - use lyon::algorithms::walk::{ - walk_along_path, RepeatedPattern, WalkerEvent, - }; - use lyon::path::iterator::PathIterator; - - Path::new(|builder| { - let segments_odd = (line_dash.segments.len() % 2 == 1) - .then(|| [line_dash.segments, line_dash.segments].concat()); - - let mut draw_line = false; - - walk_along_path( - path.raw().iter().flattened(0.01), - 0.0, - lyon::tessellation::StrokeOptions::DEFAULT_TOLERANCE, - &mut RepeatedPattern { - callback: |event: WalkerEvent<'_>| { - let point = Point { - x: event.position.x, - y: event.position.y, - }; - - if draw_line { - builder.line_to(point); - } else { - builder.move_to(point); - } - - draw_line = !draw_line; - - true - }, - index: line_dash.offset, - intervals: segments_odd - .as_deref() - .unwrap_or(line_dash.segments), - }, - ); - }) -} diff --git a/wgpu/src/widget/canvas/geometry.rs b/wgpu/src/widget/canvas/geometry.rs deleted file mode 100644 index e8ac621d..00000000 --- a/wgpu/src/widget/canvas/geometry.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::Primitive; - -/// A bunch of shapes that can be drawn. -/// -/// [`Geometry`] can be easily generated with a [`Frame`] or stored in a -/// [`Cache`]. -/// -/// [`Frame`]: crate::widget::canvas::Frame -/// [`Cache`]: crate::widget::canvas::Cache -#[derive(Debug, Clone)] -pub struct Geometry(Primitive); - -impl Geometry { - pub(crate) fn from_primitive(primitive: Primitive) -> Self { - Self(primitive) - } - - /// Turns the [`Geometry`] into a [`Primitive`]. - /// - /// This can be useful if you are building a custom widget. - pub fn into_primitive(self) -> Primitive { - self.0 - } -} -- cgit From 6cc48b5c62bac287b8f9f1c79c1fb7486c51b18f Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 3 Mar 2023 04:57:55 +0100 Subject: Move `Canvas` and `QRCode` to `iced` crate Rename `canvas` modules to `geometry` in graphics subcrates --- Cargo.toml | 9 +- examples/arc/src/main.rs | 2 +- examples/bezier_tool/src/main.rs | 2 +- examples/color_palette/src/main.rs | 2 +- examples/game_of_life/src/main.rs | 2 +- examples/sierpinski_triangle/src/main.rs | 2 +- examples/solar_system/src/main.rs | 2 +- graphics/Cargo.toml | 6 +- graphics/src/backend.rs | 2 - graphics/src/geometry.rs | 36 ++ graphics/src/geometry/fill.rs | 63 ++++ graphics/src/geometry/path.rs | 67 ++++ graphics/src/geometry/path/arc.rs | 42 +++ graphics/src/geometry/path/builder.rs | 192 ++++++++++ graphics/src/geometry/stroke.rs | 106 ++++++ graphics/src/geometry/style.rs | 23 ++ graphics/src/geometry/text.rs | 57 +++ graphics/src/lib.rs | 6 + graphics/src/renderer.rs | 8 +- native/Cargo.toml | 5 - native/src/widget/canvas.rs | 259 ------------- native/src/widget/canvas/cursor.rs | 64 ---- native/src/widget/canvas/event.rs | 21 -- native/src/widget/canvas/fill.rs | 64 ---- native/src/widget/canvas/path.rs | 67 ---- native/src/widget/canvas/path/arc.rs | 42 --- native/src/widget/canvas/path/builder.rs | 192 ---------- native/src/widget/canvas/program.rs | 108 ------ native/src/widget/canvas/stroke.rs | 106 ------ native/src/widget/canvas/style.rs | 24 -- native/src/widget/canvas/text.rs | 57 --- renderer/Cargo.toml | 8 +- renderer/src/backend.rs | 4 +- renderer/src/geometry.rs | 168 +++++++++ renderer/src/geometry/cache.rs | 85 +++++ renderer/src/lib.rs | 13 +- renderer/src/widget/canvas.rs | 177 --------- renderer/src/widget/canvas/cache.rs | 85 ----- renderer/src/widget/qr_code.rs | 301 --------------- src/widget.rs | 8 +- src/widget/canvas.rs | 238 ++++++++++++ src/widget/canvas/cursor.rs | 64 ++++ src/widget/canvas/event.rs | 21 ++ src/widget/canvas/program.rs | 108 ++++++ src/widget/qr_code.rs | 300 +++++++++++++++ tiny_skia/Cargo.toml | 2 +- tiny_skia/src/backend.rs | 2 - tiny_skia/src/canvas.rs | 287 --------------- tiny_skia/src/geometry.rs | 287 +++++++++++++++ tiny_skia/src/lib.rs | 4 +- wgpu/Cargo.toml | 2 +- wgpu/src/backend.rs | 2 - wgpu/src/canvas.rs | 608 ------------------------------- wgpu/src/geometry.rs | 608 +++++++++++++++++++++++++++++++ wgpu/src/lib.rs | 4 +- 55 files changed, 2508 insertions(+), 2516 deletions(-) create mode 100644 graphics/src/geometry.rs create mode 100644 graphics/src/geometry/fill.rs create mode 100644 graphics/src/geometry/path.rs create mode 100644 graphics/src/geometry/path/arc.rs create mode 100644 graphics/src/geometry/path/builder.rs create mode 100644 graphics/src/geometry/stroke.rs create mode 100644 graphics/src/geometry/style.rs create mode 100644 graphics/src/geometry/text.rs delete mode 100644 native/src/widget/canvas.rs delete mode 100644 native/src/widget/canvas/cursor.rs delete mode 100644 native/src/widget/canvas/event.rs delete mode 100644 native/src/widget/canvas/fill.rs delete mode 100644 native/src/widget/canvas/path.rs delete mode 100644 native/src/widget/canvas/path/arc.rs delete mode 100644 native/src/widget/canvas/path/builder.rs delete mode 100644 native/src/widget/canvas/program.rs delete mode 100644 native/src/widget/canvas/stroke.rs delete mode 100644 native/src/widget/canvas/style.rs delete mode 100644 native/src/widget/canvas/text.rs create mode 100644 renderer/src/geometry.rs create mode 100644 renderer/src/geometry/cache.rs delete mode 100644 renderer/src/widget/canvas.rs delete mode 100644 renderer/src/widget/canvas/cache.rs delete mode 100644 renderer/src/widget/qr_code.rs create mode 100644 src/widget/canvas.rs create mode 100644 src/widget/canvas/cursor.rs create mode 100644 src/widget/canvas/event.rs create mode 100644 src/widget/canvas/program.rs create mode 100644 src/widget/qr_code.rs delete mode 100644 tiny_skia/src/canvas.rs create mode 100644 tiny_skia/src/geometry.rs delete mode 100644 wgpu/src/canvas.rs create mode 100644 wgpu/src/geometry.rs diff --git a/Cargo.toml b/Cargo.toml index 942966e5..28938df9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,9 +17,9 @@ image = ["iced_renderer/image", "image_rs"] # Enables the `Svg` widget svg = ["iced_renderer/svg"] # Enables the `Canvas` widget -canvas = ["iced_renderer/canvas"] +canvas = ["iced_renderer/geometry"] # Enables the `QRCode` widget -qr_code = ["iced_renderer/qr_code"] +qr_code = ["canvas", "qrcode"] # Enables a debug view in native platforms (press F12) debug = ["iced_winit/debug"] # Enables `tokio` as the `executor::Default` on native platforms @@ -69,6 +69,11 @@ version = "0.24" package = "image" optional = true +[dependencies.qrcode] +version = "0.12" +optional = true +default-features = false + [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] features = ["image", "svg", "canvas", "qr_code"] diff --git a/examples/arc/src/main.rs b/examples/arc/src/main.rs index d71ba6f6..80ad0b5b 100644 --- a/examples/arc/src/main.rs +++ b/examples/arc/src/main.rs @@ -69,7 +69,7 @@ impl Application for Arc { } } -impl canvas::Program for Arc { +impl canvas::Program for Arc { type State = (); fn draw( diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index 5bb463c3..f1c83a16 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -92,7 +92,7 @@ mod bezier { curves: &'a [Curve], } - impl<'a> canvas::Program for Bezier<'a> { + impl<'a> canvas::Program for Bezier<'a> { type State = Option; fn update( diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs index 1109a883..5c4304ee 100644 --- a/examples/color_palette/src/main.rs +++ b/examples/color_palette/src/main.rs @@ -237,7 +237,7 @@ impl Theme { } } -impl canvas::Program for Theme { +impl canvas::Program for Theme { type State = (); fn draw( diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index cdb33aca..eab8908b 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -393,7 +393,7 @@ mod grid { } } - impl canvas::Program for Grid { + impl canvas::Program for Grid { type State = Interaction; fn update( diff --git a/examples/sierpinski_triangle/src/main.rs b/examples/sierpinski_triangle/src/main.rs index e85f8391..4faac6d6 100644 --- a/examples/sierpinski_triangle/src/main.rs +++ b/examples/sierpinski_triangle/src/main.rs @@ -97,7 +97,7 @@ struct SierpinskiGraph { cache: canvas::Cache, } -impl canvas::Program for SierpinskiGraph { +impl canvas::Program for SierpinskiGraph { type State = (); fn update( diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index 0023a69b..f2606feb 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -150,7 +150,7 @@ impl State { } } -impl canvas::Program for State { +impl canvas::Program for State { type State = (); fn draw( diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index 62e67cf8..98e6f474 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -24,7 +24,7 @@ bmp = ["image_rs/bmp"] hdr = ["image_rs/hdr"] dds = ["image_rs/dds"] farbfeld = ["image_rs/farbfeld"] -canvas = ["iced_native/canvas"] +geometry = ["lyon_path"] opengl = [] image_rs = ["kamadak-exif"] @@ -65,6 +65,10 @@ optional = true version = "0.5" optional = true +[dependencies.lyon_path] +version = "1" +optional = true + [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] all-features = true diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs index c44372e8..8658cffe 100644 --- a/graphics/src/backend.rs +++ b/graphics/src/backend.rs @@ -10,8 +10,6 @@ use std::borrow::Cow; /// /// [`Renderer`]: crate::Renderer pub trait Backend { - type Geometry: Into; - /// Trims the measurements cache. /// /// This method is currently necessary to properly trim the text cache in diff --git a/graphics/src/geometry.rs b/graphics/src/geometry.rs new file mode 100644 index 00000000..29ac84d6 --- /dev/null +++ b/graphics/src/geometry.rs @@ -0,0 +1,36 @@ +//! Draw 2D graphics for your users. +//! +//! A [`Canvas`] widget can be used to draw different kinds of 2D shapes in a +//! [`Frame`]. It can be used for animation, data visualization, game graphics, +//! and more! +pub mod fill; +pub mod path; +pub mod stroke; + +mod style; +mod text; + +pub use fill::Fill; +pub use path::Path; +pub use stroke::{LineCap, LineDash, LineJoin, Stroke}; +pub use style::Style; +pub use text::Text; + +pub use iced_native::gradient::{self, Gradient}; + +use crate::Primitive; + +#[derive(Debug, Clone)] +pub struct Geometry(pub Primitive); + +impl From for Primitive { + fn from(geometry: Geometry) -> Self { + geometry.0 + } +} + +pub trait Renderer: iced_native::Renderer { + type Geometry; + + fn draw(&mut self, geometry: Vec); +} diff --git a/graphics/src/geometry/fill.rs b/graphics/src/geometry/fill.rs new file mode 100644 index 00000000..109d5e99 --- /dev/null +++ b/graphics/src/geometry/fill.rs @@ -0,0 +1,63 @@ +//! Fill [crate::widget::canvas::Geometry] with a certain style. +use crate::{Color, Gradient}; + +pub use crate::geometry::Style; + +/// The style used to fill geometry. +#[derive(Debug, Clone)] +pub struct Fill { + /// The color or gradient of the fill. + /// + /// By default, it is set to [`Style::Solid`] with [`Color::BLACK`]. + pub style: Style, + + /// The fill rule defines how to determine what is inside and what is + /// outside of a shape. + /// + /// See the [SVG specification][1] for more details. + /// + /// By default, it is set to `NonZero`. + /// + /// [1]: https://www.w3.org/TR/SVG/painting.html#FillRuleProperty + pub rule: Rule, +} + +impl Default for Fill { + fn default() -> Self { + Self { + style: Style::Solid(Color::BLACK), + rule: Rule::NonZero, + } + } +} + +impl From for Fill { + fn from(color: Color) -> Fill { + Fill { + style: Style::Solid(color), + ..Fill::default() + } + } +} + +impl From for Fill { + fn from(gradient: Gradient) -> Self { + Fill { + style: Style::Gradient(gradient), + ..Default::default() + } + } +} + +/// The fill rule defines how to determine what is inside and what is outside of +/// a shape. +/// +/// See the [SVG specification][1]. +/// +/// [1]: https://www.w3.org/TR/SVG/painting.html#FillRuleProperty +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(missing_docs)] +pub enum Rule { + NonZero, + EvenOdd, +} diff --git a/graphics/src/geometry/path.rs b/graphics/src/geometry/path.rs new file mode 100644 index 00000000..30c387c5 --- /dev/null +++ b/graphics/src/geometry/path.rs @@ -0,0 +1,67 @@ +//! Build different kinds of 2D shapes. +pub mod arc; + +mod builder; + +#[doc(no_inline)] +pub use arc::Arc; +pub use builder::Builder; + +pub use lyon_path; + +use crate::{Point, Size}; + +/// An immutable set of points that may or may not be connected. +/// +/// A single [`Path`] can represent different kinds of 2D shapes! +#[derive(Debug, Clone)] +pub struct Path { + raw: lyon_path::Path, +} + +impl Path { + /// Creates a new [`Path`] with the provided closure. + /// + /// Use the [`Builder`] to configure your [`Path`]. + pub fn new(f: impl FnOnce(&mut Builder)) -> Self { + let mut builder = Builder::new(); + + // TODO: Make it pure instead of side-effect-based (?) + f(&mut builder); + + builder.build() + } + + /// Creates a new [`Path`] representing a line segment given its starting + /// and end points. + pub fn line(from: Point, to: Point) -> Self { + Self::new(|p| { + p.move_to(from); + p.line_to(to); + }) + } + + /// Creates a new [`Path`] representing a rectangle given its top-left + /// corner coordinate and its `Size`. + pub fn rectangle(top_left: Point, size: Size) -> Self { + Self::new(|p| p.rectangle(top_left, size)) + } + + /// Creates a new [`Path`] representing a circle given its center + /// coordinate and its radius. + pub fn circle(center: Point, radius: f32) -> Self { + Self::new(|p| p.circle(center, radius)) + } + + #[inline] + pub fn raw(&self) -> &lyon_path::Path { + &self.raw + } + + #[inline] + pub fn transform(&self, transform: &lyon_path::math::Transform) -> Path { + Path { + raw: self.raw.clone().transformed(transform), + } + } +} diff --git a/graphics/src/geometry/path/arc.rs b/graphics/src/geometry/path/arc.rs new file mode 100644 index 00000000..e0747d3e --- /dev/null +++ b/graphics/src/geometry/path/arc.rs @@ -0,0 +1,42 @@ +//! Build and draw curves. +use crate::{Point, Vector}; + +/// A segment of a differentiable curve. +#[derive(Debug, Clone, Copy)] +pub struct Arc { + /// The center of the arc. + pub center: Point, + /// The radius of the arc. + pub radius: f32, + /// The start of the segment's angle, clockwise rotation. + pub start_angle: f32, + /// The end of the segment's angle, clockwise rotation. + pub end_angle: f32, +} + +/// An elliptical [`Arc`]. +#[derive(Debug, Clone, Copy)] +pub struct Elliptical { + /// The center of the arc. + pub center: Point, + /// The radii of the arc's ellipse, defining its axes. + pub radii: Vector, + /// The rotation of the arc's ellipse. + pub rotation: f32, + /// The start of the segment's angle, clockwise rotation. + pub start_angle: f32, + /// The end of the segment's angle, clockwise rotation. + pub end_angle: f32, +} + +impl From for Elliptical { + fn from(arc: Arc) -> Elliptical { + Elliptical { + center: arc.center, + radii: Vector::new(arc.radius, arc.radius), + rotation: 0.0, + start_angle: arc.start_angle, + end_angle: arc.end_angle, + } + } +} diff --git a/graphics/src/geometry/path/builder.rs b/graphics/src/geometry/path/builder.rs new file mode 100644 index 00000000..4a9c5e36 --- /dev/null +++ b/graphics/src/geometry/path/builder.rs @@ -0,0 +1,192 @@ +use crate::geometry::path::{arc, Arc, Path}; +use crate::{Point, Size}; + +use lyon_path::builder::{self, SvgPathBuilder}; +use lyon_path::geom; +use lyon_path::math; + +/// A [`Path`] builder. +/// +/// Once a [`Path`] is built, it can no longer be mutated. +#[allow(missing_debug_implementations)] +pub struct Builder { + raw: builder::WithSvg, +} + +impl Builder { + /// Creates a new [`Builder`]. + pub fn new() -> Builder { + Builder { + raw: lyon_path::Path::builder().with_svg(), + } + } + + /// Moves the starting point of a new sub-path to the given `Point`. + #[inline] + pub fn move_to(&mut self, point: Point) { + let _ = self.raw.move_to(math::Point::new(point.x, point.y)); + } + + /// Connects the last point in the [`Path`] to the given `Point` with a + /// straight line. + #[inline] + pub fn line_to(&mut self, point: Point) { + let _ = self.raw.line_to(math::Point::new(point.x, point.y)); + } + + /// Adds an [`Arc`] to the [`Path`] from `start_angle` to `end_angle` in + /// a clockwise direction. + #[inline] + pub fn arc(&mut self, arc: Arc) { + self.ellipse(arc.into()); + } + + /// Adds a circular arc to the [`Path`] with the given control points and + /// radius. + /// + /// This essentially draws a straight line segment from the current + /// position to `a`, but fits a circular arc of `radius` tangent to that + /// segment and tangent to the line between `a` and `b`. + /// + /// With another `.line_to(b)`, the result will be a path connecting the + /// starting point and `b` with straight line segments towards `a` and a + /// circular arc smoothing out the corner at `a`. + /// + /// See [the HTML5 specification of `arcTo`](https://html.spec.whatwg.org/multipage/canvas.html#building-paths:dom-context-2d-arcto) + /// for more details and examples. + pub fn arc_to(&mut self, a: Point, b: Point, radius: f32) { + let start = self.raw.current_position(); + let mid = math::Point::new(a.x, a.y); + let end = math::Point::new(b.x, b.y); + + if start == mid || mid == end || radius == 0.0 { + let _ = self.raw.line_to(mid); + return; + } + + let double_area = start.x * (mid.y - end.y) + + mid.x * (end.y - start.y) + + end.x * (start.y - mid.y); + + if double_area == 0.0 { + let _ = self.raw.line_to(mid); + return; + } + + let to_start = (start - mid).normalize(); + let to_end = (end - mid).normalize(); + + let inner_angle = to_start.dot(to_end).acos(); + + let origin_angle = inner_angle / 2.0; + + let origin_adjacent = radius / origin_angle.tan(); + + let arc_start = mid + to_start * origin_adjacent; + let arc_end = mid + to_end * origin_adjacent; + + let sweep = to_start.cross(to_end) < 0.0; + + let _ = self.raw.line_to(arc_start); + + self.raw.arc_to( + math::Vector::new(radius, radius), + math::Angle::radians(0.0), + lyon_path::ArcFlags { + large_arc: false, + sweep, + }, + arc_end, + ); + } + + /// Adds an ellipse to the [`Path`] using a clockwise direction. + pub fn ellipse(&mut self, arc: arc::Elliptical) { + let arc = geom::Arc { + center: math::Point::new(arc.center.x, arc.center.y), + radii: math::Vector::new(arc.radii.x, arc.radii.y), + x_rotation: math::Angle::radians(arc.rotation), + start_angle: math::Angle::radians(arc.start_angle), + sweep_angle: math::Angle::radians(arc.end_angle - arc.start_angle), + }; + + let _ = self.raw.move_to(arc.sample(0.0)); + + arc.for_each_quadratic_bezier(&mut |curve| { + let _ = self.raw.quadratic_bezier_to(curve.ctrl, curve.to); + }); + } + + /// Adds a cubic Bézier curve to the [`Path`] given its two control points + /// and its end point. + #[inline] + pub fn bezier_curve_to( + &mut self, + control_a: Point, + control_b: Point, + to: Point, + ) { + let _ = self.raw.cubic_bezier_to( + math::Point::new(control_a.x, control_a.y), + math::Point::new(control_b.x, control_b.y), + math::Point::new(to.x, to.y), + ); + } + + /// Adds a quadratic Bézier curve to the [`Path`] given its control point + /// and its end point. + #[inline] + pub fn quadratic_curve_to(&mut self, control: Point, to: Point) { + let _ = self.raw.quadratic_bezier_to( + math::Point::new(control.x, control.y), + math::Point::new(to.x, to.y), + ); + } + + /// Adds a rectangle to the [`Path`] given its top-left corner coordinate + /// and its `Size`. + #[inline] + pub fn rectangle(&mut self, top_left: Point, size: Size) { + self.move_to(top_left); + self.line_to(Point::new(top_left.x + size.width, top_left.y)); + self.line_to(Point::new( + top_left.x + size.width, + top_left.y + size.height, + )); + self.line_to(Point::new(top_left.x, top_left.y + size.height)); + self.close(); + } + + /// Adds a circle to the [`Path`] given its center coordinate and its + /// radius. + #[inline] + pub fn circle(&mut self, center: Point, radius: f32) { + self.arc(Arc { + center, + radius, + start_angle: 0.0, + end_angle: 2.0 * std::f32::consts::PI, + }); + } + + /// Closes the current sub-path in the [`Path`] with a straight line to + /// the starting point. + #[inline] + pub fn close(&mut self) { + self.raw.close() + } + + /// Builds the [`Path`] of this [`Builder`]. + #[inline] + pub fn build(self) -> Path { + Path { + raw: self.raw.build(), + } + } +} + +impl Default for Builder { + fn default() -> Self { + Self::new() + } +} diff --git a/graphics/src/geometry/stroke.rs b/graphics/src/geometry/stroke.rs new file mode 100644 index 00000000..b551a9c9 --- /dev/null +++ b/graphics/src/geometry/stroke.rs @@ -0,0 +1,106 @@ +//! Create lines from a [crate::widget::canvas::Path] and assigns them various attributes/styles. +pub use crate::geometry::Style; + +use crate::Color; + +/// The style of a stroke. +#[derive(Debug, Clone)] +pub struct Stroke<'a> { + /// The color or gradient of the stroke. + /// + /// By default, it is set to a [`Style::Solid`] with [`Color::BLACK`]. + pub style: Style, + /// The distance between the two edges of the stroke. + pub width: f32, + /// The shape to be used at the end of open subpaths when they are stroked. + pub line_cap: LineCap, + /// The shape to be used at the corners of paths or basic shapes when they + /// are stroked. + pub line_join: LineJoin, + /// The dash pattern used when stroking the line. + pub line_dash: LineDash<'a>, +} + +impl<'a> Stroke<'a> { + /// Sets the color of the [`Stroke`]. + pub fn with_color(self, color: Color) -> Self { + Stroke { + style: Style::Solid(color), + ..self + } + } + + /// Sets the width of the [`Stroke`]. + pub fn with_width(self, width: f32) -> Self { + Stroke { width, ..self } + } + + /// Sets the [`LineCap`] of the [`Stroke`]. + pub fn with_line_cap(self, line_cap: LineCap) -> Self { + Stroke { line_cap, ..self } + } + + /// Sets the [`LineJoin`] of the [`Stroke`]. + pub fn with_line_join(self, line_join: LineJoin) -> Self { + Stroke { line_join, ..self } + } +} + +impl<'a> Default for Stroke<'a> { + fn default() -> Self { + Stroke { + style: Style::Solid(Color::BLACK), + width: 1.0, + line_cap: LineCap::default(), + line_join: LineJoin::default(), + line_dash: LineDash::default(), + } + } +} + +/// The shape used at the end of open subpaths when they are stroked. +#[derive(Debug, Clone, Copy)] +pub enum LineCap { + /// The stroke for each sub-path does not extend beyond its two endpoints. + Butt, + /// At the end of each sub-path, the shape representing the stroke will be + /// extended by a square. + Square, + /// At the end of each sub-path, the shape representing the stroke will be + /// extended by a semicircle. + Round, +} + +impl Default for LineCap { + fn default() -> LineCap { + LineCap::Butt + } +} + +/// The shape used at the corners of paths or basic shapes when they are +/// stroked. +#[derive(Debug, Clone, Copy)] +pub enum LineJoin { + /// A sharp corner. + Miter, + /// A round corner. + Round, + /// A bevelled corner. + Bevel, +} + +impl Default for LineJoin { + fn default() -> LineJoin { + LineJoin::Miter + } +} + +/// The dash pattern used when stroking the line. +#[derive(Debug, Clone, Copy, Default)] +pub struct LineDash<'a> { + /// The alternating lengths of lines and gaps which describe the pattern. + pub segments: &'a [f32], + + /// The offset of [`LineDash::segments`] to start the pattern. + pub offset: usize, +} diff --git a/graphics/src/geometry/style.rs b/graphics/src/geometry/style.rs new file mode 100644 index 00000000..6794f2e7 --- /dev/null +++ b/graphics/src/geometry/style.rs @@ -0,0 +1,23 @@ +use crate::{Color, Gradient}; + +/// The coloring style of some drawing. +#[derive(Debug, Clone, PartialEq)] +pub enum Style { + /// A solid [`Color`]. + Solid(Color), + + /// A [`Gradient`] color. + Gradient(Gradient), +} + +impl From for Style { + fn from(color: Color) -> Self { + Self::Solid(color) + } +} + +impl From for Style { + fn from(gradient: Gradient) -> Self { + Self::Gradient(gradient) + } +} diff --git a/graphics/src/geometry/text.rs b/graphics/src/geometry/text.rs new file mode 100644 index 00000000..8c0b2dfb --- /dev/null +++ b/graphics/src/geometry/text.rs @@ -0,0 +1,57 @@ +use crate::alignment; +use crate::{Color, Font, Point}; + +/// A bunch of text that can be drawn to a canvas +#[derive(Debug, Clone)] +pub struct Text { + /// The contents of the text + pub content: String, + /// The position of the text relative to the alignment properties. + /// By default, this position will be relative to the top-left corner coordinate meaning that + /// if the horizontal and vertical alignments are unchanged, this property will tell where the + /// top-left corner of the text should be placed. + /// By changing the horizontal_alignment and vertical_alignment properties, you are are able to + /// change what part of text is placed at this positions. + /// For example, when the horizontal_alignment and vertical_alignment are set to Center, the + /// center of the text will be placed at the given position NOT the top-left coordinate. + pub position: Point, + /// The color of the text + pub color: Color, + /// The size of the text + pub size: f32, + /// The font of the text + pub font: Font, + /// The horizontal alignment of the text + pub horizontal_alignment: alignment::Horizontal, + /// The vertical alignment of the text + pub vertical_alignment: alignment::Vertical, +} + +impl Default for Text { + fn default() -> Text { + Text { + content: String::new(), + position: Point::ORIGIN, + color: Color::BLACK, + size: 16.0, + font: Font::SansSerif, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + } + } +} + +impl From for Text { + fn from(content: String) -> Text { + Text { + content, + ..Default::default() + } + } +} + +impl From<&str> for Text { + fn from(content: &str) -> Text { + String::from(content).into() + } +} diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index 576b2d78..e56f8ad8 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -33,6 +33,9 @@ pub mod primitive; pub mod renderer; pub mod window; +#[cfg(feature = "geometry")] +pub mod geometry; + pub use antialiasing::Antialiasing; pub use backend::Backend; pub use error::Error; @@ -41,6 +44,9 @@ pub use renderer::Renderer; pub use transformation::Transformation; pub use viewport::Viewport; +#[cfg(feature = "geometry")] +pub use geometry::Geometry; + pub use iced_native::alignment; pub use iced_native::text; pub use iced_native::{ diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index 793ee7d7..cb57f429 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -215,15 +215,15 @@ where } } -#[cfg(feature = "canvas")] -impl iced_native::widget::canvas::Renderer for Renderer +#[cfg(feature = "geometry")] +impl crate::geometry::Renderer for Renderer where B: Backend, { - type Geometry = B::Geometry; + type Geometry = crate::Geometry; fn draw(&mut self, layers: Vec) { self.primitives - .extend(layers.into_iter().map(B::Geometry::into)); + .extend(layers.into_iter().map(crate::Geometry::into)); } } diff --git a/native/Cargo.toml b/native/Cargo.toml index 23533e33..1eedf0da 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -8,7 +8,6 @@ license = "MIT" repository = "https://github.com/iced-rs/iced" [features] -canvas = ["lyon_path"] debug = [] [dependencies] @@ -29,7 +28,3 @@ features = ["thread-pool"] [dependencies.iced_style] version = "0.7" path = "../style" - -[dependencies.lyon_path] -version = "1" -optional = true diff --git a/native/src/widget/canvas.rs b/native/src/widget/canvas.rs deleted file mode 100644 index 8a9addd2..00000000 --- a/native/src/widget/canvas.rs +++ /dev/null @@ -1,259 +0,0 @@ -//! Draw 2D graphics for your users. -//! -//! A [`Canvas`] widget can be used to draw different kinds of 2D shapes in a -//! [`Frame`]. It can be used for animation, data visualization, game graphics, -//! and more! -pub mod event; -pub mod fill; -pub mod path; -pub mod stroke; - -mod cursor; -mod program; -mod style; -mod text; - -pub use crate::gradient::{self, Gradient}; -pub use cursor::Cursor; -pub use event::Event; -pub use fill::Fill; -pub use path::Path; -pub use program::Program; -pub use stroke::{LineCap, LineDash, LineJoin, Stroke}; -pub use style::Style; -pub use text::Text; - -use crate::layout::{self, Layout}; -use crate::mouse; -use crate::renderer; -use crate::widget::tree::{self, Tree}; -use crate::{ - Clipboard, Element, Length, Point, Rectangle, Shell, Size, Vector, Widget, -}; - -use std::marker::PhantomData; - -/// A widget capable of drawing 2D graphics. -/// -/// ## Drawing a simple circle -/// If you want to get a quick overview, here's how we can draw a simple circle: -/// -/// ```no_run -/// # mod iced { -/// # pub mod widget { -/// # pub use iced_graphics::widget::canvas; -/// # } -/// # pub use iced_native::{Color, Rectangle, Theme}; -/// # } -/// use iced::widget::canvas::{self, Canvas, Cursor, Fill, Frame, Geometry, Path, Program}; -/// use iced::{Color, Rectangle, Theme}; -/// -/// // First, we define the data we need for drawing -/// #[derive(Debug)] -/// struct Circle { -/// radius: f32, -/// } -/// -/// // Then, we implement the `Program` trait -/// impl Program<()> for Circle { -/// type State = (); -/// -/// fn draw(&self, _state: &(), _theme: &Theme, bounds: Rectangle, _cursor: Cursor) -> Vec{ -/// // We prepare a new `Frame` -/// let mut frame = Frame::new(bounds.size()); -/// -/// // We create a `Path` representing a simple circle -/// let circle = Path::circle(frame.center(), self.radius); -/// -/// // And fill it with some color -/// frame.fill(&circle, Color::BLACK); -/// -/// // Finally, we produce the geometry -/// vec![frame.into_geometry()] -/// } -/// } -/// -/// // Finally, we simply use our `Circle` to create the `Canvas`! -/// let canvas = Canvas::new(Circle { radius: 50.0 }); -/// ``` -#[derive(Debug)] -pub struct Canvas -where - Renderer: self::Renderer, - P: Program, -{ - width: Length, - height: Length, - program: P, - message_: PhantomData, - theme_: PhantomData, -} - -impl Canvas -where - Renderer: self::Renderer, - P: Program, -{ - const DEFAULT_SIZE: f32 = 100.0; - - /// Creates a new [`Canvas`]. - pub fn new(program: P) -> Self { - Canvas { - width: Length::Fixed(Self::DEFAULT_SIZE), - height: Length::Fixed(Self::DEFAULT_SIZE), - program, - message_: PhantomData, - theme_: PhantomData, - } - } - - /// Sets the width of the [`Canvas`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the height of the [`Canvas`]. - pub fn height(mut self, height: impl Into) -> Self { - self.height = height.into(); - self - } -} - -impl Widget - for Canvas -where - Renderer: self::Renderer, - P: Program, -{ - fn tag(&self) -> tree::Tag { - struct Tag(T); - tree::Tag::of::>() - } - - fn state(&self) -> tree::State { - tree::State::new(P::State::default()) - } - - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height - } - - fn layout( - &self, - _renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - let size = limits.resolve(Size::ZERO); - - layout::Node::new(size) - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: crate::Event, - layout: Layout<'_>, - cursor_position: Point, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - let bounds = layout.bounds(); - - let canvas_event = match event { - crate::Event::Mouse(mouse_event) => Some(Event::Mouse(mouse_event)), - crate::Event::Touch(touch_event) => Some(Event::Touch(touch_event)), - crate::Event::Keyboard(keyboard_event) => { - Some(Event::Keyboard(keyboard_event)) - } - _ => None, - }; - - let cursor = Cursor::from_window_position(cursor_position); - - if let Some(canvas_event) = canvas_event { - let state = tree.state.downcast_mut::(); - - let (event_status, message) = - self.program.update(state, canvas_event, bounds, cursor); - - if let Some(message) = message { - shell.publish(message); - } - - return event_status; - } - - event::Status::Ignored - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - let bounds = layout.bounds(); - let cursor = Cursor::from_window_position(cursor_position); - let state = tree.state.downcast_ref::(); - - self.program.mouse_interaction(state, bounds, cursor) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - let bounds = layout.bounds(); - - if bounds.width < 1.0 || bounds.height < 1.0 { - return; - } - - let cursor = Cursor::from_window_position(cursor_position); - let state = tree.state.downcast_ref::(); - - renderer.with_translation( - Vector::new(bounds.x, bounds.y), - |renderer| { - renderer.draw( - self.program.draw(state, renderer, theme, bounds, cursor), - ); - }, - ); - } -} - -impl<'a, Message, Renderer, P> From> - for Element<'a, Message, Renderer> -where - Message: 'a, - Renderer: 'a + self::Renderer, - P: Program + 'a, -{ - fn from( - canvas: Canvas, - ) -> Element<'a, Message, Renderer> { - Element::new(canvas) - } -} - -pub trait Renderer: crate::Renderer { - type Geometry; - - fn draw(&mut self, geometry: Vec); -} diff --git a/native/src/widget/canvas/cursor.rs b/native/src/widget/canvas/cursor.rs deleted file mode 100644 index ef6a7771..00000000 --- a/native/src/widget/canvas/cursor.rs +++ /dev/null @@ -1,64 +0,0 @@ -use crate::{Point, Rectangle}; - -/// The mouse cursor state. -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Cursor { - /// The cursor has a defined position. - Available(Point), - - /// The cursor is currently unavailable (i.e. out of bounds or busy). - Unavailable, -} - -impl Cursor { - // TODO: Remove this once this type is used in `iced_native` to encode - // proper cursor availability - pub(crate) fn from_window_position(position: Point) -> Self { - if position.x < 0.0 || position.y < 0.0 { - Cursor::Unavailable - } else { - Cursor::Available(position) - } - } - - /// Returns the absolute position of the [`Cursor`], if available. - pub fn position(&self) -> Option { - match self { - Cursor::Available(position) => Some(*position), - Cursor::Unavailable => None, - } - } - - /// Returns the relative position of the [`Cursor`] inside the given bounds, - /// if available. - /// - /// If the [`Cursor`] is not over the provided bounds, this method will - /// return `None`. - pub fn position_in(&self, bounds: &Rectangle) -> Option { - if self.is_over(bounds) { - self.position_from(bounds.position()) - } else { - None - } - } - - /// Returns the relative position of the [`Cursor`] from the given origin, - /// if available. - pub fn position_from(&self, origin: Point) -> Option { - match self { - Cursor::Available(position) => { - Some(Point::new(position.x - origin.x, position.y - origin.y)) - } - Cursor::Unavailable => None, - } - } - - /// Returns whether the [`Cursor`] is currently over the provided bounds - /// or not. - pub fn is_over(&self, bounds: &Rectangle) -> bool { - match self { - Cursor::Available(position) => bounds.contains(*position), - Cursor::Unavailable => false, - } - } -} diff --git a/native/src/widget/canvas/event.rs b/native/src/widget/canvas/event.rs deleted file mode 100644 index 1d726577..00000000 --- a/native/src/widget/canvas/event.rs +++ /dev/null @@ -1,21 +0,0 @@ -//! Handle events of a canvas. -use crate::keyboard; -use crate::mouse; -use crate::touch; - -pub use crate::event::Status; - -/// A [`Canvas`] event. -/// -/// [`Canvas`]: crate::widget::Canvas -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Event { - /// A mouse event. - Mouse(mouse::Event), - - /// A touch event. - Touch(touch::Event), - - /// A keyboard event. - Keyboard(keyboard::Event), -} diff --git a/native/src/widget/canvas/fill.rs b/native/src/widget/canvas/fill.rs deleted file mode 100644 index 92b1e47e..00000000 --- a/native/src/widget/canvas/fill.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! Fill [crate::widget::canvas::Geometry] with a certain style. -use crate::widget::canvas::Gradient; -use crate::Color; - -pub use crate::widget::canvas::Style; - -/// The style used to fill geometry. -#[derive(Debug, Clone)] -pub struct Fill { - /// The color or gradient of the fill. - /// - /// By default, it is set to [`Style::Solid`] with [`Color::BLACK`]. - pub style: Style, - - /// The fill rule defines how to determine what is inside and what is - /// outside of a shape. - /// - /// See the [SVG specification][1] for more details. - /// - /// By default, it is set to `NonZero`. - /// - /// [1]: https://www.w3.org/TR/SVG/painting.html#FillRuleProperty - pub rule: Rule, -} - -impl Default for Fill { - fn default() -> Self { - Self { - style: Style::Solid(Color::BLACK), - rule: Rule::NonZero, - } - } -} - -impl From for Fill { - fn from(color: Color) -> Fill { - Fill { - style: Style::Solid(color), - ..Fill::default() - } - } -} - -impl From for Fill { - fn from(gradient: Gradient) -> Self { - Fill { - style: Style::Gradient(gradient), - ..Default::default() - } - } -} - -/// The fill rule defines how to determine what is inside and what is outside of -/// a shape. -/// -/// See the [SVG specification][1]. -/// -/// [1]: https://www.w3.org/TR/SVG/painting.html#FillRuleProperty -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[allow(missing_docs)] -pub enum Rule { - NonZero, - EvenOdd, -} diff --git a/native/src/widget/canvas/path.rs b/native/src/widget/canvas/path.rs deleted file mode 100644 index 30c387c5..00000000 --- a/native/src/widget/canvas/path.rs +++ /dev/null @@ -1,67 +0,0 @@ -//! Build different kinds of 2D shapes. -pub mod arc; - -mod builder; - -#[doc(no_inline)] -pub use arc::Arc; -pub use builder::Builder; - -pub use lyon_path; - -use crate::{Point, Size}; - -/// An immutable set of points that may or may not be connected. -/// -/// A single [`Path`] can represent different kinds of 2D shapes! -#[derive(Debug, Clone)] -pub struct Path { - raw: lyon_path::Path, -} - -impl Path { - /// Creates a new [`Path`] with the provided closure. - /// - /// Use the [`Builder`] to configure your [`Path`]. - pub fn new(f: impl FnOnce(&mut Builder)) -> Self { - let mut builder = Builder::new(); - - // TODO: Make it pure instead of side-effect-based (?) - f(&mut builder); - - builder.build() - } - - /// Creates a new [`Path`] representing a line segment given its starting - /// and end points. - pub fn line(from: Point, to: Point) -> Self { - Self::new(|p| { - p.move_to(from); - p.line_to(to); - }) - } - - /// Creates a new [`Path`] representing a rectangle given its top-left - /// corner coordinate and its `Size`. - pub fn rectangle(top_left: Point, size: Size) -> Self { - Self::new(|p| p.rectangle(top_left, size)) - } - - /// Creates a new [`Path`] representing a circle given its center - /// coordinate and its radius. - pub fn circle(center: Point, radius: f32) -> Self { - Self::new(|p| p.circle(center, radius)) - } - - #[inline] - pub fn raw(&self) -> &lyon_path::Path { - &self.raw - } - - #[inline] - pub fn transform(&self, transform: &lyon_path::math::Transform) -> Path { - Path { - raw: self.raw.clone().transformed(transform), - } - } -} diff --git a/native/src/widget/canvas/path/arc.rs b/native/src/widget/canvas/path/arc.rs deleted file mode 100644 index e0747d3e..00000000 --- a/native/src/widget/canvas/path/arc.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! Build and draw curves. -use crate::{Point, Vector}; - -/// A segment of a differentiable curve. -#[derive(Debug, Clone, Copy)] -pub struct Arc { - /// The center of the arc. - pub center: Point, - /// The radius of the arc. - pub radius: f32, - /// The start of the segment's angle, clockwise rotation. - pub start_angle: f32, - /// The end of the segment's angle, clockwise rotation. - pub end_angle: f32, -} - -/// An elliptical [`Arc`]. -#[derive(Debug, Clone, Copy)] -pub struct Elliptical { - /// The center of the arc. - pub center: Point, - /// The radii of the arc's ellipse, defining its axes. - pub radii: Vector, - /// The rotation of the arc's ellipse. - pub rotation: f32, - /// The start of the segment's angle, clockwise rotation. - pub start_angle: f32, - /// The end of the segment's angle, clockwise rotation. - pub end_angle: f32, -} - -impl From for Elliptical { - fn from(arc: Arc) -> Elliptical { - Elliptical { - center: arc.center, - radii: Vector::new(arc.radius, arc.radius), - rotation: 0.0, - start_angle: arc.start_angle, - end_angle: arc.end_angle, - } - } -} diff --git a/native/src/widget/canvas/path/builder.rs b/native/src/widget/canvas/path/builder.rs deleted file mode 100644 index 84fda052..00000000 --- a/native/src/widget/canvas/path/builder.rs +++ /dev/null @@ -1,192 +0,0 @@ -use crate::widget::canvas::path::{arc, Arc, Path}; -use crate::{Point, Size}; - -use lyon_path::builder::{self, SvgPathBuilder}; -use lyon_path::geom; -use lyon_path::math; - -/// A [`Path`] builder. -/// -/// Once a [`Path`] is built, it can no longer be mutated. -#[allow(missing_debug_implementations)] -pub struct Builder { - raw: builder::WithSvg, -} - -impl Builder { - /// Creates a new [`Builder`]. - pub fn new() -> Builder { - Builder { - raw: lyon_path::Path::builder().with_svg(), - } - } - - /// Moves the starting point of a new sub-path to the given `Point`. - #[inline] - pub fn move_to(&mut self, point: Point) { - let _ = self.raw.move_to(math::Point::new(point.x, point.y)); - } - - /// Connects the last point in the [`Path`] to the given `Point` with a - /// straight line. - #[inline] - pub fn line_to(&mut self, point: Point) { - let _ = self.raw.line_to(math::Point::new(point.x, point.y)); - } - - /// Adds an [`Arc`] to the [`Path`] from `start_angle` to `end_angle` in - /// a clockwise direction. - #[inline] - pub fn arc(&mut self, arc: Arc) { - self.ellipse(arc.into()); - } - - /// Adds a circular arc to the [`Path`] with the given control points and - /// radius. - /// - /// This essentially draws a straight line segment from the current - /// position to `a`, but fits a circular arc of `radius` tangent to that - /// segment and tangent to the line between `a` and `b`. - /// - /// With another `.line_to(b)`, the result will be a path connecting the - /// starting point and `b` with straight line segments towards `a` and a - /// circular arc smoothing out the corner at `a`. - /// - /// See [the HTML5 specification of `arcTo`](https://html.spec.whatwg.org/multipage/canvas.html#building-paths:dom-context-2d-arcto) - /// for more details and examples. - pub fn arc_to(&mut self, a: Point, b: Point, radius: f32) { - let start = self.raw.current_position(); - let mid = math::Point::new(a.x, a.y); - let end = math::Point::new(b.x, b.y); - - if start == mid || mid == end || radius == 0.0 { - let _ = self.raw.line_to(mid); - return; - } - - let double_area = start.x * (mid.y - end.y) - + mid.x * (end.y - start.y) - + end.x * (start.y - mid.y); - - if double_area == 0.0 { - let _ = self.raw.line_to(mid); - return; - } - - let to_start = (start - mid).normalize(); - let to_end = (end - mid).normalize(); - - let inner_angle = to_start.dot(to_end).acos(); - - let origin_angle = inner_angle / 2.0; - - let origin_adjacent = radius / origin_angle.tan(); - - let arc_start = mid + to_start * origin_adjacent; - let arc_end = mid + to_end * origin_adjacent; - - let sweep = to_start.cross(to_end) < 0.0; - - let _ = self.raw.line_to(arc_start); - - self.raw.arc_to( - math::Vector::new(radius, radius), - math::Angle::radians(0.0), - lyon_path::ArcFlags { - large_arc: false, - sweep, - }, - arc_end, - ); - } - - /// Adds an ellipse to the [`Path`] using a clockwise direction. - pub fn ellipse(&mut self, arc: arc::Elliptical) { - let arc = geom::Arc { - center: math::Point::new(arc.center.x, arc.center.y), - radii: math::Vector::new(arc.radii.x, arc.radii.y), - x_rotation: math::Angle::radians(arc.rotation), - start_angle: math::Angle::radians(arc.start_angle), - sweep_angle: math::Angle::radians(arc.end_angle - arc.start_angle), - }; - - let _ = self.raw.move_to(arc.sample(0.0)); - - arc.for_each_quadratic_bezier(&mut |curve| { - let _ = self.raw.quadratic_bezier_to(curve.ctrl, curve.to); - }); - } - - /// Adds a cubic Bézier curve to the [`Path`] given its two control points - /// and its end point. - #[inline] - pub fn bezier_curve_to( - &mut self, - control_a: Point, - control_b: Point, - to: Point, - ) { - let _ = self.raw.cubic_bezier_to( - math::Point::new(control_a.x, control_a.y), - math::Point::new(control_b.x, control_b.y), - math::Point::new(to.x, to.y), - ); - } - - /// Adds a quadratic Bézier curve to the [`Path`] given its control point - /// and its end point. - #[inline] - pub fn quadratic_curve_to(&mut self, control: Point, to: Point) { - let _ = self.raw.quadratic_bezier_to( - math::Point::new(control.x, control.y), - math::Point::new(to.x, to.y), - ); - } - - /// Adds a rectangle to the [`Path`] given its top-left corner coordinate - /// and its `Size`. - #[inline] - pub fn rectangle(&mut self, top_left: Point, size: Size) { - self.move_to(top_left); - self.line_to(Point::new(top_left.x + size.width, top_left.y)); - self.line_to(Point::new( - top_left.x + size.width, - top_left.y + size.height, - )); - self.line_to(Point::new(top_left.x, top_left.y + size.height)); - self.close(); - } - - /// Adds a circle to the [`Path`] given its center coordinate and its - /// radius. - #[inline] - pub fn circle(&mut self, center: Point, radius: f32) { - self.arc(Arc { - center, - radius, - start_angle: 0.0, - end_angle: 2.0 * std::f32::consts::PI, - }); - } - - /// Closes the current sub-path in the [`Path`] with a straight line to - /// the starting point. - #[inline] - pub fn close(&mut self) { - self.raw.close() - } - - /// Builds the [`Path`] of this [`Builder`]. - #[inline] - pub fn build(self) -> Path { - Path { - raw: self.raw.build(), - } - } -} - -impl Default for Builder { - fn default() -> Self { - Self::new() - } -} diff --git a/native/src/widget/canvas/program.rs b/native/src/widget/canvas/program.rs deleted file mode 100644 index 17a5a137..00000000 --- a/native/src/widget/canvas/program.rs +++ /dev/null @@ -1,108 +0,0 @@ -use crate::widget::canvas::event::{self, Event}; -use crate::widget::canvas::mouse; -use crate::widget::canvas::{Cursor, Renderer}; -use crate::Rectangle; - -/// The state and logic of a [`Canvas`]. -/// -/// A [`Program`] can mutate internal state and produce messages for an -/// application. -/// -/// [`Canvas`]: crate::widget::Canvas -pub trait Program -where - Renderer: self::Renderer, -{ - /// The internal state mutated by the [`Program`]. - type State: Default + 'static; - - /// Updates the [`State`](Self::State) of the [`Program`]. - /// - /// When a [`Program`] is used in a [`Canvas`], the runtime will call this - /// method for each [`Event`]. - /// - /// This method can optionally return a `Message` to notify an application - /// of any meaningful interactions. - /// - /// By default, this method does and returns nothing. - /// - /// [`Canvas`]: crate::widget::Canvas - fn update( - &self, - _state: &mut Self::State, - _event: Event, - _bounds: Rectangle, - _cursor: Cursor, - ) -> (event::Status, Option) { - (event::Status::Ignored, None) - } - - /// Draws the state of the [`Program`], producing a bunch of [`Geometry`]. - /// - /// [`Geometry`] can be easily generated with a [`Frame`] or stored in a - /// [`Cache`]. - /// - /// [`Frame`]: crate::widget::canvas::Frame - /// [`Cache`]: crate::widget::canvas::Cache - fn draw( - &self, - state: &Self::State, - renderer: &Renderer, - theme: &Renderer::Theme, - bounds: Rectangle, - cursor: Cursor, - ) -> Vec; - - /// Returns the current mouse interaction of the [`Program`]. - /// - /// The interaction returned will be in effect even if the cursor position - /// is out of bounds of the program's [`Canvas`]. - /// - /// [`Canvas`]: crate::widget::Canvas - fn mouse_interaction( - &self, - _state: &Self::State, - _bounds: Rectangle, - _cursor: Cursor, - ) -> mouse::Interaction { - mouse::Interaction::default() - } -} - -impl Program for &T -where - Renderer: self::Renderer, - T: Program, -{ - type State = T::State; - - fn update( - &self, - state: &mut Self::State, - event: Event, - bounds: Rectangle, - cursor: Cursor, - ) -> (event::Status, Option) { - T::update(self, state, event, bounds, cursor) - } - - fn draw( - &self, - state: &Self::State, - renderer: &Renderer, - theme: &Renderer::Theme, - bounds: Rectangle, - cursor: Cursor, - ) -> Vec { - T::draw(self, state, renderer, theme, bounds, cursor) - } - - fn mouse_interaction( - &self, - state: &Self::State, - bounds: Rectangle, - cursor: Cursor, - ) -> mouse::Interaction { - T::mouse_interaction(self, state, bounds, cursor) - } -} diff --git a/native/src/widget/canvas/stroke.rs b/native/src/widget/canvas/stroke.rs deleted file mode 100644 index ab4727b2..00000000 --- a/native/src/widget/canvas/stroke.rs +++ /dev/null @@ -1,106 +0,0 @@ -//! Create lines from a [crate::widget::canvas::Path] and assigns them various attributes/styles. -pub use crate::widget::canvas::Style; - -use crate::Color; - -/// The style of a stroke. -#[derive(Debug, Clone)] -pub struct Stroke<'a> { - /// The color or gradient of the stroke. - /// - /// By default, it is set to a [`Style::Solid`] with [`Color::BLACK`]. - pub style: Style, - /// The distance between the two edges of the stroke. - pub width: f32, - /// The shape to be used at the end of open subpaths when they are stroked. - pub line_cap: LineCap, - /// The shape to be used at the corners of paths or basic shapes when they - /// are stroked. - pub line_join: LineJoin, - /// The dash pattern used when stroking the line. - pub line_dash: LineDash<'a>, -} - -impl<'a> Stroke<'a> { - /// Sets the color of the [`Stroke`]. - pub fn with_color(self, color: Color) -> Self { - Stroke { - style: Style::Solid(color), - ..self - } - } - - /// Sets the width of the [`Stroke`]. - pub fn with_width(self, width: f32) -> Self { - Stroke { width, ..self } - } - - /// Sets the [`LineCap`] of the [`Stroke`]. - pub fn with_line_cap(self, line_cap: LineCap) -> Self { - Stroke { line_cap, ..self } - } - - /// Sets the [`LineJoin`] of the [`Stroke`]. - pub fn with_line_join(self, line_join: LineJoin) -> Self { - Stroke { line_join, ..self } - } -} - -impl<'a> Default for Stroke<'a> { - fn default() -> Self { - Stroke { - style: Style::Solid(Color::BLACK), - width: 1.0, - line_cap: LineCap::default(), - line_join: LineJoin::default(), - line_dash: LineDash::default(), - } - } -} - -/// The shape used at the end of open subpaths when they are stroked. -#[derive(Debug, Clone, Copy)] -pub enum LineCap { - /// The stroke for each sub-path does not extend beyond its two endpoints. - Butt, - /// At the end of each sub-path, the shape representing the stroke will be - /// extended by a square. - Square, - /// At the end of each sub-path, the shape representing the stroke will be - /// extended by a semicircle. - Round, -} - -impl Default for LineCap { - fn default() -> LineCap { - LineCap::Butt - } -} - -/// The shape used at the corners of paths or basic shapes when they are -/// stroked. -#[derive(Debug, Clone, Copy)] -pub enum LineJoin { - /// A sharp corner. - Miter, - /// A round corner. - Round, - /// A bevelled corner. - Bevel, -} - -impl Default for LineJoin { - fn default() -> LineJoin { - LineJoin::Miter - } -} - -/// The dash pattern used when stroking the line. -#[derive(Debug, Clone, Copy, Default)] -pub struct LineDash<'a> { - /// The alternating lengths of lines and gaps which describe the pattern. - pub segments: &'a [f32], - - /// The offset of [`LineDash::segments`] to start the pattern. - pub offset: usize, -} diff --git a/native/src/widget/canvas/style.rs b/native/src/widget/canvas/style.rs deleted file mode 100644 index 2642fdb8..00000000 --- a/native/src/widget/canvas/style.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::widget::canvas::Gradient; -use crate::Color; - -/// The coloring style of some drawing. -#[derive(Debug, Clone, PartialEq)] -pub enum Style { - /// A solid [`Color`]. - Solid(Color), - - /// A [`Gradient`] color. - Gradient(Gradient), -} - -impl From for Style { - fn from(color: Color) -> Self { - Self::Solid(color) - } -} - -impl From for Style { - fn from(gradient: Gradient) -> Self { - Self::Gradient(gradient) - } -} diff --git a/native/src/widget/canvas/text.rs b/native/src/widget/canvas/text.rs deleted file mode 100644 index 8c0b2dfb..00000000 --- a/native/src/widget/canvas/text.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::alignment; -use crate::{Color, Font, Point}; - -/// A bunch of text that can be drawn to a canvas -#[derive(Debug, Clone)] -pub struct Text { - /// The contents of the text - pub content: String, - /// The position of the text relative to the alignment properties. - /// By default, this position will be relative to the top-left corner coordinate meaning that - /// if the horizontal and vertical alignments are unchanged, this property will tell where the - /// top-left corner of the text should be placed. - /// By changing the horizontal_alignment and vertical_alignment properties, you are are able to - /// change what part of text is placed at this positions. - /// For example, when the horizontal_alignment and vertical_alignment are set to Center, the - /// center of the text will be placed at the given position NOT the top-left coordinate. - pub position: Point, - /// The color of the text - pub color: Color, - /// The size of the text - pub size: f32, - /// The font of the text - pub font: Font, - /// The horizontal alignment of the text - pub horizontal_alignment: alignment::Horizontal, - /// The vertical alignment of the text - pub vertical_alignment: alignment::Vertical, -} - -impl Default for Text { - fn default() -> Text { - Text { - content: String::new(), - position: Point::ORIGIN, - color: Color::BLACK, - size: 16.0, - font: Font::SansSerif, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, - } - } -} - -impl From for Text { - fn from(content: String) -> Text { - Text { - content, - ..Default::default() - } - } -} - -impl From<&str> for Text { - fn from(content: &str) -> Text { - String::from(content).into() - } -} diff --git a/renderer/Cargo.toml b/renderer/Cargo.toml index 429b55c2..189f5309 100644 --- a/renderer/Cargo.toml +++ b/renderer/Cargo.toml @@ -6,8 +6,7 @@ edition = "2021" [features] image = ["iced_wgpu/image", "iced_tiny_skia/image"] svg = ["iced_wgpu/svg", "iced_tiny_skia/svg"] -canvas = ["iced_wgpu/canvas", "iced_tiny_skia/canvas"] -qr_code = ["canvas", "qrcode"] +geometry = ["iced_wgpu/geometry", "iced_tiny_skia/geometry"] tracing = ["iced_wgpu/tracing"] [dependencies] @@ -31,8 +30,3 @@ iced_wgpu = { version = "0.9", path = "../wgpu" } [target.'cfg(target_arch = "wasm32")'.dependencies] iced_wgpu = { version = "0.9", path = "../wgpu", features = ["webgl"] } - -[dependencies.qrcode] -version = "0.12" -optional = true -default-features = false diff --git a/renderer/src/backend.rs b/renderer/src/backend.rs index 6c0b4e5c..b0a409dc 100644 --- a/renderer/src/backend.rs +++ b/renderer/src/backend.rs @@ -1,4 +1,4 @@ -use crate::{Font, Geometry, Point, Size}; +use crate::{Font, Point, Size}; use iced_graphics::backend; use iced_graphics::text; @@ -12,8 +12,6 @@ pub enum Backend { } impl iced_graphics::Backend for Backend { - type Geometry = Geometry; - fn trim_measurements(&mut self) { match self { Self::Wgpu(backend) => backend.trim_measurements(), diff --git a/renderer/src/geometry.rs b/renderer/src/geometry.rs new file mode 100644 index 00000000..e491ea73 --- /dev/null +++ b/renderer/src/geometry.rs @@ -0,0 +1,168 @@ +mod cache; + +pub use cache::Cache; + +pub use iced_graphics::geometry::*; + +use crate::{Backend, Point, Rectangle, Size, Vector}; + +pub enum Frame { + Wgpu(iced_wgpu::geometry::Frame), + TinySkia(iced_tiny_skia::geometry::Frame), +} + +macro_rules! delegate { + ($frame:expr, $name:ident => $body:expr) => { + match $frame { + Self::Wgpu($name) => $body, + Self::TinySkia($name) => $body, + } + }; +} + +impl Frame { + pub fn new(renderer: &crate::Renderer, size: Size) -> Self { + match renderer.backend() { + Backend::Wgpu(_) => { + Frame::Wgpu(iced_wgpu::geometry::Frame::new(size)) + } + Backend::TinySkia(_) => { + Frame::TinySkia(iced_tiny_skia::geometry::Frame::new(size)) + } + } + } + + /// Returns the width of the [`Frame`]. + #[inline] + pub fn width(&self) -> f32 { + delegate!(self, frame => frame.width()) + } + + /// Returns the height of the [`Frame`]. + #[inline] + pub fn height(&self) -> f32 { + delegate!(self, frame => frame.height()) + } + + /// Returns the dimensions of the [`Frame`]. + #[inline] + pub fn size(&self) -> Size { + delegate!(self, frame => frame.size()) + } + + /// Returns the coordinate of the center of the [`Frame`]. + #[inline] + pub fn center(&self) -> Point { + delegate!(self, frame => frame.center()) + } + + /// Draws the given [`Path`] on the [`Frame`] by filling it with the + /// provided style. + pub fn fill(&mut self, path: &Path, fill: impl Into) { + delegate!(self, frame => frame.fill(path, fill)); + } + + /// Draws an axis-aligned rectangle given its top-left corner coordinate and + /// its `Size` on the [`Frame`] by filling it with the provided style. + pub fn fill_rectangle( + &mut self, + top_left: Point, + size: Size, + fill: impl Into, + ) { + delegate!(self, frame => frame.fill_rectangle(top_left, size, fill)); + } + + /// Draws the stroke of the given [`Path`] on the [`Frame`] with the + /// provided style. + pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { + delegate!(self, frame => frame.stroke(path, stroke)); + } + + /// Draws the characters of the given [`Text`] on the [`Frame`], filling + /// them with the given color. + /// + /// __Warning:__ Text currently does not work well with rotations and scale + /// transforms! The position will be correctly transformed, but the + /// resulting glyphs will not be rotated or scaled properly. + /// + /// Additionally, all text will be rendered on top of all the layers of + /// a [`Canvas`]. Therefore, it is currently only meant to be used for + /// overlays, which is the most common use case. + /// + /// Support for vectorial text is planned, and should address all these + /// limitations. + /// + /// [`Canvas`]: crate::widget::Canvas + pub fn fill_text(&mut self, text: impl Into) { + delegate!(self, frame => frame.fill_text(text)); + } + + /// Stores the current transform of the [`Frame`] and executes the given + /// drawing operations, restoring the transform afterwards. + /// + /// This method is useful to compose transforms and perform drawing + /// operations in different coordinate systems. + #[inline] + pub fn with_save(&mut self, f: impl FnOnce(&mut Frame)) { + delegate!(self, frame => frame.push_transform()); + + f(self); + + delegate!(self, frame => frame.pop_transform()); + } + + /// Executes the given drawing operations within a [`Rectangle`] region, + /// clipping any geometry that overflows its bounds. Any transformations + /// performed are local to the provided closure. + /// + /// This method is useful to perform drawing operations that need to be + /// clipped. + #[inline] + pub fn with_clip(&mut self, region: Rectangle, f: impl FnOnce(&mut Frame)) { + let mut frame = match self { + Self::Wgpu(_) => { + Self::Wgpu(iced_wgpu::geometry::Frame::new(region.size())) + } + Self::TinySkia(_) => Self::TinySkia( + iced_tiny_skia::geometry::Frame::new(region.size()), + ), + }; + + f(&mut frame); + + let translation = Vector::new(region.x, region.y); + + match (self, frame) { + (Self::Wgpu(target), Self::Wgpu(frame)) => { + target.clip(frame, translation); + } + (Self::TinySkia(target), Self::TinySkia(frame)) => { + target.clip(frame, translation); + } + _ => unreachable!(), + }; + } + + /// Applies a translation to the current transform of the [`Frame`]. + #[inline] + pub fn translate(&mut self, translation: Vector) { + delegate!(self, frame => frame.translate(translation)); + } + + /// Applies a rotation in radians to the current transform of the [`Frame`]. + #[inline] + pub fn rotate(&mut self, angle: f32) { + delegate!(self, frame => frame.rotate(angle)); + } + + /// Applies a scaling to the current transform of the [`Frame`]. + #[inline] + pub fn scale(&mut self, scale: f32) { + delegate!(self, frame => frame.scale(scale)); + } + + pub fn into_geometry(self) -> Geometry { + Geometry(delegate!(self, frame => frame.into_primitive())) + } +} diff --git a/renderer/src/geometry/cache.rs b/renderer/src/geometry/cache.rs new file mode 100644 index 00000000..1f1febdd --- /dev/null +++ b/renderer/src/geometry/cache.rs @@ -0,0 +1,85 @@ +use crate::geometry::{Frame, Geometry}; +use crate::{Primitive, Renderer, Size}; + +use std::cell::RefCell; +use std::sync::Arc; + +/// A simple cache that stores generated [`Geometry`] to avoid recomputation. +/// +/// A [`Cache`] will not redraw its geometry unless the dimensions of its layer +/// change or it is explicitly cleared. +#[derive(Debug, Default)] +pub struct Cache { + state: RefCell, +} + +#[derive(Debug, Default)] +enum State { + #[default] + Empty, + Filled { + bounds: Size, + primitive: Arc, + }, +} + +impl Cache { + /// Creates a new empty [`Cache`]. + pub fn new() -> Self { + Cache { + state: Default::default(), + } + } + + /// Clears the [`Cache`], forcing a redraw the next time it is used. + pub fn clear(&self) { + *self.state.borrow_mut() = State::Empty; + } + + /// Draws [`Geometry`] using the provided closure and stores it in the + /// [`Cache`]. + /// + /// The closure will only be called when + /// - the bounds have changed since the previous draw call. + /// - the [`Cache`] is empty or has been explicitly cleared. + /// + /// Otherwise, the previously stored [`Geometry`] will be returned. The + /// [`Cache`] is not cleared in this case. In other words, it will keep + /// returning the stored [`Geometry`] if needed. + pub fn draw( + &self, + renderer: &Renderer, + bounds: Size, + draw_fn: impl FnOnce(&mut Frame), + ) -> Geometry { + use std::ops::Deref; + + if let State::Filled { + bounds: cached_bounds, + primitive, + } = self.state.borrow().deref() + { + if *cached_bounds == bounds { + return Geometry(Primitive::Cache { + content: primitive.clone(), + }); + } + } + + let mut frame = Frame::new(renderer, bounds); + draw_fn(&mut frame); + + let primitive = { + let geometry = frame.into_geometry(); + + Arc::new(geometry.0) + }; + + *self.state.borrow_mut() = State::Filled { + bounds, + primitive: primitive.clone(), + }; + + Geometry(Primitive::Cache { content: primitive }) + } +} diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index d9c85e82..aae3322d 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -1,6 +1,8 @@ -pub mod widget; pub mod window; +#[cfg(feature = "geometry")] +pub mod geometry; + mod backend; mod settings; @@ -19,12 +21,3 @@ pub use iced_graphics::{ /// [`iced`]: https://github.com/iced-rs/iced pub type Renderer = iced_graphics::Renderer; - -#[derive(Debug, Clone)] -pub struct Geometry(pub(crate) Primitive); - -impl From for Primitive { - fn from(geometry: Geometry) -> Self { - geometry.0 - } -} diff --git a/renderer/src/widget/canvas.rs b/renderer/src/widget/canvas.rs deleted file mode 100644 index 35c8fff9..00000000 --- a/renderer/src/widget/canvas.rs +++ /dev/null @@ -1,177 +0,0 @@ -mod cache; - -pub use cache::Cache; - -pub use iced_native::widget::canvas::event::{self, Event}; -pub use iced_native::widget::canvas::fill::{self, Fill}; -pub use iced_native::widget::canvas::gradient::{self, Gradient}; -pub use iced_native::widget::canvas::path::{self, Path}; -pub use iced_native::widget::canvas::stroke::{self, Stroke}; -pub use iced_native::widget::canvas::{ - Canvas, Cursor, LineCap, LineDash, LineJoin, Program, Renderer, Style, Text, -}; - -use crate::{Backend, Point, Rectangle, Size, Vector}; - -pub use crate::Geometry; - -pub enum Frame { - Wgpu(iced_wgpu::canvas::Frame), - TinySkia(iced_tiny_skia::canvas::Frame), -} - -macro_rules! delegate { - ($frame:expr, $name:ident => $body:expr) => { - match $frame { - Self::Wgpu($name) => $body, - Self::TinySkia($name) => $body, - } - }; -} - -impl Frame { - pub fn new(renderer: &crate::Renderer, size: Size) -> Self { - match renderer.backend() { - Backend::Wgpu(_) => { - Frame::Wgpu(iced_wgpu::canvas::Frame::new(size)) - } - Backend::TinySkia(_) => { - Frame::TinySkia(iced_tiny_skia::canvas::Frame::new(size)) - } - } - } - - /// Returns the width of the [`Frame`]. - #[inline] - pub fn width(&self) -> f32 { - delegate!(self, frame => frame.width()) - } - - /// Returns the height of the [`Frame`]. - #[inline] - pub fn height(&self) -> f32 { - delegate!(self, frame => frame.height()) - } - - /// Returns the dimensions of the [`Frame`]. - #[inline] - pub fn size(&self) -> Size { - delegate!(self, frame => frame.size()) - } - - /// Returns the coordinate of the center of the [`Frame`]. - #[inline] - pub fn center(&self) -> Point { - delegate!(self, frame => frame.center()) - } - - /// Draws the given [`Path`] on the [`Frame`] by filling it with the - /// provided style. - pub fn fill(&mut self, path: &Path, fill: impl Into) { - delegate!(self, frame => frame.fill(path, fill)); - } - - /// Draws an axis-aligned rectangle given its top-left corner coordinate and - /// its `Size` on the [`Frame`] by filling it with the provided style. - pub fn fill_rectangle( - &mut self, - top_left: Point, - size: Size, - fill: impl Into, - ) { - delegate!(self, frame => frame.fill_rectangle(top_left, size, fill)); - } - - /// Draws the stroke of the given [`Path`] on the [`Frame`] with the - /// provided style. - pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { - delegate!(self, frame => frame.stroke(path, stroke)); - } - - /// Draws the characters of the given [`Text`] on the [`Frame`], filling - /// them with the given color. - /// - /// __Warning:__ Text currently does not work well with rotations and scale - /// transforms! The position will be correctly transformed, but the - /// resulting glyphs will not be rotated or scaled properly. - /// - /// Additionally, all text will be rendered on top of all the layers of - /// a [`Canvas`]. Therefore, it is currently only meant to be used for - /// overlays, which is the most common use case. - /// - /// Support for vectorial text is planned, and should address all these - /// limitations. - /// - /// [`Canvas`]: crate::widget::Canvas - pub fn fill_text(&mut self, text: impl Into) { - delegate!(self, frame => frame.fill_text(text)); - } - - /// Stores the current transform of the [`Frame`] and executes the given - /// drawing operations, restoring the transform afterwards. - /// - /// This method is useful to compose transforms and perform drawing - /// operations in different coordinate systems. - #[inline] - pub fn with_save(&mut self, f: impl FnOnce(&mut Frame)) { - delegate!(self, frame => frame.push_transform()); - - f(self); - - delegate!(self, frame => frame.pop_transform()); - } - - /// Executes the given drawing operations within a [`Rectangle`] region, - /// clipping any geometry that overflows its bounds. Any transformations - /// performed are local to the provided closure. - /// - /// This method is useful to perform drawing operations that need to be - /// clipped. - #[inline] - pub fn with_clip(&mut self, region: Rectangle, f: impl FnOnce(&mut Frame)) { - let mut frame = match self { - Self::Wgpu(_) => { - Self::Wgpu(iced_wgpu::canvas::Frame::new(region.size())) - } - Self::TinySkia(_) => Self::TinySkia( - iced_tiny_skia::canvas::Frame::new(region.size()), - ), - }; - - f(&mut frame); - - let translation = Vector::new(region.x, region.y); - - match (self, frame) { - (Self::Wgpu(target), Self::Wgpu(frame)) => { - target.clip(frame, translation); - } - (Self::TinySkia(target), Self::TinySkia(frame)) => { - target.clip(frame, translation); - } - _ => unreachable!(), - }; - } - - /// Applies a translation to the current transform of the [`Frame`]. - #[inline] - pub fn translate(&mut self, translation: Vector) { - delegate!(self, frame => frame.translate(translation)); - } - - /// Applies a rotation in radians to the current transform of the [`Frame`]. - #[inline] - pub fn rotate(&mut self, angle: f32) { - delegate!(self, frame => frame.rotate(angle)); - } - - /// Applies a scaling to the current transform of the [`Frame`]. - #[inline] - pub fn scale(&mut self, scale: f32) { - delegate!(self, frame => frame.scale(scale)); - } - - pub fn into_geometry(self) -> Geometry { - Geometry(delegate!(self, frame => frame.into_primitive())) - } -} diff --git a/renderer/src/widget/canvas/cache.rs b/renderer/src/widget/canvas/cache.rs deleted file mode 100644 index 7d6b4811..00000000 --- a/renderer/src/widget/canvas/cache.rs +++ /dev/null @@ -1,85 +0,0 @@ -use crate::widget::canvas::{Frame, Geometry}; -use crate::{Primitive, Renderer, Size}; - -use std::cell::RefCell; -use std::sync::Arc; - -/// A simple cache that stores generated [`Geometry`] to avoid recomputation. -/// -/// A [`Cache`] will not redraw its geometry unless the dimensions of its layer -/// change or it is explicitly cleared. -#[derive(Debug, Default)] -pub struct Cache { - state: RefCell, -} - -#[derive(Debug, Default)] -enum State { - #[default] - Empty, - Filled { - bounds: Size, - primitive: Arc, - }, -} - -impl Cache { - /// Creates a new empty [`Cache`]. - pub fn new() -> Self { - Cache { - state: Default::default(), - } - } - - /// Clears the [`Cache`], forcing a redraw the next time it is used. - pub fn clear(&self) { - *self.state.borrow_mut() = State::Empty; - } - - /// Draws [`Geometry`] using the provided closure and stores it in the - /// [`Cache`]. - /// - /// The closure will only be called when - /// - the bounds have changed since the previous draw call. - /// - the [`Cache`] is empty or has been explicitly cleared. - /// - /// Otherwise, the previously stored [`Geometry`] will be returned. The - /// [`Cache`] is not cleared in this case. In other words, it will keep - /// returning the stored [`Geometry`] if needed. - pub fn draw( - &self, - renderer: &Renderer, - bounds: Size, - draw_fn: impl FnOnce(&mut Frame), - ) -> Geometry { - use std::ops::Deref; - - if let State::Filled { - bounds: cached_bounds, - primitive, - } = self.state.borrow().deref() - { - if *cached_bounds == bounds { - return Geometry(Primitive::Cache { - content: primitive.clone(), - }); - } - } - - let mut frame = Frame::new(renderer, bounds); - draw_fn(&mut frame); - - let primitive = { - let geometry = frame.into_geometry(); - - Arc::new(geometry.0) - }; - - *self.state.borrow_mut() = State::Filled { - bounds, - primitive: primitive.clone(), - }; - - Geometry(Primitive::Cache { content: primitive }) - } -} diff --git a/renderer/src/widget/qr_code.rs b/renderer/src/widget/qr_code.rs deleted file mode 100644 index aae4ec88..00000000 --- a/renderer/src/widget/qr_code.rs +++ /dev/null @@ -1,301 +0,0 @@ -//! Encode and display information in a QR code. -use crate::widget::canvas; -use crate::Renderer; - -use iced_graphics::renderer; - -use iced_native::layout; -use iced_native::widget::Tree; -use iced_native::{ - Color, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget, -}; -use thiserror::Error; - -const DEFAULT_CELL_SIZE: u16 = 4; -const QUIET_ZONE: usize = 2; - -/// A type of matrix barcode consisting of squares arranged in a grid which -/// can be read by an imaging device, such as a camera. -#[derive(Debug)] -pub struct QRCode<'a> { - state: &'a State, - dark: Color, - light: Color, - cell_size: u16, -} - -impl<'a> QRCode<'a> { - /// Creates a new [`QRCode`] with the provided [`State`]. - pub fn new(state: &'a State) -> Self { - Self { - cell_size: DEFAULT_CELL_SIZE, - dark: Color::BLACK, - light: Color::WHITE, - state, - } - } - - /// Sets both the dark and light [`Color`]s of the [`QRCode`]. - pub fn color(mut self, dark: Color, light: Color) -> Self { - self.dark = dark; - self.light = light; - self - } - - /// Sets the size of the squares of the grid cell of the [`QRCode`]. - pub fn cell_size(mut self, cell_size: u16) -> Self { - self.cell_size = cell_size; - self - } -} - -impl<'a, Message, Theme> Widget> for QRCode<'a> { - fn width(&self) -> Length { - Length::Shrink - } - - fn height(&self) -> Length { - Length::Shrink - } - - fn layout( - &self, - _renderer: &Renderer, - _limits: &layout::Limits, - ) -> layout::Node { - let side_length = (self.state.width + 2 * QUIET_ZONE) as f32 - * f32::from(self.cell_size); - - layout::Node::new(Size::new(side_length, side_length)) - } - - fn draw( - &self, - _state: &Tree, - renderer: &mut Renderer, - _theme: &Theme, - _style: &renderer::Style, - layout: Layout<'_>, - _cursor_position: Point, - _viewport: &Rectangle, - ) { - use iced_native::Renderer as _; - - let bounds = layout.bounds(); - let side_length = self.state.width + 2 * QUIET_ZONE; - - // Reuse cache if possible - let geometry = - self.state.cache.draw(renderer, bounds.size(), |frame| { - // Scale units to cell size - frame.scale(f32::from(self.cell_size)); - - // Draw background - frame.fill_rectangle( - Point::ORIGIN, - Size::new(side_length as f32, side_length as f32), - self.light, - ); - - // Avoid drawing on the quiet zone - frame.translate(Vector::new( - QUIET_ZONE as f32, - QUIET_ZONE as f32, - )); - - // Draw contents - self.state - .contents - .iter() - .enumerate() - .filter(|(_, value)| **value == qrcode::Color::Dark) - .for_each(|(index, _)| { - let row = index / self.state.width; - let column = index % self.state.width; - - frame.fill_rectangle( - Point::new(column as f32, row as f32), - Size::UNIT, - self.dark, - ); - }); - }); - - let translation = Vector::new(bounds.x, bounds.y); - - renderer.with_translation(translation, |renderer| { - renderer.draw_primitive(geometry.0); - }); - } -} - -impl<'a, Message, Theme> From> - for Element<'a, Message, Renderer> -{ - fn from(qr_code: QRCode<'a>) -> Self { - Self::new(qr_code) - } -} - -/// The state of a [`QRCode`]. -/// -/// It stores the data that will be displayed. -#[derive(Debug)] -pub struct State { - contents: Vec, - width: usize, - cache: canvas::Cache, -} - -impl State { - /// Creates a new [`State`] with the provided data. - /// - /// This method uses an [`ErrorCorrection::Medium`] and chooses the smallest - /// size to display the data. - pub fn new(data: impl AsRef<[u8]>) -> Result { - let encoded = qrcode::QrCode::new(data)?; - - Ok(Self::build(encoded)) - } - - /// Creates a new [`State`] with the provided [`ErrorCorrection`]. - pub fn with_error_correction( - data: impl AsRef<[u8]>, - error_correction: ErrorCorrection, - ) -> Result { - let encoded = qrcode::QrCode::with_error_correction_level( - data, - error_correction.into(), - )?; - - Ok(Self::build(encoded)) - } - - /// Creates a new [`State`] with the provided [`Version`] and - /// [`ErrorCorrection`]. - pub fn with_version( - data: impl AsRef<[u8]>, - version: Version, - error_correction: ErrorCorrection, - ) -> Result { - let encoded = qrcode::QrCode::with_version( - data, - version.into(), - error_correction.into(), - )?; - - Ok(Self::build(encoded)) - } - - fn build(encoded: qrcode::QrCode) -> Self { - let width = encoded.width(); - let contents = encoded.into_colors(); - - Self { - contents, - width, - cache: canvas::Cache::new(), - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -/// The size of a [`QRCode`]. -/// -/// The higher the version the larger the grid of cells, and therefore the more -/// information the [`QRCode`] can carry. -pub enum Version { - /// A normal QR code version. It should be between 1 and 40. - Normal(u8), - - /// A micro QR code version. It should be between 1 and 4. - Micro(u8), -} - -impl From for qrcode::Version { - fn from(version: Version) -> Self { - match version { - Version::Normal(v) => qrcode::Version::Normal(i16::from(v)), - Version::Micro(v) => qrcode::Version::Micro(i16::from(v)), - } - } -} - -/// The error correction level. -/// -/// It controls the amount of data that can be damaged while still being able -/// to recover the original information. -/// -/// A higher error correction level allows for more corrupted data. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ErrorCorrection { - /// Low error correction. 7% of the data can be restored. - Low, - /// Medium error correction. 15% of the data can be restored. - Medium, - /// Quartile error correction. 25% of the data can be restored. - Quartile, - /// High error correction. 30% of the data can be restored. - High, -} - -impl From for qrcode::EcLevel { - fn from(ec_level: ErrorCorrection) -> Self { - match ec_level { - ErrorCorrection::Low => qrcode::EcLevel::L, - ErrorCorrection::Medium => qrcode::EcLevel::M, - ErrorCorrection::Quartile => qrcode::EcLevel::Q, - ErrorCorrection::High => qrcode::EcLevel::H, - } - } -} - -/// An error that occurred when building a [`State`] for a [`QRCode`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] -pub enum Error { - /// The data is too long to encode in a QR code for the chosen [`Version`]. - #[error( - "The data is too long to encode in a QR code for the chosen version" - )] - DataTooLong, - - /// The chosen [`Version`] and [`ErrorCorrection`] combination is invalid. - #[error( - "The chosen version and error correction level combination is invalid." - )] - InvalidVersion, - - /// One or more characters in the provided data are not supported by the - /// chosen [`Version`]. - #[error( - "One or more characters in the provided data are not supported by the \ - chosen version" - )] - UnsupportedCharacterSet, - - /// The chosen ECI designator is invalid. A valid designator should be - /// between 0 and 999999. - #[error( - "The chosen ECI designator is invalid. A valid designator should be \ - between 0 and 999999." - )] - InvalidEciDesignator, - - /// A character that does not belong to the character set was found. - #[error("A character that does not belong to the character set was found")] - InvalidCharacter, -} - -impl From for Error { - fn from(error: qrcode::types::QrError) -> Self { - use qrcode::types::QrError; - - match error { - QrError::DataTooLong => Error::DataTooLong, - QrError::InvalidVersion => Error::InvalidVersion, - QrError::UnsupportedCharacterSet => Error::UnsupportedCharacterSet, - QrError::InvalidEciDesignator => Error::InvalidEciDesignator, - QrError::InvalidCharacter => Error::InvalidCharacter, - } - } -} diff --git a/src/widget.rs b/src/widget.rs index f3a66101..19434e84 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -165,14 +165,14 @@ pub use vertical_slider::VerticalSlider; #[cfg(feature = "canvas")] #[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] -pub use iced_renderer::widget::canvas; +pub mod canvas; #[cfg(feature = "canvas")] #[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] /// Creates a new [`Canvas`]. -pub fn canvas(program: P) -> Canvas +pub fn canvas(program: P) -> Canvas where - Renderer: canvas::Renderer, + Renderer: iced_renderer::geometry::Renderer, P: canvas::Program, { Canvas::new(program) @@ -193,7 +193,7 @@ pub mod image { #[cfg(feature = "qr_code")] #[cfg_attr(docsrs, doc(cfg(feature = "qr_code")))] -pub use iced_renderer::widget::qr_code; +pub mod qr_code; #[cfg(feature = "svg")] #[cfg_attr(docsrs, doc(cfg(feature = "svg")))] diff --git a/src/widget/canvas.rs b/src/widget/canvas.rs new file mode 100644 index 00000000..bc5995c6 --- /dev/null +++ b/src/widget/canvas.rs @@ -0,0 +1,238 @@ +//! Draw 2D graphics for your users. +pub mod event; + +mod cursor; +mod program; + +pub use cursor::Cursor; +pub use event::Event; +pub use program::Program; + +pub use iced_renderer::geometry::*; + +use crate::{Length, Point, Rectangle, Size, Vector}; + +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::widget::tree::{self, Tree}; +use iced_native::{Clipboard, Element, Shell, Widget}; + +use std::marker::PhantomData; + +/// A widget capable of drawing 2D graphics. +/// +/// ## Drawing a simple circle +/// If you want to get a quick overview, here's how we can draw a simple circle: +/// +/// ```no_run +/// use iced::widget::canvas::{self, Canvas, Cursor, Fill, Frame, Geometry, Path, Program}; +/// use iced::{Color, Rectangle, Theme, Renderer}; +/// +/// // First, we define the data we need for drawing +/// #[derive(Debug)] +/// struct Circle { +/// radius: f32, +/// } +/// +/// // Then, we implement the `Program` trait +/// impl Program<()> for Circle { +/// type State = (); +/// +/// fn draw(&self, _state: &(), renderer: &Renderer, _theme: &Theme, bounds: Rectangle, _cursor: Cursor) -> Vec{ +/// // We prepare a new `Frame` +/// let mut frame = Frame::new(renderer, bounds.size()); +/// +/// // We create a `Path` representing a simple circle +/// let circle = Path::circle(frame.center(), self.radius); +/// +/// // And fill it with some color +/// frame.fill(&circle, Color::BLACK); +/// +/// // Finally, we produce the geometry +/// vec![frame.into_geometry()] +/// } +/// } +/// +/// // Finally, we simply use our `Circle` to create the `Canvas`! +/// let canvas = Canvas::new(Circle { radius: 50.0 }); +/// ``` +#[derive(Debug)] +pub struct Canvas +where + Renderer: iced_renderer::geometry::Renderer, + P: Program, +{ + width: Length, + height: Length, + program: P, + message_: PhantomData, + theme_: PhantomData, +} + +impl Canvas +where + Renderer: iced_renderer::geometry::Renderer, + P: Program, +{ + const DEFAULT_SIZE: f32 = 100.0; + + /// Creates a new [`Canvas`]. + pub fn new(program: P) -> Self { + Canvas { + width: Length::Fixed(Self::DEFAULT_SIZE), + height: Length::Fixed(Self::DEFAULT_SIZE), + program, + message_: PhantomData, + theme_: PhantomData, + } + } + + /// Sets the width of the [`Canvas`]. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Canvas`]. + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } +} + +impl Widget + for Canvas +where + Renderer: iced_renderer::geometry::Renderer, + P: Program, +{ + fn tag(&self) -> tree::Tag { + struct Tag(T); + tree::Tag::of::>() + } + + fn state(&self) -> tree::State { + tree::State::new(P::State::default()) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + let size = limits.resolve(Size::ZERO); + + layout::Node::new(size) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: iced_native::Event, + layout: Layout<'_>, + cursor_position: Point, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + let bounds = layout.bounds(); + + let canvas_event = match event { + iced_native::Event::Mouse(mouse_event) => { + Some(Event::Mouse(mouse_event)) + } + iced_native::Event::Touch(touch_event) => { + Some(Event::Touch(touch_event)) + } + iced_native::Event::Keyboard(keyboard_event) => { + Some(Event::Keyboard(keyboard_event)) + } + _ => None, + }; + + let cursor = Cursor::from_window_position(cursor_position); + + if let Some(canvas_event) = canvas_event { + let state = tree.state.downcast_mut::(); + + let (event_status, message) = + self.program.update(state, canvas_event, bounds, cursor); + + if let Some(message) = message { + shell.publish(message); + } + + return event_status; + } + + event::Status::Ignored + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + let bounds = layout.bounds(); + let cursor = Cursor::from_window_position(cursor_position); + let state = tree.state.downcast_ref::(); + + self.program.mouse_interaction(state, bounds, cursor) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + + if bounds.width < 1.0 || bounds.height < 1.0 { + return; + } + + let cursor = Cursor::from_window_position(cursor_position); + let state = tree.state.downcast_ref::(); + + renderer.with_translation( + Vector::new(bounds.x, bounds.y), + |renderer| { + renderer.draw( + self.program.draw(state, renderer, theme, bounds, cursor), + ); + }, + ); + } +} + +impl<'a, P, Message, Renderer> From> + for Element<'a, Message, Renderer> +where + Message: 'a, + Renderer: 'a + iced_renderer::geometry::Renderer, + P: Program + 'a, +{ + fn from( + canvas: Canvas, + ) -> Element<'a, Message, Renderer> { + Element::new(canvas) + } +} diff --git a/src/widget/canvas/cursor.rs b/src/widget/canvas/cursor.rs new file mode 100644 index 00000000..ef6a7771 --- /dev/null +++ b/src/widget/canvas/cursor.rs @@ -0,0 +1,64 @@ +use crate::{Point, Rectangle}; + +/// The mouse cursor state. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Cursor { + /// The cursor has a defined position. + Available(Point), + + /// The cursor is currently unavailable (i.e. out of bounds or busy). + Unavailable, +} + +impl Cursor { + // TODO: Remove this once this type is used in `iced_native` to encode + // proper cursor availability + pub(crate) fn from_window_position(position: Point) -> Self { + if position.x < 0.0 || position.y < 0.0 { + Cursor::Unavailable + } else { + Cursor::Available(position) + } + } + + /// Returns the absolute position of the [`Cursor`], if available. + pub fn position(&self) -> Option { + match self { + Cursor::Available(position) => Some(*position), + Cursor::Unavailable => None, + } + } + + /// Returns the relative position of the [`Cursor`] inside the given bounds, + /// if available. + /// + /// If the [`Cursor`] is not over the provided bounds, this method will + /// return `None`. + pub fn position_in(&self, bounds: &Rectangle) -> Option { + if self.is_over(bounds) { + self.position_from(bounds.position()) + } else { + None + } + } + + /// Returns the relative position of the [`Cursor`] from the given origin, + /// if available. + pub fn position_from(&self, origin: Point) -> Option { + match self { + Cursor::Available(position) => { + Some(Point::new(position.x - origin.x, position.y - origin.y)) + } + Cursor::Unavailable => None, + } + } + + /// Returns whether the [`Cursor`] is currently over the provided bounds + /// or not. + pub fn is_over(&self, bounds: &Rectangle) -> bool { + match self { + Cursor::Available(position) => bounds.contains(*position), + Cursor::Unavailable => false, + } + } +} diff --git a/src/widget/canvas/event.rs b/src/widget/canvas/event.rs new file mode 100644 index 00000000..7c733a4d --- /dev/null +++ b/src/widget/canvas/event.rs @@ -0,0 +1,21 @@ +//! Handle events of a canvas. +use iced_native::keyboard; +use iced_native::mouse; +use iced_native::touch; + +pub use iced_native::event::Status; + +/// A [`Canvas`] event. +/// +/// [`Canvas`]: crate::widget::Canvas +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Event { + /// A mouse event. + Mouse(mouse::Event), + + /// A touch event. + Touch(touch::Event), + + /// A keyboard event. + Keyboard(keyboard::Event), +} diff --git a/src/widget/canvas/program.rs b/src/widget/canvas/program.rs new file mode 100644 index 00000000..fd15663a --- /dev/null +++ b/src/widget/canvas/program.rs @@ -0,0 +1,108 @@ +use crate::widget::canvas::event::{self, Event}; +use crate::widget::canvas::mouse; +use crate::widget::canvas::Cursor; +use crate::Rectangle; + +/// The state and logic of a [`Canvas`]. +/// +/// A [`Program`] can mutate internal state and produce messages for an +/// application. +/// +/// [`Canvas`]: crate::widget::Canvas +pub trait Program +where + Renderer: iced_renderer::geometry::Renderer, +{ + /// The internal state mutated by the [`Program`]. + type State: Default + 'static; + + /// Updates the [`State`](Self::State) of the [`Program`]. + /// + /// When a [`Program`] is used in a [`Canvas`], the runtime will call this + /// method for each [`Event`]. + /// + /// This method can optionally return a `Message` to notify an application + /// of any meaningful interactions. + /// + /// By default, this method does and returns nothing. + /// + /// [`Canvas`]: crate::widget::Canvas + fn update( + &self, + _state: &mut Self::State, + _event: Event, + _bounds: Rectangle, + _cursor: Cursor, + ) -> (event::Status, Option) { + (event::Status::Ignored, None) + } + + /// Draws the state of the [`Program`], producing a bunch of [`Geometry`]. + /// + /// [`Geometry`] can be easily generated with a [`Frame`] or stored in a + /// [`Cache`]. + /// + /// [`Frame`]: crate::widget::canvas::Frame + /// [`Cache`]: crate::widget::canvas::Cache + fn draw( + &self, + state: &Self::State, + renderer: &Renderer, + theme: &Renderer::Theme, + bounds: Rectangle, + cursor: Cursor, + ) -> Vec; + + /// Returns the current mouse interaction of the [`Program`]. + /// + /// The interaction returned will be in effect even if the cursor position + /// is out of bounds of the program's [`Canvas`]. + /// + /// [`Canvas`]: crate::widget::Canvas + fn mouse_interaction( + &self, + _state: &Self::State, + _bounds: Rectangle, + _cursor: Cursor, + ) -> mouse::Interaction { + mouse::Interaction::default() + } +} + +impl Program for &T +where + Renderer: iced_renderer::geometry::Renderer, + T: Program, +{ + type State = T::State; + + fn update( + &self, + state: &mut Self::State, + event: Event, + bounds: Rectangle, + cursor: Cursor, + ) -> (event::Status, Option) { + T::update(self, state, event, bounds, cursor) + } + + fn draw( + &self, + state: &Self::State, + renderer: &Renderer, + theme: &Renderer::Theme, + bounds: Rectangle, + cursor: Cursor, + ) -> Vec { + T::draw(self, state, renderer, theme, bounds, cursor) + } + + fn mouse_interaction( + &self, + state: &Self::State, + bounds: Rectangle, + cursor: Cursor, + ) -> mouse::Interaction { + T::mouse_interaction(self, state, bounds, cursor) + } +} diff --git a/src/widget/qr_code.rs b/src/widget/qr_code.rs new file mode 100644 index 00000000..66442d5d --- /dev/null +++ b/src/widget/qr_code.rs @@ -0,0 +1,300 @@ +//! Encode and display information in a QR code. +use crate::widget::canvas; +use crate::Renderer; + +use iced_native::layout; +use iced_native::renderer; +use iced_native::widget::Tree; +use iced_native::{ + Color, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget, +}; +use thiserror::Error; + +const DEFAULT_CELL_SIZE: u16 = 4; +const QUIET_ZONE: usize = 2; + +/// A type of matrix barcode consisting of squares arranged in a grid which +/// can be read by an imaging device, such as a camera. +#[derive(Debug)] +pub struct QRCode<'a> { + state: &'a State, + dark: Color, + light: Color, + cell_size: u16, +} + +impl<'a> QRCode<'a> { + /// Creates a new [`QRCode`] with the provided [`State`]. + pub fn new(state: &'a State) -> Self { + Self { + cell_size: DEFAULT_CELL_SIZE, + dark: Color::BLACK, + light: Color::WHITE, + state, + } + } + + /// Sets both the dark and light [`Color`]s of the [`QRCode`]. + pub fn color(mut self, dark: Color, light: Color) -> Self { + self.dark = dark; + self.light = light; + self + } + + /// Sets the size of the squares of the grid cell of the [`QRCode`]. + pub fn cell_size(mut self, cell_size: u16) -> Self { + self.cell_size = cell_size; + self + } +} + +impl<'a, Message, Theme> Widget> for QRCode<'a> { + fn width(&self) -> Length { + Length::Shrink + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + _renderer: &Renderer, + _limits: &layout::Limits, + ) -> layout::Node { + let side_length = (self.state.width + 2 * QUIET_ZONE) as f32 + * f32::from(self.cell_size); + + layout::Node::new(Size::new(side_length, side_length)) + } + + fn draw( + &self, + _state: &Tree, + renderer: &mut Renderer, + _theme: &Theme, + _style: &renderer::Style, + layout: Layout<'_>, + _cursor_position: Point, + _viewport: &Rectangle, + ) { + use iced_native::Renderer as _; + + let bounds = layout.bounds(); + let side_length = self.state.width + 2 * QUIET_ZONE; + + // Reuse cache if possible + let geometry = + self.state.cache.draw(renderer, bounds.size(), |frame| { + // Scale units to cell size + frame.scale(f32::from(self.cell_size)); + + // Draw background + frame.fill_rectangle( + Point::ORIGIN, + Size::new(side_length as f32, side_length as f32), + self.light, + ); + + // Avoid drawing on the quiet zone + frame.translate(Vector::new( + QUIET_ZONE as f32, + QUIET_ZONE as f32, + )); + + // Draw contents + self.state + .contents + .iter() + .enumerate() + .filter(|(_, value)| **value == qrcode::Color::Dark) + .for_each(|(index, _)| { + let row = index / self.state.width; + let column = index % self.state.width; + + frame.fill_rectangle( + Point::new(column as f32, row as f32), + Size::UNIT, + self.dark, + ); + }); + }); + + let translation = Vector::new(bounds.x, bounds.y); + + renderer.with_translation(translation, |renderer| { + renderer.draw_primitive(geometry.0); + }); + } +} + +impl<'a, Message, Theme> From> + for Element<'a, Message, Renderer> +{ + fn from(qr_code: QRCode<'a>) -> Self { + Self::new(qr_code) + } +} + +/// The state of a [`QRCode`]. +/// +/// It stores the data that will be displayed. +#[derive(Debug)] +pub struct State { + contents: Vec, + width: usize, + cache: canvas::Cache, +} + +impl State { + /// Creates a new [`State`] with the provided data. + /// + /// This method uses an [`ErrorCorrection::Medium`] and chooses the smallest + /// size to display the data. + pub fn new(data: impl AsRef<[u8]>) -> Result { + let encoded = qrcode::QrCode::new(data)?; + + Ok(Self::build(encoded)) + } + + /// Creates a new [`State`] with the provided [`ErrorCorrection`]. + pub fn with_error_correction( + data: impl AsRef<[u8]>, + error_correction: ErrorCorrection, + ) -> Result { + let encoded = qrcode::QrCode::with_error_correction_level( + data, + error_correction.into(), + )?; + + Ok(Self::build(encoded)) + } + + /// Creates a new [`State`] with the provided [`Version`] and + /// [`ErrorCorrection`]. + pub fn with_version( + data: impl AsRef<[u8]>, + version: Version, + error_correction: ErrorCorrection, + ) -> Result { + let encoded = qrcode::QrCode::with_version( + data, + version.into(), + error_correction.into(), + )?; + + Ok(Self::build(encoded)) + } + + fn build(encoded: qrcode::QrCode) -> Self { + let width = encoded.width(); + let contents = encoded.into_colors(); + + Self { + contents, + width, + cache: canvas::Cache::new(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// The size of a [`QRCode`]. +/// +/// The higher the version the larger the grid of cells, and therefore the more +/// information the [`QRCode`] can carry. +pub enum Version { + /// A normal QR code version. It should be between 1 and 40. + Normal(u8), + + /// A micro QR code version. It should be between 1 and 4. + Micro(u8), +} + +impl From for qrcode::Version { + fn from(version: Version) -> Self { + match version { + Version::Normal(v) => qrcode::Version::Normal(i16::from(v)), + Version::Micro(v) => qrcode::Version::Micro(i16::from(v)), + } + } +} + +/// The error correction level. +/// +/// It controls the amount of data that can be damaged while still being able +/// to recover the original information. +/// +/// A higher error correction level allows for more corrupted data. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ErrorCorrection { + /// Low error correction. 7% of the data can be restored. + Low, + /// Medium error correction. 15% of the data can be restored. + Medium, + /// Quartile error correction. 25% of the data can be restored. + Quartile, + /// High error correction. 30% of the data can be restored. + High, +} + +impl From for qrcode::EcLevel { + fn from(ec_level: ErrorCorrection) -> Self { + match ec_level { + ErrorCorrection::Low => qrcode::EcLevel::L, + ErrorCorrection::Medium => qrcode::EcLevel::M, + ErrorCorrection::Quartile => qrcode::EcLevel::Q, + ErrorCorrection::High => qrcode::EcLevel::H, + } + } +} + +/// An error that occurred when building a [`State`] for a [`QRCode`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] +pub enum Error { + /// The data is too long to encode in a QR code for the chosen [`Version`]. + #[error( + "The data is too long to encode in a QR code for the chosen version" + )] + DataTooLong, + + /// The chosen [`Version`] and [`ErrorCorrection`] combination is invalid. + #[error( + "The chosen version and error correction level combination is invalid." + )] + InvalidVersion, + + /// One or more characters in the provided data are not supported by the + /// chosen [`Version`]. + #[error( + "One or more characters in the provided data are not supported by the \ + chosen version" + )] + UnsupportedCharacterSet, + + /// The chosen ECI designator is invalid. A valid designator should be + /// between 0 and 999999. + #[error( + "The chosen ECI designator is invalid. A valid designator should be \ + between 0 and 999999." + )] + InvalidEciDesignator, + + /// A character that does not belong to the character set was found. + #[error("A character that does not belong to the character set was found")] + InvalidCharacter, +} + +impl From for Error { + fn from(error: qrcode::types::QrError) -> Self { + use qrcode::types::QrError; + + match error { + QrError::DataTooLong => Error::DataTooLong, + QrError::InvalidVersion => Error::InvalidVersion, + QrError::UnsupportedCharacterSet => Error::UnsupportedCharacterSet, + QrError::InvalidEciDesignator => Error::InvalidEciDesignator, + QrError::InvalidCharacter => Error::InvalidCharacter, + } + } +} diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml index 72181735..c4c36aba 100644 --- a/tiny_skia/Cargo.toml +++ b/tiny_skia/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" [features] image = [] svg = [] -canvas = ["iced_native/canvas"] +geometry = ["iced_graphics/geometry"] [dependencies] raw-window-handle = "0.5" diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 6883a953..050c6c75 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -436,8 +436,6 @@ fn adjust_clip_mask( } impl iced_graphics::Backend for Backend { - type Geometry = (); - fn trim_measurements(&mut self) { self.text_pipeline.trim_measurement_cache(); } diff --git a/tiny_skia/src/canvas.rs b/tiny_skia/src/canvas.rs deleted file mode 100644 index 958063d2..00000000 --- a/tiny_skia/src/canvas.rs +++ /dev/null @@ -1,287 +0,0 @@ -use crate::{Point, Primitive, Rectangle, Size, Vector}; - -use iced_native::widget::canvas::fill::{self, Fill}; -use iced_native::widget::canvas::stroke::{self, Stroke}; -use iced_native::widget::canvas::{Path, Style, Text}; -use iced_native::Gradient; - -pub struct Frame { - size: Size, - transform: tiny_skia::Transform, - stack: Vec, - primitives: Vec, -} - -impl Frame { - pub fn new(size: Size) -> Self { - Self { - size, - transform: tiny_skia::Transform::identity(), - stack: Vec::new(), - primitives: Vec::new(), - } - } - - pub fn width(&self) -> f32 { - self.size.width - } - - pub fn height(&self) -> f32 { - self.size.height - } - - pub fn size(&self) -> Size { - self.size - } - - pub fn center(&self) -> Point { - Point::new(self.size.width / 2.0, self.size.height / 2.0) - } - - pub fn fill(&mut self, path: &Path, fill: impl Into) { - let path = convert_path(path); - let fill = fill.into(); - - self.primitives.push(Primitive::Fill { - path, - paint: into_paint(fill.style), - rule: into_fill_rule(fill.rule), - transform: self.transform, - }); - } - - pub fn fill_rectangle( - &mut self, - top_left: Point, - size: Size, - fill: impl Into, - ) { - let path = convert_path(&Path::rectangle(top_left, size)); - let fill = fill.into(); - - self.primitives.push(Primitive::Fill { - path, - paint: tiny_skia::Paint { - anti_alias: false, - ..into_paint(fill.style) - }, - rule: into_fill_rule(fill.rule), - transform: self.transform, - }); - } - - pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { - let path = convert_path(path); - let stroke = stroke.into(); - let skia_stroke = into_stroke(&stroke); - - self.primitives.push(Primitive::Stroke { - path, - paint: into_paint(stroke.style), - stroke: skia_stroke, - transform: self.transform, - }); - } - - pub fn fill_text(&mut self, text: impl Into) { - let text = text.into(); - - let position = if self.transform.is_identity() { - text.position - } else { - let mut transformed = [tiny_skia::Point { - x: text.position.x, - y: text.position.y, - }]; - - self.transform.map_points(&mut transformed); - - Point::new(transformed[0].x, transformed[0].y) - }; - - // TODO: Use vectorial text instead of primitive - self.primitives.push(Primitive::Text { - content: text.content, - bounds: Rectangle { - x: position.x, - y: position.y, - width: f32::INFINITY, - height: f32::INFINITY, - }, - color: text.color, - size: text.size, - font: text.font, - horizontal_alignment: text.horizontal_alignment, - vertical_alignment: text.vertical_alignment, - }); - } - - pub fn push_transform(&mut self) { - self.stack.push(self.transform); - } - - pub fn pop_transform(&mut self) { - self.transform = self.stack.pop().expect("Pop transform"); - } - - pub fn clip(&mut self, _frame: Self, _translation: Vector) {} - - pub fn translate(&mut self, translation: Vector) { - self.transform = - self.transform.pre_translate(translation.x, translation.y); - } - - pub fn rotate(&mut self, angle: f32) { - self.transform = self - .transform - .pre_concat(tiny_skia::Transform::from_rotate(angle.to_degrees())); - } - - pub fn scale(&mut self, scale: f32) { - self.transform = self.transform.pre_scale(scale, scale); - } - - pub fn into_primitive(self) -> Primitive { - Primitive::Clip { - bounds: Rectangle::new(Point::ORIGIN, self.size), - content: Box::new(Primitive::Group { - primitives: self.primitives, - }), - } - } -} - -fn convert_path(path: &Path) -> tiny_skia::Path { - use iced_native::widget::canvas::path::lyon_path; - - let mut builder = tiny_skia::PathBuilder::new(); - let mut last_point = Default::default(); - - for event in path.raw().iter() { - match event { - lyon_path::Event::Begin { at } => { - builder.move_to(at.x, at.y); - - last_point = at; - } - lyon_path::Event::Line { from, to } => { - if last_point != from { - builder.move_to(from.x, from.y); - } - - builder.line_to(to.x, to.y); - - last_point = to; - } - lyon_path::Event::Quadratic { from, ctrl, to } => { - if last_point != from { - builder.move_to(from.x, from.y); - } - - builder.quad_to(ctrl.x, ctrl.y, to.x, to.y); - - last_point = to; - } - lyon_path::Event::Cubic { - from, - ctrl1, - ctrl2, - to, - } => { - if last_point != from { - builder.move_to(from.x, from.y); - } - - builder - .cubic_to(ctrl1.x, ctrl1.y, ctrl2.x, ctrl2.y, to.x, to.y); - - last_point = to; - } - lyon_path::Event::End { close, .. } => { - if close { - builder.close(); - } - } - } - } - - builder - .finish() - .expect("Convert lyon path to tiny_skia path") -} - -pub fn into_paint(style: Style) -> tiny_skia::Paint<'static> { - tiny_skia::Paint { - shader: match style { - Style::Solid(color) => tiny_skia::Shader::SolidColor( - tiny_skia::Color::from_rgba(color.b, color.g, color.r, color.a) - .expect("Create color"), - ), - Style::Gradient(gradient) => match gradient { - Gradient::Linear(linear) => tiny_skia::LinearGradient::new( - tiny_skia::Point { - x: linear.start.x, - y: linear.start.y, - }, - tiny_skia::Point { - x: linear.end.x, - y: linear.end.y, - }, - linear - .color_stops - .into_iter() - .map(|stop| { - tiny_skia::GradientStop::new( - stop.offset, - tiny_skia::Color::from_rgba( - stop.color.b, - stop.color.g, - stop.color.r, - stop.color.a, - ) - .expect("Create color"), - ) - }) - .collect(), - tiny_skia::SpreadMode::Pad, - tiny_skia::Transform::identity(), - ) - .expect("Create linear gradient"), - }, - }, - anti_alias: true, - ..Default::default() - } -} - -pub fn into_fill_rule(rule: fill::Rule) -> tiny_skia::FillRule { - match rule { - fill::Rule::EvenOdd => tiny_skia::FillRule::EvenOdd, - fill::Rule::NonZero => tiny_skia::FillRule::Winding, - } -} - -pub fn into_stroke(stroke: &Stroke) -> tiny_skia::Stroke { - tiny_skia::Stroke { - width: stroke.width, - line_cap: match stroke.line_cap { - stroke::LineCap::Butt => tiny_skia::LineCap::Butt, - stroke::LineCap::Square => tiny_skia::LineCap::Square, - stroke::LineCap::Round => tiny_skia::LineCap::Round, - }, - line_join: match stroke.line_join { - stroke::LineJoin::Miter => tiny_skia::LineJoin::Miter, - stroke::LineJoin::Round => tiny_skia::LineJoin::Round, - stroke::LineJoin::Bevel => tiny_skia::LineJoin::Bevel, - }, - dash: if stroke.line_dash.segments.is_empty() { - None - } else { - tiny_skia::StrokeDash::new( - stroke.line_dash.segments.into(), - stroke.line_dash.offset as f32, - ) - }, - ..Default::default() - } -} diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs new file mode 100644 index 00000000..73fc1ebd --- /dev/null +++ b/tiny_skia/src/geometry.rs @@ -0,0 +1,287 @@ +use crate::{Point, Primitive, Rectangle, Size, Vector}; + +use iced_graphics::geometry::fill::{self, Fill}; +use iced_graphics::geometry::stroke::{self, Stroke}; +use iced_graphics::geometry::{Path, Style, Text}; +use iced_graphics::Gradient; + +pub struct Frame { + size: Size, + transform: tiny_skia::Transform, + stack: Vec, + primitives: Vec, +} + +impl Frame { + pub fn new(size: Size) -> Self { + Self { + size, + transform: tiny_skia::Transform::identity(), + stack: Vec::new(), + primitives: Vec::new(), + } + } + + pub fn width(&self) -> f32 { + self.size.width + } + + pub fn height(&self) -> f32 { + self.size.height + } + + pub fn size(&self) -> Size { + self.size + } + + pub fn center(&self) -> Point { + Point::new(self.size.width / 2.0, self.size.height / 2.0) + } + + pub fn fill(&mut self, path: &Path, fill: impl Into) { + let path = convert_path(path); + let fill = fill.into(); + + self.primitives.push(Primitive::Fill { + path, + paint: into_paint(fill.style), + rule: into_fill_rule(fill.rule), + transform: self.transform, + }); + } + + pub fn fill_rectangle( + &mut self, + top_left: Point, + size: Size, + fill: impl Into, + ) { + let path = convert_path(&Path::rectangle(top_left, size)); + let fill = fill.into(); + + self.primitives.push(Primitive::Fill { + path, + paint: tiny_skia::Paint { + anti_alias: false, + ..into_paint(fill.style) + }, + rule: into_fill_rule(fill.rule), + transform: self.transform, + }); + } + + pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { + let path = convert_path(path); + let stroke = stroke.into(); + let skia_stroke = into_stroke(&stroke); + + self.primitives.push(Primitive::Stroke { + path, + paint: into_paint(stroke.style), + stroke: skia_stroke, + transform: self.transform, + }); + } + + pub fn fill_text(&mut self, text: impl Into) { + let text = text.into(); + + let position = if self.transform.is_identity() { + text.position + } else { + let mut transformed = [tiny_skia::Point { + x: text.position.x, + y: text.position.y, + }]; + + self.transform.map_points(&mut transformed); + + Point::new(transformed[0].x, transformed[0].y) + }; + + // TODO: Use vectorial text instead of primitive + self.primitives.push(Primitive::Text { + content: text.content, + bounds: Rectangle { + x: position.x, + y: position.y, + width: f32::INFINITY, + height: f32::INFINITY, + }, + color: text.color, + size: text.size, + font: text.font, + horizontal_alignment: text.horizontal_alignment, + vertical_alignment: text.vertical_alignment, + }); + } + + pub fn push_transform(&mut self) { + self.stack.push(self.transform); + } + + pub fn pop_transform(&mut self) { + self.transform = self.stack.pop().expect("Pop transform"); + } + + pub fn clip(&mut self, _frame: Self, _translation: Vector) {} + + pub fn translate(&mut self, translation: Vector) { + self.transform = + self.transform.pre_translate(translation.x, translation.y); + } + + pub fn rotate(&mut self, angle: f32) { + self.transform = self + .transform + .pre_concat(tiny_skia::Transform::from_rotate(angle.to_degrees())); + } + + pub fn scale(&mut self, scale: f32) { + self.transform = self.transform.pre_scale(scale, scale); + } + + pub fn into_primitive(self) -> Primitive { + Primitive::Clip { + bounds: Rectangle::new(Point::ORIGIN, self.size), + content: Box::new(Primitive::Group { + primitives: self.primitives, + }), + } + } +} + +fn convert_path(path: &Path) -> tiny_skia::Path { + use iced_graphics::geometry::path::lyon_path; + + let mut builder = tiny_skia::PathBuilder::new(); + let mut last_point = Default::default(); + + for event in path.raw().iter() { + match event { + lyon_path::Event::Begin { at } => { + builder.move_to(at.x, at.y); + + last_point = at; + } + lyon_path::Event::Line { from, to } => { + if last_point != from { + builder.move_to(from.x, from.y); + } + + builder.line_to(to.x, to.y); + + last_point = to; + } + lyon_path::Event::Quadratic { from, ctrl, to } => { + if last_point != from { + builder.move_to(from.x, from.y); + } + + builder.quad_to(ctrl.x, ctrl.y, to.x, to.y); + + last_point = to; + } + lyon_path::Event::Cubic { + from, + ctrl1, + ctrl2, + to, + } => { + if last_point != from { + builder.move_to(from.x, from.y); + } + + builder + .cubic_to(ctrl1.x, ctrl1.y, ctrl2.x, ctrl2.y, to.x, to.y); + + last_point = to; + } + lyon_path::Event::End { close, .. } => { + if close { + builder.close(); + } + } + } + } + + builder + .finish() + .expect("Convert lyon path to tiny_skia path") +} + +pub fn into_paint(style: Style) -> tiny_skia::Paint<'static> { + tiny_skia::Paint { + shader: match style { + Style::Solid(color) => tiny_skia::Shader::SolidColor( + tiny_skia::Color::from_rgba(color.b, color.g, color.r, color.a) + .expect("Create color"), + ), + Style::Gradient(gradient) => match gradient { + Gradient::Linear(linear) => tiny_skia::LinearGradient::new( + tiny_skia::Point { + x: linear.start.x, + y: linear.start.y, + }, + tiny_skia::Point { + x: linear.end.x, + y: linear.end.y, + }, + linear + .color_stops + .into_iter() + .map(|stop| { + tiny_skia::GradientStop::new( + stop.offset, + tiny_skia::Color::from_rgba( + stop.color.b, + stop.color.g, + stop.color.r, + stop.color.a, + ) + .expect("Create color"), + ) + }) + .collect(), + tiny_skia::SpreadMode::Pad, + tiny_skia::Transform::identity(), + ) + .expect("Create linear gradient"), + }, + }, + anti_alias: true, + ..Default::default() + } +} + +pub fn into_fill_rule(rule: fill::Rule) -> tiny_skia::FillRule { + match rule { + fill::Rule::EvenOdd => tiny_skia::FillRule::EvenOdd, + fill::Rule::NonZero => tiny_skia::FillRule::Winding, + } +} + +pub fn into_stroke(stroke: &Stroke) -> tiny_skia::Stroke { + tiny_skia::Stroke { + width: stroke.width, + line_cap: match stroke.line_cap { + stroke::LineCap::Butt => tiny_skia::LineCap::Butt, + stroke::LineCap::Square => tiny_skia::LineCap::Square, + stroke::LineCap::Round => tiny_skia::LineCap::Round, + }, + line_join: match stroke.line_join { + stroke::LineJoin::Miter => tiny_skia::LineJoin::Miter, + stroke::LineJoin::Round => tiny_skia::LineJoin::Round, + stroke::LineJoin::Bevel => tiny_skia::LineJoin::Bevel, + }, + dash: if stroke.line_dash.segments.is_empty() { + None + } else { + tiny_skia::StrokeDash::new( + stroke.line_dash.segments.into(), + stroke.line_dash.offset as f32, + ) + }, + ..Default::default() + } +} diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index e66e6412..ef5c6b1d 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -4,8 +4,8 @@ mod backend; mod settings; mod text; -#[cfg(feature = "canvas")] -pub mod canvas; +#[cfg(feature = "geometry")] +pub mod geometry; pub use iced_graphics::primitive; diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 0bcef71c..2e39a9e7 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -21,7 +21,7 @@ bmp = ["iced_graphics/bmp"] hdr = ["iced_graphics/hdr"] dds = ["iced_graphics/dds"] farbfeld = ["iced_graphics/farbfeld"] -canvas = ["iced_graphics/canvas", "lyon"] +geometry = ["iced_graphics/geometry", "lyon"] spirv = ["wgpu/spirv"] webgl = ["wgpu/webgl"] diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 10dc5b4f..6f39a5fe 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -329,8 +329,6 @@ impl Backend { } impl iced_graphics::Backend for Backend { - type Geometry = (); - fn trim_measurements(&mut self) { self.text_pipeline.trim_measurement_cache() } diff --git a/wgpu/src/canvas.rs b/wgpu/src/canvas.rs deleted file mode 100644 index e8d540c3..00000000 --- a/wgpu/src/canvas.rs +++ /dev/null @@ -1,608 +0,0 @@ -use iced_graphics::primitive::{self, Primitive}; -use iced_native::widget::canvas::fill::{self, Fill}; -use iced_native::widget::canvas::{ - LineCap, LineDash, LineJoin, Path, Stroke, Style, Text, -}; -use iced_native::{Gradient, Point, Rectangle, Size, Vector}; - -use lyon::geom::euclid; -use lyon::tessellation; -use std::borrow::Cow; - -/// The frame of a [`Canvas`]. -/// -/// [`Canvas`]: crate::widget::Canvas -#[allow(missing_debug_implementations)] -pub struct Frame { - size: Size, - buffers: BufferStack, - primitives: Vec, - transforms: Transforms, - fill_tessellator: tessellation::FillTessellator, - stroke_tessellator: tessellation::StrokeTessellator, -} - -enum Buffer { - Solid(tessellation::VertexBuffers), - Gradient( - tessellation::VertexBuffers, - Gradient, - ), -} - -struct BufferStack { - stack: Vec, -} - -impl BufferStack { - fn new() -> Self { - Self { stack: Vec::new() } - } - - fn get_mut(&mut self, style: &Style) -> &mut Buffer { - match style { - Style::Solid(_) => match self.stack.last() { - Some(Buffer::Solid(_)) => {} - _ => { - self.stack.push(Buffer::Solid( - tessellation::VertexBuffers::new(), - )); - } - }, - Style::Gradient(gradient) => match self.stack.last() { - Some(Buffer::Gradient(_, last)) if gradient == last => {} - _ => { - self.stack.push(Buffer::Gradient( - tessellation::VertexBuffers::new(), - gradient.clone(), - )); - } - }, - } - - self.stack.last_mut().unwrap() - } - - fn get_fill<'a>( - &'a mut self, - style: &Style, - ) -> Box { - match (style, self.get_mut(style)) { - (Style::Solid(color), Buffer::Solid(buffer)) => { - Box::new(tessellation::BuffersBuilder::new( - buffer, - TriangleVertex2DBuilder(color.into_linear()), - )) - } - (Style::Gradient(_), Buffer::Gradient(buffer, _)) => Box::new( - tessellation::BuffersBuilder::new(buffer, Vertex2DBuilder), - ), - _ => unreachable!(), - } - } - - fn get_stroke<'a>( - &'a mut self, - style: &Style, - ) -> Box { - match (style, self.get_mut(style)) { - (Style::Solid(color), Buffer::Solid(buffer)) => { - Box::new(tessellation::BuffersBuilder::new( - buffer, - TriangleVertex2DBuilder(color.into_linear()), - )) - } - (Style::Gradient(_), Buffer::Gradient(buffer, _)) => Box::new( - tessellation::BuffersBuilder::new(buffer, Vertex2DBuilder), - ), - _ => unreachable!(), - } - } -} - -#[derive(Debug)] -struct Transforms { - previous: Vec, - current: Transform, -} - -#[derive(Debug, Clone, Copy)] -struct Transform { - raw: lyon::math::Transform, - is_identity: bool, -} - -impl Transform { - /// Transforms the given [Point] by the transformation matrix. - fn transform_point(&self, point: &mut Point) { - let transformed = self - .raw - .transform_point(euclid::Point2D::new(point.x, point.y)); - point.x = transformed.x; - point.y = transformed.y; - } - - fn transform_style(&self, style: Style) -> Style { - match style { - Style::Solid(color) => Style::Solid(color), - Style::Gradient(gradient) => { - Style::Gradient(self.transform_gradient(gradient)) - } - } - } - - fn transform_gradient(&self, mut gradient: Gradient) -> Gradient { - let (start, end) = match &mut gradient { - Gradient::Linear(linear) => (&mut linear.start, &mut linear.end), - }; - self.transform_point(start); - self.transform_point(end); - gradient - } -} - -impl Frame { - /// Creates a new empty [`Frame`] with the given dimensions. - /// - /// The default coordinate system of a [`Frame`] has its origin at the - /// top-left corner of its bounds. - pub fn new(size: Size) -> Frame { - Frame { - size, - buffers: BufferStack::new(), - primitives: Vec::new(), - transforms: Transforms { - previous: Vec::new(), - current: Transform { - raw: lyon::math::Transform::identity(), - is_identity: true, - }, - }, - fill_tessellator: tessellation::FillTessellator::new(), - stroke_tessellator: tessellation::StrokeTessellator::new(), - } - } - - /// Returns the width of the [`Frame`]. - #[inline] - pub fn width(&self) -> f32 { - self.size.width - } - - /// Returns the height of the [`Frame`]. - #[inline] - pub fn height(&self) -> f32 { - self.size.height - } - - /// Returns the dimensions of the [`Frame`]. - #[inline] - pub fn size(&self) -> Size { - self.size - } - - /// Returns the coordinate of the center of the [`Frame`]. - #[inline] - pub fn center(&self) -> Point { - Point::new(self.size.width / 2.0, self.size.height / 2.0) - } - - /// Draws the given [`Path`] on the [`Frame`] by filling it with the - /// provided style. - pub fn fill(&mut self, path: &Path, fill: impl Into) { - let Fill { style, rule } = fill.into(); - - let mut buffer = self - .buffers - .get_fill(&self.transforms.current.transform_style(style)); - - let options = tessellation::FillOptions::default() - .with_fill_rule(into_fill_rule(rule)); - - if self.transforms.current.is_identity { - self.fill_tessellator.tessellate_path( - path.raw(), - &options, - buffer.as_mut(), - ) - } else { - let path = path.transform(&self.transforms.current.raw); - - self.fill_tessellator.tessellate_path( - path.raw(), - &options, - buffer.as_mut(), - ) - } - .expect("Tessellate path."); - } - - /// Draws an axis-aligned rectangle given its top-left corner coordinate and - /// its `Size` on the [`Frame`] by filling it with the provided style. - pub fn fill_rectangle( - &mut self, - top_left: Point, - size: Size, - fill: impl Into, - ) { - let Fill { style, rule } = fill.into(); - - let mut buffer = self - .buffers - .get_fill(&self.transforms.current.transform_style(style)); - - let top_left = - self.transforms.current.raw.transform_point( - lyon::math::Point::new(top_left.x, top_left.y), - ); - - let size = - self.transforms.current.raw.transform_vector( - lyon::math::Vector::new(size.width, size.height), - ); - - let options = tessellation::FillOptions::default() - .with_fill_rule(into_fill_rule(rule)); - - self.fill_tessellator - .tessellate_rectangle( - &lyon::math::Box2D::new(top_left, top_left + size), - &options, - buffer.as_mut(), - ) - .expect("Fill rectangle"); - } - - /// Draws the stroke of the given [`Path`] on the [`Frame`] with the - /// provided style. - pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { - let stroke = stroke.into(); - - let mut buffer = self - .buffers - .get_stroke(&self.transforms.current.transform_style(stroke.style)); - - let mut options = tessellation::StrokeOptions::default(); - options.line_width = stroke.width; - options.start_cap = into_line_cap(stroke.line_cap); - options.end_cap = into_line_cap(stroke.line_cap); - options.line_join = into_line_join(stroke.line_join); - - let path = if stroke.line_dash.segments.is_empty() { - Cow::Borrowed(path) - } else { - Cow::Owned(dashed(path, stroke.line_dash)) - }; - - if self.transforms.current.is_identity { - self.stroke_tessellator.tessellate_path( - path.raw(), - &options, - buffer.as_mut(), - ) - } else { - let path = path.transform(&self.transforms.current.raw); - - self.stroke_tessellator.tessellate_path( - path.raw(), - &options, - buffer.as_mut(), - ) - } - .expect("Stroke path"); - } - - /// Draws the characters of the given [`Text`] on the [`Frame`], filling - /// them with the given color. - /// - /// __Warning:__ Text currently does not work well with rotations and scale - /// transforms! The position will be correctly transformed, but the - /// resulting glyphs will not be rotated or scaled properly. - /// - /// Additionally, all text will be rendered on top of all the layers of - /// a [`Canvas`]. Therefore, it is currently only meant to be used for - /// overlays, which is the most common use case. - /// - /// Support for vectorial text is planned, and should address all these - /// limitations. - /// - /// [`Canvas`]: crate::widget::Canvas - pub fn fill_text(&mut self, text: impl Into) { - let text = text.into(); - - let position = if self.transforms.current.is_identity { - text.position - } else { - let transformed = self.transforms.current.raw.transform_point( - lyon::math::Point::new(text.position.x, text.position.y), - ); - - Point::new(transformed.x, transformed.y) - }; - - // TODO: Use vectorial text instead of primitive - self.primitives.push(Primitive::Text { - content: text.content, - bounds: Rectangle { - x: position.x, - y: position.y, - width: f32::INFINITY, - height: f32::INFINITY, - }, - color: text.color, - size: text.size, - font: text.font, - horizontal_alignment: text.horizontal_alignment, - vertical_alignment: text.vertical_alignment, - }); - } - - /// Stores the current transform of the [`Frame`] and executes the given - /// drawing operations, restoring the transform afterwards. - /// - /// This method is useful to compose transforms and perform drawing - /// operations in different coordinate systems. - #[inline] - pub fn with_save(&mut self, f: impl FnOnce(&mut Frame)) { - self.push_transform(); - - f(self); - - self.pop_transform(); - } - - pub fn push_transform(&mut self) { - self.transforms.previous.push(self.transforms.current); - } - - pub fn pop_transform(&mut self) { - self.transforms.current = self.transforms.previous.pop().unwrap(); - } - - /// Executes the given drawing operations within a [`Rectangle`] region, - /// clipping any geometry that overflows its bounds. Any transformations - /// performed are local to the provided closure. - /// - /// This method is useful to perform drawing operations that need to be - /// clipped. - #[inline] - pub fn with_clip(&mut self, region: Rectangle, f: impl FnOnce(&mut Frame)) { - let mut frame = Frame::new(region.size()); - - f(&mut frame); - - let translation = Vector::new(region.x, region.y); - - self.clip(frame, translation); - } - - pub fn clip(&mut self, frame: Frame, translation: Vector) { - let size = frame.size(); - let primitives = frame.into_primitives(); - - let (text, meshes) = primitives - .into_iter() - .partition(|primitive| matches!(primitive, Primitive::Text { .. })); - - self.primitives.push(Primitive::Group { - primitives: vec![ - Primitive::Translate { - translation, - content: Box::new(Primitive::Group { primitives: meshes }), - }, - Primitive::Translate { - translation, - content: Box::new(Primitive::Clip { - bounds: Rectangle::with_size(size), - content: Box::new(Primitive::Group { - primitives: text, - }), - }), - }, - ], - }); - } - - /// Applies a translation to the current transform of the [`Frame`]. - #[inline] - pub fn translate(&mut self, translation: Vector) { - self.transforms.current.raw = self - .transforms - .current - .raw - .pre_translate(lyon::math::Vector::new( - translation.x, - translation.y, - )); - self.transforms.current.is_identity = false; - } - - /// Applies a rotation in radians to the current transform of the [`Frame`]. - #[inline] - pub fn rotate(&mut self, angle: f32) { - self.transforms.current.raw = self - .transforms - .current - .raw - .pre_rotate(lyon::math::Angle::radians(angle)); - self.transforms.current.is_identity = false; - } - - /// Applies a scaling to the current transform of the [`Frame`]. - #[inline] - pub fn scale(&mut self, scale: f32) { - self.transforms.current.raw = - self.transforms.current.raw.pre_scale(scale, scale); - self.transforms.current.is_identity = false; - } - - /// Produces the [`Primitive`] representing everything drawn on the [`Frame`]. - pub fn into_primitive(self) -> Primitive { - Primitive::Group { - primitives: self.into_primitives(), - } - } - - fn into_primitives(mut self) -> Vec { - for buffer in self.buffers.stack { - match buffer { - Buffer::Solid(buffer) => { - if !buffer.indices.is_empty() { - self.primitives.push(Primitive::SolidMesh { - buffers: primitive::Mesh2D { - vertices: buffer.vertices, - indices: buffer.indices, - }, - size: self.size, - }) - } - } - Buffer::Gradient(buffer, gradient) => { - if !buffer.indices.is_empty() { - self.primitives.push(Primitive::GradientMesh { - buffers: primitive::Mesh2D { - vertices: buffer.vertices, - indices: buffer.indices, - }, - size: self.size, - gradient, - }) - } - } - } - } - - self.primitives - } -} - -struct Vertex2DBuilder; - -impl tessellation::FillVertexConstructor - for Vertex2DBuilder -{ - fn new_vertex( - &mut self, - vertex: tessellation::FillVertex<'_>, - ) -> primitive::Vertex2D { - let position = vertex.position(); - - primitive::Vertex2D { - position: [position.x, position.y], - } - } -} - -impl tessellation::StrokeVertexConstructor - for Vertex2DBuilder -{ - fn new_vertex( - &mut self, - vertex: tessellation::StrokeVertex<'_, '_>, - ) -> primitive::Vertex2D { - let position = vertex.position(); - - primitive::Vertex2D { - position: [position.x, position.y], - } - } -} - -struct TriangleVertex2DBuilder([f32; 4]); - -impl tessellation::FillVertexConstructor - for TriangleVertex2DBuilder -{ - fn new_vertex( - &mut self, - vertex: tessellation::FillVertex<'_>, - ) -> primitive::ColoredVertex2D { - let position = vertex.position(); - - primitive::ColoredVertex2D { - position: [position.x, position.y], - color: self.0, - } - } -} - -impl tessellation::StrokeVertexConstructor - for TriangleVertex2DBuilder -{ - fn new_vertex( - &mut self, - vertex: tessellation::StrokeVertex<'_, '_>, - ) -> primitive::ColoredVertex2D { - let position = vertex.position(); - - primitive::ColoredVertex2D { - position: [position.x, position.y], - color: self.0, - } - } -} - -fn into_line_join(line_join: LineJoin) -> lyon::tessellation::LineJoin { - match line_join { - LineJoin::Miter => lyon::tessellation::LineJoin::Miter, - LineJoin::Round => lyon::tessellation::LineJoin::Round, - LineJoin::Bevel => lyon::tessellation::LineJoin::Bevel, - } -} - -fn into_line_cap(line_cap: LineCap) -> lyon::tessellation::LineCap { - match line_cap { - LineCap::Butt => lyon::tessellation::LineCap::Butt, - LineCap::Square => lyon::tessellation::LineCap::Square, - LineCap::Round => lyon::tessellation::LineCap::Round, - } -} - -fn into_fill_rule(rule: fill::Rule) -> lyon::tessellation::FillRule { - match rule { - fill::Rule::NonZero => lyon::tessellation::FillRule::NonZero, - fill::Rule::EvenOdd => lyon::tessellation::FillRule::EvenOdd, - } -} - -pub(super) fn dashed(path: &Path, line_dash: LineDash<'_>) -> Path { - use lyon::algorithms::walk::{ - walk_along_path, RepeatedPattern, WalkerEvent, - }; - use lyon::path::iterator::PathIterator; - - Path::new(|builder| { - let segments_odd = (line_dash.segments.len() % 2 == 1) - .then(|| [line_dash.segments, line_dash.segments].concat()); - - let mut draw_line = false; - - walk_along_path( - path.raw().iter().flattened(0.01), - 0.0, - lyon::tessellation::StrokeOptions::DEFAULT_TOLERANCE, - &mut RepeatedPattern { - callback: |event: WalkerEvent<'_>| { - let point = Point { - x: event.position.x, - y: event.position.y, - }; - - if draw_line { - builder.line_to(point); - } else { - builder.move_to(point); - } - - draw_line = !draw_line; - - true - }, - index: line_dash.offset, - intervals: segments_odd - .as_deref() - .unwrap_or(line_dash.segments), - }, - ); - }) -} diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs new file mode 100644 index 00000000..11e8126f --- /dev/null +++ b/wgpu/src/geometry.rs @@ -0,0 +1,608 @@ +use iced_graphics::geometry::fill::{self, Fill}; +use iced_graphics::geometry::{ + LineCap, LineDash, LineJoin, Path, Stroke, Style, Text, +}; +use iced_graphics::primitive::{self, Primitive}; +use iced_graphics::{Gradient, Point, Rectangle, Size, Vector}; + +use lyon::geom::euclid; +use lyon::tessellation; +use std::borrow::Cow; + +/// The frame of a [`Canvas`]. +/// +/// [`Canvas`]: crate::widget::Canvas +#[allow(missing_debug_implementations)] +pub struct Frame { + size: Size, + buffers: BufferStack, + primitives: Vec, + transforms: Transforms, + fill_tessellator: tessellation::FillTessellator, + stroke_tessellator: tessellation::StrokeTessellator, +} + +enum Buffer { + Solid(tessellation::VertexBuffers), + Gradient( + tessellation::VertexBuffers, + Gradient, + ), +} + +struct BufferStack { + stack: Vec, +} + +impl BufferStack { + fn new() -> Self { + Self { stack: Vec::new() } + } + + fn get_mut(&mut self, style: &Style) -> &mut Buffer { + match style { + Style::Solid(_) => match self.stack.last() { + Some(Buffer::Solid(_)) => {} + _ => { + self.stack.push(Buffer::Solid( + tessellation::VertexBuffers::new(), + )); + } + }, + Style::Gradient(gradient) => match self.stack.last() { + Some(Buffer::Gradient(_, last)) if gradient == last => {} + _ => { + self.stack.push(Buffer::Gradient( + tessellation::VertexBuffers::new(), + gradient.clone(), + )); + } + }, + } + + self.stack.last_mut().unwrap() + } + + fn get_fill<'a>( + &'a mut self, + style: &Style, + ) -> Box { + match (style, self.get_mut(style)) { + (Style::Solid(color), Buffer::Solid(buffer)) => { + Box::new(tessellation::BuffersBuilder::new( + buffer, + TriangleVertex2DBuilder(color.into_linear()), + )) + } + (Style::Gradient(_), Buffer::Gradient(buffer, _)) => Box::new( + tessellation::BuffersBuilder::new(buffer, Vertex2DBuilder), + ), + _ => unreachable!(), + } + } + + fn get_stroke<'a>( + &'a mut self, + style: &Style, + ) -> Box { + match (style, self.get_mut(style)) { + (Style::Solid(color), Buffer::Solid(buffer)) => { + Box::new(tessellation::BuffersBuilder::new( + buffer, + TriangleVertex2DBuilder(color.into_linear()), + )) + } + (Style::Gradient(_), Buffer::Gradient(buffer, _)) => Box::new( + tessellation::BuffersBuilder::new(buffer, Vertex2DBuilder), + ), + _ => unreachable!(), + } + } +} + +#[derive(Debug)] +struct Transforms { + previous: Vec, + current: Transform, +} + +#[derive(Debug, Clone, Copy)] +struct Transform { + raw: lyon::math::Transform, + is_identity: bool, +} + +impl Transform { + /// Transforms the given [Point] by the transformation matrix. + fn transform_point(&self, point: &mut Point) { + let transformed = self + .raw + .transform_point(euclid::Point2D::new(point.x, point.y)); + point.x = transformed.x; + point.y = transformed.y; + } + + fn transform_style(&self, style: Style) -> Style { + match style { + Style::Solid(color) => Style::Solid(color), + Style::Gradient(gradient) => { + Style::Gradient(self.transform_gradient(gradient)) + } + } + } + + fn transform_gradient(&self, mut gradient: Gradient) -> Gradient { + let (start, end) = match &mut gradient { + Gradient::Linear(linear) => (&mut linear.start, &mut linear.end), + }; + self.transform_point(start); + self.transform_point(end); + gradient + } +} + +impl Frame { + /// Creates a new empty [`Frame`] with the given dimensions. + /// + /// The default coordinate system of a [`Frame`] has its origin at the + /// top-left corner of its bounds. + pub fn new(size: Size) -> Frame { + Frame { + size, + buffers: BufferStack::new(), + primitives: Vec::new(), + transforms: Transforms { + previous: Vec::new(), + current: Transform { + raw: lyon::math::Transform::identity(), + is_identity: true, + }, + }, + fill_tessellator: tessellation::FillTessellator::new(), + stroke_tessellator: tessellation::StrokeTessellator::new(), + } + } + + /// Returns the width of the [`Frame`]. + #[inline] + pub fn width(&self) -> f32 { + self.size.width + } + + /// Returns the height of the [`Frame`]. + #[inline] + pub fn height(&self) -> f32 { + self.size.height + } + + /// Returns the dimensions of the [`Frame`]. + #[inline] + pub fn size(&self) -> Size { + self.size + } + + /// Returns the coordinate of the center of the [`Frame`]. + #[inline] + pub fn center(&self) -> Point { + Point::new(self.size.width / 2.0, self.size.height / 2.0) + } + + /// Draws the given [`Path`] on the [`Frame`] by filling it with the + /// provided style. + pub fn fill(&mut self, path: &Path, fill: impl Into) { + let Fill { style, rule } = fill.into(); + + let mut buffer = self + .buffers + .get_fill(&self.transforms.current.transform_style(style)); + + let options = tessellation::FillOptions::default() + .with_fill_rule(into_fill_rule(rule)); + + if self.transforms.current.is_identity { + self.fill_tessellator.tessellate_path( + path.raw(), + &options, + buffer.as_mut(), + ) + } else { + let path = path.transform(&self.transforms.current.raw); + + self.fill_tessellator.tessellate_path( + path.raw(), + &options, + buffer.as_mut(), + ) + } + .expect("Tessellate path."); + } + + /// Draws an axis-aligned rectangle given its top-left corner coordinate and + /// its `Size` on the [`Frame`] by filling it with the provided style. + pub fn fill_rectangle( + &mut self, + top_left: Point, + size: Size, + fill: impl Into, + ) { + let Fill { style, rule } = fill.into(); + + let mut buffer = self + .buffers + .get_fill(&self.transforms.current.transform_style(style)); + + let top_left = + self.transforms.current.raw.transform_point( + lyon::math::Point::new(top_left.x, top_left.y), + ); + + let size = + self.transforms.current.raw.transform_vector( + lyon::math::Vector::new(size.width, size.height), + ); + + let options = tessellation::FillOptions::default() + .with_fill_rule(into_fill_rule(rule)); + + self.fill_tessellator + .tessellate_rectangle( + &lyon::math::Box2D::new(top_left, top_left + size), + &options, + buffer.as_mut(), + ) + .expect("Fill rectangle"); + } + + /// Draws the stroke of the given [`Path`] on the [`Frame`] with the + /// provided style. + pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { + let stroke = stroke.into(); + + let mut buffer = self + .buffers + .get_stroke(&self.transforms.current.transform_style(stroke.style)); + + let mut options = tessellation::StrokeOptions::default(); + options.line_width = stroke.width; + options.start_cap = into_line_cap(stroke.line_cap); + options.end_cap = into_line_cap(stroke.line_cap); + options.line_join = into_line_join(stroke.line_join); + + let path = if stroke.line_dash.segments.is_empty() { + Cow::Borrowed(path) + } else { + Cow::Owned(dashed(path, stroke.line_dash)) + }; + + if self.transforms.current.is_identity { + self.stroke_tessellator.tessellate_path( + path.raw(), + &options, + buffer.as_mut(), + ) + } else { + let path = path.transform(&self.transforms.current.raw); + + self.stroke_tessellator.tessellate_path( + path.raw(), + &options, + buffer.as_mut(), + ) + } + .expect("Stroke path"); + } + + /// Draws the characters of the given [`Text`] on the [`Frame`], filling + /// them with the given color. + /// + /// __Warning:__ Text currently does not work well with rotations and scale + /// transforms! The position will be correctly transformed, but the + /// resulting glyphs will not be rotated or scaled properly. + /// + /// Additionally, all text will be rendered on top of all the layers of + /// a [`Canvas`]. Therefore, it is currently only meant to be used for + /// overlays, which is the most common use case. + /// + /// Support for vectorial text is planned, and should address all these + /// limitations. + /// + /// [`Canvas`]: crate::widget::Canvas + pub fn fill_text(&mut self, text: impl Into) { + let text = text.into(); + + let position = if self.transforms.current.is_identity { + text.position + } else { + let transformed = self.transforms.current.raw.transform_point( + lyon::math::Point::new(text.position.x, text.position.y), + ); + + Point::new(transformed.x, transformed.y) + }; + + // TODO: Use vectorial text instead of primitive + self.primitives.push(Primitive::Text { + content: text.content, + bounds: Rectangle { + x: position.x, + y: position.y, + width: f32::INFINITY, + height: f32::INFINITY, + }, + color: text.color, + size: text.size, + font: text.font, + horizontal_alignment: text.horizontal_alignment, + vertical_alignment: text.vertical_alignment, + }); + } + + /// Stores the current transform of the [`Frame`] and executes the given + /// drawing operations, restoring the transform afterwards. + /// + /// This method is useful to compose transforms and perform drawing + /// operations in different coordinate systems. + #[inline] + pub fn with_save(&mut self, f: impl FnOnce(&mut Frame)) { + self.push_transform(); + + f(self); + + self.pop_transform(); + } + + pub fn push_transform(&mut self) { + self.transforms.previous.push(self.transforms.current); + } + + pub fn pop_transform(&mut self) { + self.transforms.current = self.transforms.previous.pop().unwrap(); + } + + /// Executes the given drawing operations within a [`Rectangle`] region, + /// clipping any geometry that overflows its bounds. Any transformations + /// performed are local to the provided closure. + /// + /// This method is useful to perform drawing operations that need to be + /// clipped. + #[inline] + pub fn with_clip(&mut self, region: Rectangle, f: impl FnOnce(&mut Frame)) { + let mut frame = Frame::new(region.size()); + + f(&mut frame); + + let translation = Vector::new(region.x, region.y); + + self.clip(frame, translation); + } + + pub fn clip(&mut self, frame: Frame, translation: Vector) { + let size = frame.size(); + let primitives = frame.into_primitives(); + + let (text, meshes) = primitives + .into_iter() + .partition(|primitive| matches!(primitive, Primitive::Text { .. })); + + self.primitives.push(Primitive::Group { + primitives: vec![ + Primitive::Translate { + translation, + content: Box::new(Primitive::Group { primitives: meshes }), + }, + Primitive::Translate { + translation, + content: Box::new(Primitive::Clip { + bounds: Rectangle::with_size(size), + content: Box::new(Primitive::Group { + primitives: text, + }), + }), + }, + ], + }); + } + + /// Applies a translation to the current transform of the [`Frame`]. + #[inline] + pub fn translate(&mut self, translation: Vector) { + self.transforms.current.raw = self + .transforms + .current + .raw + .pre_translate(lyon::math::Vector::new( + translation.x, + translation.y, + )); + self.transforms.current.is_identity = false; + } + + /// Applies a rotation in radians to the current transform of the [`Frame`]. + #[inline] + pub fn rotate(&mut self, angle: f32) { + self.transforms.current.raw = self + .transforms + .current + .raw + .pre_rotate(lyon::math::Angle::radians(angle)); + self.transforms.current.is_identity = false; + } + + /// Applies a scaling to the current transform of the [`Frame`]. + #[inline] + pub fn scale(&mut self, scale: f32) { + self.transforms.current.raw = + self.transforms.current.raw.pre_scale(scale, scale); + self.transforms.current.is_identity = false; + } + + /// Produces the [`Primitive`] representing everything drawn on the [`Frame`]. + pub fn into_primitive(self) -> Primitive { + Primitive::Group { + primitives: self.into_primitives(), + } + } + + fn into_primitives(mut self) -> Vec { + for buffer in self.buffers.stack { + match buffer { + Buffer::Solid(buffer) => { + if !buffer.indices.is_empty() { + self.primitives.push(Primitive::SolidMesh { + buffers: primitive::Mesh2D { + vertices: buffer.vertices, + indices: buffer.indices, + }, + size: self.size, + }) + } + } + Buffer::Gradient(buffer, gradient) => { + if !buffer.indices.is_empty() { + self.primitives.push(Primitive::GradientMesh { + buffers: primitive::Mesh2D { + vertices: buffer.vertices, + indices: buffer.indices, + }, + size: self.size, + gradient, + }) + } + } + } + } + + self.primitives + } +} + +struct Vertex2DBuilder; + +impl tessellation::FillVertexConstructor + for Vertex2DBuilder +{ + fn new_vertex( + &mut self, + vertex: tessellation::FillVertex<'_>, + ) -> primitive::Vertex2D { + let position = vertex.position(); + + primitive::Vertex2D { + position: [position.x, position.y], + } + } +} + +impl tessellation::StrokeVertexConstructor + for Vertex2DBuilder +{ + fn new_vertex( + &mut self, + vertex: tessellation::StrokeVertex<'_, '_>, + ) -> primitive::Vertex2D { + let position = vertex.position(); + + primitive::Vertex2D { + position: [position.x, position.y], + } + } +} + +struct TriangleVertex2DBuilder([f32; 4]); + +impl tessellation::FillVertexConstructor + for TriangleVertex2DBuilder +{ + fn new_vertex( + &mut self, + vertex: tessellation::FillVertex<'_>, + ) -> primitive::ColoredVertex2D { + let position = vertex.position(); + + primitive::ColoredVertex2D { + position: [position.x, position.y], + color: self.0, + } + } +} + +impl tessellation::StrokeVertexConstructor + for TriangleVertex2DBuilder +{ + fn new_vertex( + &mut self, + vertex: tessellation::StrokeVertex<'_, '_>, + ) -> primitive::ColoredVertex2D { + let position = vertex.position(); + + primitive::ColoredVertex2D { + position: [position.x, position.y], + color: self.0, + } + } +} + +fn into_line_join(line_join: LineJoin) -> lyon::tessellation::LineJoin { + match line_join { + LineJoin::Miter => lyon::tessellation::LineJoin::Miter, + LineJoin::Round => lyon::tessellation::LineJoin::Round, + LineJoin::Bevel => lyon::tessellation::LineJoin::Bevel, + } +} + +fn into_line_cap(line_cap: LineCap) -> lyon::tessellation::LineCap { + match line_cap { + LineCap::Butt => lyon::tessellation::LineCap::Butt, + LineCap::Square => lyon::tessellation::LineCap::Square, + LineCap::Round => lyon::tessellation::LineCap::Round, + } +} + +fn into_fill_rule(rule: fill::Rule) -> lyon::tessellation::FillRule { + match rule { + fill::Rule::NonZero => lyon::tessellation::FillRule::NonZero, + fill::Rule::EvenOdd => lyon::tessellation::FillRule::EvenOdd, + } +} + +pub(super) fn dashed(path: &Path, line_dash: LineDash<'_>) -> Path { + use lyon::algorithms::walk::{ + walk_along_path, RepeatedPattern, WalkerEvent, + }; + use lyon::path::iterator::PathIterator; + + Path::new(|builder| { + let segments_odd = (line_dash.segments.len() % 2 == 1) + .then(|| [line_dash.segments, line_dash.segments].concat()); + + let mut draw_line = false; + + walk_along_path( + path.raw().iter().flattened(0.01), + 0.0, + lyon::tessellation::StrokeOptions::DEFAULT_TOLERANCE, + &mut RepeatedPattern { + callback: |event: WalkerEvent<'_>| { + let point = Point { + x: event.position.x, + y: event.position.y, + }; + + if draw_line { + builder.line_to(point); + } else { + builder.move_to(point); + } + + draw_line = !draw_line; + + true + }, + index: line_dash.offset, + intervals: segments_odd + .as_deref() + .unwrap_or(line_dash.segments), + }, + ); + }) +} diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 31db16a8..4439b185 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -41,8 +41,8 @@ pub mod layer; pub mod settings; pub mod window; -#[cfg(feature = "canvas")] -pub mod canvas; +#[cfg(feature = "geometry")] +pub mod geometry; mod backend; mod buffer; -- cgit From c54409d1711e1f615c7ea4b02c082954e340632a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 3 Mar 2023 17:31:44 +0100 Subject: Remove `canvas` leftovers in `iced_native` --- native/src/widget.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/native/src/widget.rs b/native/src/widget.rs index f107cd69..2b3ca7be 100644 --- a/native/src/widget.rs +++ b/native/src/widget.rs @@ -83,14 +83,6 @@ pub use tree::Tree; #[doc(no_inline)] pub use vertical_slider::VerticalSlider; -#[cfg(feature = "canvas")] -#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] -pub mod canvas; - -#[cfg(feature = "canvas")] -#[doc(no_inline)] -pub use canvas::Canvas; - pub use action::Action; pub use id::Id; pub use operation::Operation; -- cgit From 12781c717a08bf0e7bfb2594e568f89af3676d52 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 3 Mar 2023 20:45:49 +0100 Subject: Expose `window` commands for Wasm builds --- examples/todos/src/main.rs | 15 ++++----------- src/window.rs | 1 - 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 0f5bfe30..6a87f58c 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -8,8 +8,6 @@ use iced::widget::{ text_input, Text, }; use iced::window; -#[cfg(not(target_arch = "wasm32"))] -use iced::window::Mode; use iced::{Application, Element}; use iced::{Color, Command, Font, Length, Settings, Subscription}; @@ -51,11 +49,8 @@ enum Message { CreateTask, FilterChanged(Filter), TaskMessage(usize, TaskMessage), - TabPressed { - shift: bool, - }, - #[cfg(not(target_arch = "wasm32"))] - ToggleFullscreen(Mode), + TabPressed { shift: bool }, + ToggleFullscreen(window::Mode), } impl Application for Todos { @@ -162,7 +157,6 @@ impl Application for Todos { widget::focus_next() } } - #[cfg(not(target_arch = "wasm32"))] Message::ToggleFullscreen(mode) => { window::change_mode(mode) } @@ -276,7 +270,6 @@ impl Application for Todos { ) => Some(Message::TabPressed { shift: modifiers.shift(), }), - #[cfg(not(target_arch = "wasm32"))] ( Event::Keyboard(keyboard::Event::KeyPressed { key_code, @@ -285,10 +278,10 @@ impl Application for Todos { event::Status::Ignored, ) => match key_code { KeyCode::Up => { - Some(Message::ToggleFullscreen(Mode::Fullscreen)) + Some(Message::ToggleFullscreen(window::Mode::Fullscreen)) } KeyCode::Down => { - Some(Message::ToggleFullscreen(Mode::Windowed)) + Some(Message::ToggleFullscreen(window::Mode::Windowed)) } _ => None, }, diff --git a/src/window.rs b/src/window.rs index 2018053f..5a199580 100644 --- a/src/window.rs +++ b/src/window.rs @@ -8,5 +8,4 @@ pub use icon::Icon; pub use position::Position; pub use settings::Settings; -#[cfg(not(target_arch = "wasm32"))] pub use crate::runtime::window::*; -- cgit From 3a0d34c0240f4421737a6a08761f99d6f8140d02 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 4 Mar 2023 05:37:11 +0100 Subject: Create `iced_widget` subcrate and re-organize the whole codebase --- Cargo.toml | 29 +- core/Cargo.toml | 1 + core/src/clipboard.rs | 23 + core/src/element.rs | 608 ++++++++++++ core/src/event.rs | 78 ++ core/src/hasher.rs | 13 + core/src/image.rs | 174 ++++ core/src/layout.rs | 65 ++ core/src/layout/DRUID_LICENSE | 202 ++++ core/src/layout/flex.rs | 232 +++++ core/src/layout/limits.rs | 163 ++++ core/src/layout/node.rs | 91 ++ core/src/lib.rs | 24 + core/src/mouse.rs | 3 + core/src/mouse/click.rs | 76 ++ core/src/overlay.rs | 122 +++ core/src/overlay/element.rs | 270 ++++++ core/src/overlay/group.rs | 172 ++++ core/src/renderer.rs | 99 ++ core/src/renderer/null.rs | 82 ++ core/src/shell.rs | 108 +++ core/src/svg.rs | 89 ++ core/src/text.rs | 111 +++ core/src/touch.rs | 23 + core/src/widget.rs | 145 +++ core/src/widget/id.rs | 43 + core/src/widget/operation.rs | 112 +++ core/src/widget/operation/focusable.rs | 203 ++++ core/src/widget/operation/scrollable.rs | 54 ++ core/src/widget/operation/text_input.rs | 131 +++ core/src/widget/text.rs | 277 ++++++ core/src/widget/tree.rs | 187 ++++ core/src/window.rs | 10 + core/src/window/event.rs | 58 ++ core/src/window/mode.rs | 12 + core/src/window/redraw_request.rs | 38 + core/src/window/user_attention.rs | 21 + examples/component/Cargo.toml | 4 +- examples/component/src/main.rs | 22 +- examples/custom_quad/Cargo.toml | 3 +- examples/custom_quad/src/main.rs | 8 +- examples/custom_widget/Cargo.toml | 3 +- examples/custom_widget/src/main.rs | 8 +- examples/events/Cargo.toml | 1 - examples/events/src/main.rs | 9 +- examples/geometry/Cargo.toml | 3 +- examples/geometry/src/main.rs | 38 +- examples/integration/Cargo.toml | 1 + examples/integration/src/controls.rs | 10 +- examples/integration/src/main.rs | 91 +- examples/integration/src/scene.rs | 2 +- examples/lazy/Cargo.toml | 3 +- examples/lazy/src/main.rs | 3 +- examples/modal/Cargo.toml | 3 +- examples/modal/src/main.rs | 44 +- examples/pane_grid/Cargo.toml | 4 +- examples/pane_grid/src/main.rs | 8 +- examples/toast/Cargo.toml | 3 +- examples/toast/src/main.rs | 24 +- examples/url_handler/Cargo.toml | 1 - examples/url_handler/src/main.rs | 10 +- graphics/Cargo.toml | 10 +- graphics/src/backend.rs | 8 +- graphics/src/compositor.rs | 92 ++ graphics/src/geometry.rs | 4 +- graphics/src/geometry/fill.rs | 2 +- graphics/src/geometry/path.rs | 2 +- graphics/src/geometry/path/arc.rs | 2 +- graphics/src/geometry/path/builder.rs | 3 +- graphics/src/geometry/stroke.rs | 2 +- graphics/src/geometry/style.rs | 2 +- graphics/src/geometry/text.rs | 4 +- graphics/src/image/raster.rs | 4 +- graphics/src/image/storage.rs | 2 +- graphics/src/image/vector.rs | 5 +- graphics/src/lib.rs | 11 +- graphics/src/overlay.rs | 2 - graphics/src/overlay/menu.rs | 3 - graphics/src/primitive.rs | 9 +- graphics/src/renderer.rs | 20 +- graphics/src/viewport.rs | 4 +- graphics/src/window.rs | 10 - graphics/src/window/compositor.rs | 90 -- graphics/src/window/gl_compositor.rs | 71 -- lazy/Cargo.toml | 18 - lazy/src/cache.rs | 13 - lazy/src/component.rs | 575 ----------- lazy/src/lazy.rs | 396 -------- lazy/src/lib.rs | 66 -- lazy/src/responsive.rs | 425 --------- native/Cargo.toml | 7 - native/src/clipboard.rs | 22 - native/src/command.rs | 4 +- native/src/debug/basic.rs | 2 +- native/src/element.rs | 583 ----------- native/src/event.rs | 78 -- native/src/hasher.rs | 13 - native/src/image.rs | 174 ---- native/src/layout.rs | 65 -- native/src/layout/DRUID_LICENSE | 202 ---- native/src/layout/flex.rs | 232 ----- native/src/layout/limits.rs | 163 ---- native/src/layout/node.rs | 91 -- native/src/lib.rs | 39 +- native/src/mouse.rs | 6 - native/src/mouse/click.rs | 76 -- native/src/overlay.rs | 125 --- native/src/overlay/element.rs | 270 ------ native/src/overlay/group.rs | 174 ---- native/src/overlay/menu.rs | 519 ---------- native/src/program.rs | 6 +- native/src/program/state.rs | 18 +- native/src/renderer.rs | 98 -- native/src/renderer/null.rs | 82 -- native/src/runtime.rs | 4 +- native/src/shell.rs | 108 --- native/src/subscription.rs | 17 +- native/src/svg.rs | 89 -- native/src/text.rs | 111 --- native/src/touch.rs | 23 - native/src/user_interface.rs | 82 +- native/src/widget.rs | 206 ---- native/src/widget/action.rs | 5 +- native/src/widget/button.rs | 455 --------- native/src/widget/checkbox.rs | 321 ------- native/src/widget/column.rs | 264 ----- native/src/widget/container.rs | 368 ------- native/src/widget/helpers.rs | 317 ------ native/src/widget/id.rs | 43 - native/src/widget/image.rs | 204 ---- native/src/widget/image/viewer.rs | 428 --------- native/src/widget/operation.rs | 112 --- native/src/widget/operation/focusable.rs | 203 ---- native/src/widget/operation/scrollable.rs | 54 -- native/src/widget/operation/text_input.rs | 131 --- native/src/widget/pane_grid.rs | 991 ------------------- native/src/widget/pane_grid/axis.rs | 241 ----- native/src/widget/pane_grid/configuration.rs | 26 - native/src/widget/pane_grid/content.rs | 373 -------- native/src/widget/pane_grid/direction.rs | 12 - native/src/widget/pane_grid/draggable.rs | 12 - native/src/widget/pane_grid/node.rs | 250 ----- native/src/widget/pane_grid/pane.rs | 5 - native/src/widget/pane_grid/split.rs | 5 - native/src/widget/pane_grid/state.rs | 350 ------- native/src/widget/pane_grid/title_bar.rs | 432 --------- native/src/widget/pick_list.rs | 657 ------------- native/src/widget/progress_bar.rs | 168 ---- native/src/widget/radio.rs | 299 ------ native/src/widget/row.rs | 253 ----- native/src/widget/rule.rs | 147 --- native/src/widget/scrollable.rs | 1327 -------------------------- native/src/widget/slider.rs | 473 --------- native/src/widget/space.rs | 85 -- native/src/widget/svg.rs | 195 ---- native/src/widget/text.rs | 263 ----- native/src/widget/text_input.rs | 1218 ----------------------- native/src/widget/text_input/cursor.rs | 189 ---- native/src/widget/text_input/editor.rs | 70 -- native/src/widget/text_input/value.rs | 133 --- native/src/widget/toggler.rs | 324 ------- native/src/widget/tooltip.rs | 387 -------- native/src/widget/tree.rs | 187 ---- native/src/widget/vertical_slider.rs | 468 --------- native/src/window.rs | 13 +- native/src/window/action.rs | 4 +- native/src/window/event.rs | 58 -- native/src/window/mode.rs | 12 - native/src/window/redraw_request.rs | 38 - native/src/window/user_attention.rs | 21 - renderer/Cargo.toml | 4 - renderer/src/backend.rs | 11 +- renderer/src/compositor.rs | 133 +++ renderer/src/geometry.rs | 6 +- renderer/src/geometry/cache.rs | 4 +- renderer/src/lib.rs | 14 +- renderer/src/settings.rs | 3 +- renderer/src/window.rs | 3 - renderer/src/window/compositor.rs | 132 --- src/advanced.rs | 9 + src/application.rs | 12 +- src/clipboard.rs | 3 - src/element.rs | 5 - src/error.rs | 16 +- src/executor.rs | 14 - src/keyboard.rs | 2 - src/lib.rs | 134 ++- src/mouse.rs | 2 - src/overlay.rs | 18 - src/result.rs | 6 - src/touch.rs | 2 +- src/widget.rs | 239 ----- src/widget/canvas.rs | 238 ----- src/widget/canvas/cursor.rs | 64 -- src/widget/canvas/event.rs | 21 - src/widget/canvas/program.rs | 108 --- src/widget/qr_code.rs | 300 ------ src/window.rs | 5 +- style/src/lib.rs | 3 +- style/src/text.rs | 2 +- style/src/theme.rs | 2 +- tiny_skia/src/backend.rs | 18 +- tiny_skia/src/geometry.rs | 12 +- tiny_skia/src/lib.rs | 11 +- tiny_skia/src/settings.rs | 2 +- tiny_skia/src/text.rs | 11 +- tiny_skia/src/window/compositor.rs | 9 +- wgpu/Cargo.toml | 4 - wgpu/src/backend.rs | 20 +- wgpu/src/geometry.rs | 8 +- wgpu/src/image.rs | 21 +- wgpu/src/image/atlas.rs | 4 +- wgpu/src/image/atlas/allocation.rs | 3 +- wgpu/src/image/atlas/allocator.rs | 4 +- wgpu/src/image/atlas/entry.rs | 5 +- wgpu/src/layer.rs | 9 +- wgpu/src/layer/image.rs | 6 +- wgpu/src/layer/mesh.rs | 4 +- wgpu/src/layer/text.rs | 3 +- wgpu/src/lib.rs | 15 +- wgpu/src/quad.rs | 6 +- wgpu/src/settings.rs | 5 +- wgpu/src/text.rs | 14 +- wgpu/src/triangle.rs | 24 +- wgpu/src/triangle/msaa.rs | 4 +- wgpu/src/window/compositor.rs | 10 +- widget/Cargo.toml | 37 + widget/src/button.rs | 455 +++++++++ widget/src/canvas.rs | 238 +++++ widget/src/canvas/cursor.rs | 64 ++ widget/src/canvas/event.rs | 21 + widget/src/canvas/program.rs | 109 +++ widget/src/checkbox.rs | 323 +++++++ widget/src/column.rs | 264 +++++ widget/src/container.rs | 368 +++++++ widget/src/helpers.rs | 362 +++++++ widget/src/image.rs | 205 ++++ widget/src/image/viewer.rs | 428 +++++++++ widget/src/lazy.rs | 409 ++++++++ widget/src/lazy/cache.rs | 13 + widget/src/lazy/component.rs | 575 +++++++++++ widget/src/lazy/helpers.rs | 39 + widget/src/lazy/responsive.rs | 427 +++++++++ widget/src/lib.rs | 122 +++ widget/src/overlay.rs | 1 + widget/src/overlay/menu.rs | 519 ++++++++++ widget/src/pane_grid.rs | 991 +++++++++++++++++++ widget/src/pane_grid/axis.rs | 241 +++++ widget/src/pane_grid/configuration.rs | 26 + widget/src/pane_grid/content.rs | 373 ++++++++ widget/src/pane_grid/direction.rs | 12 + widget/src/pane_grid/draggable.rs | 12 + widget/src/pane_grid/node.rs | 250 +++++ widget/src/pane_grid/pane.rs | 5 + widget/src/pane_grid/split.rs | 5 + widget/src/pane_grid/state.rs | 348 +++++++ widget/src/pane_grid/title_bar.rs | 432 +++++++++ widget/src/pick_list.rs | 658 +++++++++++++ widget/src/progress_bar.rs | 172 ++++ widget/src/qr_code.rs | 297 ++++++ widget/src/radio.rs | 300 ++++++ widget/src/row.rs | 253 +++++ widget/src/rule.rs | 147 +++ widget/src/scrollable.rs | 1325 +++++++++++++++++++++++++ widget/src/slider.rs | 471 +++++++++ widget/src/space.rs | 86 ++ widget/src/svg.rs | 195 ++++ widget/src/text.rs | 4 + widget/src/text_input.rs | 1221 ++++++++++++++++++++++++ widget/src/text_input/cursor.rs | 189 ++++ widget/src/text_input/editor.rs | 70 ++ widget/src/text_input/value.rs | 133 +++ widget/src/toggler.rs | 326 +++++++ widget/src/tooltip.rs | 388 ++++++++ widget/src/vertical_slider.rs | 471 +++++++++ winit/Cargo.toml | 6 +- winit/src/application.rs | 77 +- winit/src/application/state.rs | 14 +- winit/src/clipboard.rs | 7 +- winit/src/conversion.rs | 11 +- winit/src/error.rs | 7 +- winit/src/lib.rs | 8 +- winit/src/proxy.rs | 2 +- winit/src/system.rs | 7 +- winit/src/window.rs | 52 +- 285 files changed, 19342 insertions(+), 19566 deletions(-) create mode 100644 core/src/clipboard.rs create mode 100644 core/src/element.rs create mode 100644 core/src/event.rs create mode 100644 core/src/hasher.rs create mode 100644 core/src/image.rs create mode 100644 core/src/layout.rs create mode 100644 core/src/layout/DRUID_LICENSE create mode 100644 core/src/layout/flex.rs create mode 100644 core/src/layout/limits.rs create mode 100644 core/src/layout/node.rs create mode 100644 core/src/mouse/click.rs create mode 100644 core/src/overlay.rs create mode 100644 core/src/overlay/element.rs create mode 100644 core/src/overlay/group.rs create mode 100644 core/src/renderer.rs create mode 100644 core/src/renderer/null.rs create mode 100644 core/src/shell.rs create mode 100644 core/src/svg.rs create mode 100644 core/src/text.rs create mode 100644 core/src/touch.rs create mode 100644 core/src/widget.rs create mode 100644 core/src/widget/id.rs create mode 100644 core/src/widget/operation.rs create mode 100644 core/src/widget/operation/focusable.rs create mode 100644 core/src/widget/operation/scrollable.rs create mode 100644 core/src/widget/operation/text_input.rs create mode 100644 core/src/widget/text.rs create mode 100644 core/src/widget/tree.rs create mode 100644 core/src/window.rs create mode 100644 core/src/window/event.rs create mode 100644 core/src/window/mode.rs create mode 100644 core/src/window/redraw_request.rs create mode 100644 core/src/window/user_attention.rs create mode 100644 graphics/src/compositor.rs delete mode 100644 graphics/src/overlay.rs delete mode 100644 graphics/src/overlay/menu.rs delete mode 100644 graphics/src/window.rs delete mode 100644 graphics/src/window/compositor.rs delete mode 100644 graphics/src/window/gl_compositor.rs delete mode 100644 lazy/Cargo.toml delete mode 100644 lazy/src/cache.rs delete mode 100644 lazy/src/component.rs delete mode 100644 lazy/src/lazy.rs delete mode 100644 lazy/src/lib.rs delete mode 100644 lazy/src/responsive.rs delete mode 100644 native/src/element.rs delete mode 100644 native/src/event.rs delete mode 100644 native/src/hasher.rs delete mode 100644 native/src/image.rs delete mode 100644 native/src/layout.rs delete mode 100644 native/src/layout/DRUID_LICENSE delete mode 100644 native/src/layout/flex.rs delete mode 100644 native/src/layout/limits.rs delete mode 100644 native/src/layout/node.rs delete mode 100644 native/src/mouse.rs delete mode 100644 native/src/mouse/click.rs delete mode 100644 native/src/overlay.rs delete mode 100644 native/src/overlay/element.rs delete mode 100644 native/src/overlay/group.rs delete mode 100644 native/src/overlay/menu.rs delete mode 100644 native/src/renderer.rs delete mode 100644 native/src/renderer/null.rs delete mode 100644 native/src/shell.rs delete mode 100644 native/src/svg.rs delete mode 100644 native/src/text.rs delete mode 100644 native/src/touch.rs delete mode 100644 native/src/widget/button.rs delete mode 100644 native/src/widget/checkbox.rs delete mode 100644 native/src/widget/column.rs delete mode 100644 native/src/widget/container.rs delete mode 100644 native/src/widget/helpers.rs delete mode 100644 native/src/widget/id.rs delete mode 100644 native/src/widget/image.rs delete mode 100644 native/src/widget/image/viewer.rs delete mode 100644 native/src/widget/operation.rs delete mode 100644 native/src/widget/operation/focusable.rs delete mode 100644 native/src/widget/operation/scrollable.rs delete mode 100644 native/src/widget/operation/text_input.rs delete mode 100644 native/src/widget/pane_grid.rs delete mode 100644 native/src/widget/pane_grid/axis.rs delete mode 100644 native/src/widget/pane_grid/configuration.rs delete mode 100644 native/src/widget/pane_grid/content.rs delete mode 100644 native/src/widget/pane_grid/direction.rs delete mode 100644 native/src/widget/pane_grid/draggable.rs delete mode 100644 native/src/widget/pane_grid/node.rs delete mode 100644 native/src/widget/pane_grid/pane.rs delete mode 100644 native/src/widget/pane_grid/split.rs delete mode 100644 native/src/widget/pane_grid/state.rs delete mode 100644 native/src/widget/pane_grid/title_bar.rs delete mode 100644 native/src/widget/pick_list.rs delete mode 100644 native/src/widget/progress_bar.rs delete mode 100644 native/src/widget/radio.rs delete mode 100644 native/src/widget/row.rs delete mode 100644 native/src/widget/rule.rs delete mode 100644 native/src/widget/scrollable.rs delete mode 100644 native/src/widget/slider.rs delete mode 100644 native/src/widget/space.rs delete mode 100644 native/src/widget/svg.rs delete mode 100644 native/src/widget/text.rs delete mode 100644 native/src/widget/text_input.rs delete mode 100644 native/src/widget/text_input/cursor.rs delete mode 100644 native/src/widget/text_input/editor.rs delete mode 100644 native/src/widget/text_input/value.rs delete mode 100644 native/src/widget/toggler.rs delete mode 100644 native/src/widget/tooltip.rs delete mode 100644 native/src/widget/tree.rs delete mode 100644 native/src/widget/vertical_slider.rs delete mode 100644 native/src/window/event.rs delete mode 100644 native/src/window/mode.rs delete mode 100644 native/src/window/redraw_request.rs delete mode 100644 native/src/window/user_attention.rs create mode 100644 renderer/src/compositor.rs delete mode 100644 renderer/src/window.rs delete mode 100644 renderer/src/window/compositor.rs create mode 100644 src/advanced.rs delete mode 100644 src/clipboard.rs delete mode 100644 src/element.rs delete mode 100644 src/executor.rs delete mode 100644 src/keyboard.rs delete mode 100644 src/mouse.rs delete mode 100644 src/overlay.rs delete mode 100644 src/result.rs delete mode 100644 src/widget.rs delete mode 100644 src/widget/canvas.rs delete mode 100644 src/widget/canvas/cursor.rs delete mode 100644 src/widget/canvas/event.rs delete mode 100644 src/widget/canvas/program.rs delete mode 100644 src/widget/qr_code.rs create mode 100644 widget/Cargo.toml create mode 100644 widget/src/button.rs create mode 100644 widget/src/canvas.rs create mode 100644 widget/src/canvas/cursor.rs create mode 100644 widget/src/canvas/event.rs create mode 100644 widget/src/canvas/program.rs create mode 100644 widget/src/checkbox.rs create mode 100644 widget/src/column.rs create mode 100644 widget/src/container.rs create mode 100644 widget/src/helpers.rs create mode 100644 widget/src/image.rs create mode 100644 widget/src/image/viewer.rs create mode 100644 widget/src/lazy.rs create mode 100644 widget/src/lazy/cache.rs create mode 100644 widget/src/lazy/component.rs create mode 100644 widget/src/lazy/helpers.rs create mode 100644 widget/src/lazy/responsive.rs create mode 100644 widget/src/lib.rs create mode 100644 widget/src/overlay.rs create mode 100644 widget/src/overlay/menu.rs create mode 100644 widget/src/pane_grid.rs create mode 100644 widget/src/pane_grid/axis.rs create mode 100644 widget/src/pane_grid/configuration.rs create mode 100644 widget/src/pane_grid/content.rs create mode 100644 widget/src/pane_grid/direction.rs create mode 100644 widget/src/pane_grid/draggable.rs create mode 100644 widget/src/pane_grid/node.rs create mode 100644 widget/src/pane_grid/pane.rs create mode 100644 widget/src/pane_grid/split.rs create mode 100644 widget/src/pane_grid/state.rs create mode 100644 widget/src/pane_grid/title_bar.rs create mode 100644 widget/src/pick_list.rs create mode 100644 widget/src/progress_bar.rs create mode 100644 widget/src/qr_code.rs create mode 100644 widget/src/radio.rs create mode 100644 widget/src/row.rs create mode 100644 widget/src/rule.rs create mode 100644 widget/src/scrollable.rs create mode 100644 widget/src/slider.rs create mode 100644 widget/src/space.rs create mode 100644 widget/src/svg.rs create mode 100644 widget/src/text.rs create mode 100644 widget/src/text_input.rs create mode 100644 widget/src/text_input/cursor.rs create mode 100644 widget/src/text_input/editor.rs create mode 100644 widget/src/text_input/value.rs create mode 100644 widget/src/toggler.rs create mode 100644 widget/src/tooltip.rs create mode 100644 widget/src/vertical_slider.rs diff --git a/Cargo.toml b/Cargo.toml index 28938df9..49a52311 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,13 +13,15 @@ categories = ["gui"] [features] # Enables the `Image` widget -image = ["iced_renderer/image", "image_rs"] +image = ["iced_widget/image", "image_rs"] # Enables the `Svg` widget -svg = ["iced_renderer/svg"] +svg = ["iced_widget/svg"] # Enables the `Canvas` widget -canvas = ["iced_renderer/geometry"] +canvas = ["iced_widget/canvas"] # Enables the `QRCode` widget -qr_code = ["canvas", "qrcode"] +qr_code = ["iced_widget/qr_code"] +# Enables lazy widgets +lazy = ["iced_widget/lazy"] # Enables a debug view in native platforms (press F12) debug = ["iced_winit/debug"] # Enables `tokio` as the `executor::Default` on native platforms @@ -32,11 +34,8 @@ smol = ["iced_futures/smol"] palette = ["iced_core/palette"] # Enables querying system information system = ["iced_winit/system"] -# Enables chrome traces -chrome-trace = [ - "iced_winit/chrome-trace", - "iced_renderer/tracing", -] +# Enables the advanced module +advanced = [] [badges] maintenance = { status = "actively-developed" } @@ -46,12 +45,12 @@ members = [ "core", "futures", "graphics", - "lazy", "native", "renderer", "style", "tiny_skia", "wgpu", + "widget", "winit", "examples/*", ] @@ -59,21 +58,15 @@ members = [ [dependencies] iced_core = { version = "0.8", path = "core" } iced_futures = { version = "0.6", path = "futures" } -iced_native = { version = "0.9", path = "native" } -iced_renderer = { version = "0.1", path = "renderer" } +iced_widget = { version = "0.1", path = "widget" } iced_winit = { version = "0.8", path = "winit", features = ["application"] } -thiserror = "1.0" +thiserror = "1" [dependencies.image_rs] version = "0.24" package = "image" optional = true -[dependencies.qrcode] -version = "0.12" -optional = true -default-features = false - [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] features = ["image", "svg", "canvas", "qr_code"] diff --git a/core/Cargo.toml b/core/Cargo.toml index 7ccb7b7a..9edc20f6 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -10,6 +10,7 @@ repository = "https://github.com/iced-rs/iced" [dependencies] bitflags = "1.2" thiserror = "1" +twox-hash = { version = "1.5", default-features = false } [dependencies.palette] version = "0.6" diff --git a/core/src/clipboard.rs b/core/src/clipboard.rs new file mode 100644 index 00000000..081b4004 --- /dev/null +++ b/core/src/clipboard.rs @@ -0,0 +1,23 @@ +//! Access the clipboard. + +/// A buffer for short-term storage and transfer within and between +/// applications. +pub trait Clipboard { + /// Reads the current content of the [`Clipboard`] as text. + fn read(&self) -> Option; + + /// Writes the given text contents to the [`Clipboard`]. + fn write(&mut self, contents: String); +} + +/// A null implementation of the [`Clipboard`] trait. +#[derive(Debug, Clone, Copy)] +pub struct Null; + +impl Clipboard for Null { + fn read(&self) -> Option { + None + } + + fn write(&mut self, _contents: String) {} +} diff --git a/core/src/element.rs b/core/src/element.rs new file mode 100644 index 00000000..98c53737 --- /dev/null +++ b/core/src/element.rs @@ -0,0 +1,608 @@ +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::overlay; +use crate::renderer; +use crate::widget; +use crate::widget::tree::{self, Tree}; +use crate::{ + Clipboard, Color, Layout, Length, Point, Rectangle, Shell, Widget, +}; + +use std::any::Any; +use std::borrow::Borrow; + +/// A generic [`Widget`]. +/// +/// It is useful to build composable user interfaces that do not leak +/// implementation details in their __view logic__. +/// +/// If you have a [built-in widget], you should be able to use `Into` +/// to turn it into an [`Element`]. +/// +/// [built-in widget]: crate::widget +#[allow(missing_debug_implementations)] +pub struct Element<'a, Message, Renderer> { + widget: Box + 'a>, +} + +impl<'a, Message, Renderer> Element<'a, Message, Renderer> { + /// Creates a new [`Element`] containing the given [`Widget`]. + pub fn new(widget: impl Widget + 'a) -> Self + where + Renderer: crate::Renderer, + { + Self { + widget: Box::new(widget), + } + } + + /// Returns a reference to the [`Widget`] of the [`Element`], + pub fn as_widget(&self) -> &dyn Widget { + self.widget.as_ref() + } + + /// Returns a mutable reference to the [`Widget`] of the [`Element`], + pub fn as_widget_mut(&mut self) -> &mut dyn Widget { + self.widget.as_mut() + } + + /// Applies a transformation to the produced message of the [`Element`]. + /// + /// This method is useful when you want to decouple different parts of your + /// UI and make them __composable__. + /// + /// # Example + /// Imagine we want to use [our counter](index.html#usage). But instead of + /// showing a single counter, we want to display many of them. We can reuse + /// the `Counter` type as it is! + /// + /// We use composition to model the __state__ of our new application: + /// + /// ``` + /// # mod counter { + /// # pub struct Counter; + /// # } + /// use counter::Counter; + /// + /// struct ManyCounters { + /// counters: Vec, + /// } + /// ``` + /// + /// We can store the state of multiple counters now. However, the + /// __messages__ we implemented before describe the user interactions + /// of a __single__ counter. Right now, we need to also identify which + /// counter is receiving user interactions. Can we use composition again? + /// Yes. + /// + /// ``` + /// # mod counter { + /// # #[derive(Debug, Clone, Copy)] + /// # pub enum Message {} + /// # } + /// #[derive(Debug, Clone, Copy)] + /// pub enum Message { + /// Counter(usize, counter::Message) + /// } + /// ``` + /// + /// We compose the previous __messages__ with the index of the counter + /// producing them. Let's implement our __view logic__ now: + /// + /// ```no_run + /// # mod counter { + /// # #[derive(Debug, Clone, Copy)] + /// # pub enum Message {} + /// # pub struct Counter; + /// # + /// # impl Counter { + /// # pub fn view( + /// # &self, + /// # ) -> iced_core::Element { + /// # unimplemented!() + /// # } + /// # } + /// # } + /// # + /// # mod iced { + /// # pub use iced_core::renderer::Null as Renderer; + /// # pub use iced_core::Element; + /// # + /// # pub mod widget { + /// # pub struct Row { + /// # _t: std::marker::PhantomData, + /// # } + /// # + /// # impl Row { + /// # pub fn new() -> Self { + /// # unimplemented!() + /// # } + /// # + /// # pub fn spacing(mut self, _: u32) -> Self { + /// # unimplemented!() + /// # } + /// # + /// # pub fn push( + /// # mut self, + /// # _: iced_core::Element, + /// # ) -> Self { + /// # unimplemented!() + /// # } + /// # } + /// # } + /// # } + /// # + /// use counter::Counter; + /// + /// use iced::widget::Row; + /// use iced::{Element, Renderer}; + /// + /// struct ManyCounters { + /// counters: Vec, + /// } + /// + /// #[derive(Debug, Clone, Copy)] + /// pub enum Message { + /// Counter(usize, counter::Message), + /// } + /// + /// impl ManyCounters { + /// pub fn view(&mut self) -> Row { + /// // We can quickly populate a `Row` by folding over our counters + /// self.counters.iter_mut().enumerate().fold( + /// Row::new().spacing(20), + /// |row, (index, counter)| { + /// // We display the counter + /// let element: Element = + /// counter.view().into(); + /// + /// row.push( + /// // Here we turn our `Element` into + /// // an `Element` by combining the `index` and the + /// // message of the `element`. + /// element + /// .map(move |message| Message::Counter(index, message)), + /// ) + /// }, + /// ) + /// } + /// } + /// ``` + /// + /// Finally, our __update logic__ is pretty straightforward: simple + /// delegation. + /// + /// ``` + /// # mod counter { + /// # #[derive(Debug, Clone, Copy)] + /// # pub enum Message {} + /// # pub struct Counter; + /// # + /// # impl Counter { + /// # pub fn update(&mut self, _message: Message) {} + /// # } + /// # } + /// # + /// # use counter::Counter; + /// # + /// # struct ManyCounters { + /// # counters: Vec, + /// # } + /// # + /// # #[derive(Debug, Clone, Copy)] + /// # pub enum Message { + /// # Counter(usize, counter::Message) + /// # } + /// impl ManyCounters { + /// pub fn update(&mut self, message: Message) { + /// match message { + /// Message::Counter(index, counter_msg) => { + /// if let Some(counter) = self.counters.get_mut(index) { + /// counter.update(counter_msg); + /// } + /// } + /// } + /// } + /// } + /// ``` + pub fn map( + self, + f: impl Fn(Message) -> B + 'a, + ) -> Element<'a, B, Renderer> + where + Message: 'a, + Renderer: crate::Renderer + 'a, + B: 'a, + { + Element::new(Map::new(self.widget, f)) + } + + /// Marks the [`Element`] as _to-be-explained_. + /// + /// The [`Renderer`] will explain the layout of the [`Element`] graphically. + /// This can be very useful for debugging your layout! + /// + /// [`Renderer`]: crate::Renderer + pub fn explain>( + self, + color: C, + ) -> Element<'a, Message, Renderer> + where + Message: 'static, + Renderer: crate::Renderer + 'a, + { + Element { + widget: Box::new(Explain::new(self, color.into())), + } + } +} + +impl<'a, Message, Renderer> Borrow + 'a> + for Element<'a, Message, Renderer> +{ + fn borrow(&self) -> &(dyn Widget + 'a) { + self.widget.borrow() + } +} + +impl<'a, Message, Renderer> Borrow + 'a> + for &Element<'a, Message, Renderer> +{ + fn borrow(&self) -> &(dyn Widget + 'a) { + self.widget.borrow() + } +} + +struct Map<'a, A, B, Renderer> { + widget: Box + 'a>, + mapper: Box B + 'a>, +} + +impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> { + pub fn new( + widget: Box + 'a>, + mapper: F, + ) -> Map<'a, A, B, Renderer> + where + F: 'a + Fn(A) -> B, + { + Map { + widget, + mapper: Box::new(mapper), + } + } +} + +impl<'a, A, B, Renderer> Widget for Map<'a, A, B, Renderer> +where + Renderer: crate::Renderer + 'a, + A: 'a, + B: 'a, +{ + fn tag(&self) -> tree::Tag { + self.widget.tag() + } + + fn state(&self) -> tree::State { + self.widget.state() + } + + fn children(&self) -> Vec { + self.widget.children() + } + + fn diff(&self, tree: &mut Tree) { + self.widget.diff(tree) + } + + fn width(&self) -> Length { + self.widget.width() + } + + fn height(&self) -> Length { + self.widget.height() + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.widget.layout(renderer, limits) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation, + ) { + struct MapOperation<'a, B> { + operation: &'a mut dyn widget::Operation, + } + + impl<'a, T, B> widget::Operation for MapOperation<'a, B> { + fn container( + &mut self, + id: Option<&widget::Id>, + operate_on_children: &mut dyn FnMut( + &mut dyn widget::Operation, + ), + ) { + self.operation.container(id, &mut |operation| { + operate_on_children(&mut MapOperation { operation }); + }); + } + + fn focusable( + &mut self, + state: &mut dyn widget::operation::Focusable, + id: Option<&widget::Id>, + ) { + self.operation.focusable(state, id); + } + + fn scrollable( + &mut self, + state: &mut dyn widget::operation::Scrollable, + id: Option<&widget::Id>, + ) { + self.operation.scrollable(state, id); + } + + fn text_input( + &mut self, + state: &mut dyn widget::operation::TextInput, + id: Option<&widget::Id>, + ) { + self.operation.text_input(state, id); + } + + fn custom(&mut self, state: &mut dyn Any, id: Option<&widget::Id>) { + self.operation.custom(state, id); + } + } + + self.widget.operate( + tree, + layout, + renderer, + &mut MapOperation { 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<'_, B>, + ) -> event::Status { + let mut local_messages = Vec::new(); + let mut local_shell = Shell::new(&mut local_messages); + + let status = self.widget.on_event( + tree, + event, + layout, + cursor_position, + renderer, + clipboard, + &mut local_shell, + ); + + shell.merge(local_shell, &self.mapper); + + status + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + self.widget.draw( + tree, + renderer, + theme, + style, + layout, + cursor_position, + viewport, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.widget.mouse_interaction( + tree, + layout, + cursor_position, + viewport, + renderer, + ) + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option> { + let mapper = &self.mapper; + + self.widget + .overlay(tree, layout, renderer) + .map(move |overlay| overlay.map(mapper)) + } +} + +struct Explain<'a, Message, Renderer: crate::Renderer> { + element: Element<'a, Message, Renderer>, + color: Color, +} + +impl<'a, Message, Renderer> Explain<'a, Message, Renderer> +where + Renderer: crate::Renderer, +{ + fn new(element: Element<'a, Message, Renderer>, color: Color) -> Self { + Explain { element, color } + } +} + +impl<'a, Message, Renderer> Widget + for Explain<'a, Message, Renderer> +where + Renderer: crate::Renderer, +{ + fn width(&self) -> Length { + self.element.widget.width() + } + + fn height(&self) -> Length { + self.element.widget.height() + } + + fn tag(&self) -> tree::Tag { + self.element.widget.tag() + } + + fn state(&self) -> tree::State { + self.element.widget.state() + } + + fn children(&self) -> Vec { + self.element.widget.children() + } + + fn diff(&self, tree: &mut Tree) { + self.element.widget.diff(tree); + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.element.widget.layout(renderer, limits) + } + + fn operate( + &self, + state: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation, + ) { + self.element + .widget + .operate(state, layout, renderer, operation) + } + + fn on_event( + &mut self, + state: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + self.element.widget.on_event( + state, + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + } + + fn draw( + &self, + state: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + fn explain_layout( + renderer: &mut Renderer, + color: Color, + layout: Layout<'_>, + ) { + renderer.fill_quad( + renderer::Quad { + bounds: layout.bounds(), + border_color: color, + border_width: 1.0, + border_radius: 0.0.into(), + }, + Color::TRANSPARENT, + ); + + for child in layout.children() { + explain_layout(renderer, color, child); + } + } + + self.element.widget.draw( + state, + renderer, + theme, + style, + layout, + cursor_position, + viewport, + ); + + explain_layout(renderer, self.color, layout); + } + + fn mouse_interaction( + &self, + state: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.element.widget.mouse_interaction( + state, + layout, + cursor_position, + viewport, + renderer, + ) + } + + fn overlay<'b>( + &'b mut self, + state: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option> { + self.element.widget.overlay(state, layout, renderer) + } +} diff --git a/core/src/event.rs b/core/src/event.rs new file mode 100644 index 00000000..953cd73f --- /dev/null +++ b/core/src/event.rs @@ -0,0 +1,78 @@ +//! Handle events of a user interface. +use crate::keyboard; +use crate::mouse; +use crate::touch; +use crate::window; + +/// A user interface event. +/// +/// _**Note:** This type is largely incomplete! If you need to track +/// additional events, feel free to [open an issue] and share your use case!_ +/// +/// [open an issue]: https://github.com/iced-rs/iced/issues +#[derive(Debug, Clone, PartialEq)] +pub enum Event { + /// A keyboard event + Keyboard(keyboard::Event), + + /// A mouse event + Mouse(mouse::Event), + + /// A window event + Window(window::Event), + + /// A touch event + Touch(touch::Event), + + /// A platform specific event + PlatformSpecific(PlatformSpecific), +} + +/// A platform specific event +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PlatformSpecific { + /// A MacOS specific event + MacOS(MacOS), +} + +/// Describes an event specific to MacOS +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MacOS { + /// Triggered when the app receives an URL from the system + /// + /// _**Note:** For this event to be triggered, the executable needs to be properly [bundled]!_ + /// + /// [bundled]: https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW19 + ReceivedUrl(String), +} + +/// The status of an [`Event`] after being processed. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Status { + /// The [`Event`] was **NOT** handled by any widget. + Ignored, + + /// The [`Event`] was handled and processed by a widget. + Captured, +} + +impl Status { + /// Merges two [`Status`] into one. + /// + /// `Captured` takes precedence over `Ignored`: + /// + /// ``` + /// use iced_core::event::Status; + /// + /// assert_eq!(Status::Ignored.merge(Status::Ignored), Status::Ignored); + /// assert_eq!(Status::Ignored.merge(Status::Captured), Status::Captured); + /// assert_eq!(Status::Captured.merge(Status::Ignored), Status::Captured); + /// assert_eq!(Status::Captured.merge(Status::Captured), Status::Captured); + /// ``` + pub fn merge(self, b: Self) -> Self { + match self { + Status::Ignored => b, + Status::Captured => Status::Captured, + } + } +} diff --git a/core/src/hasher.rs b/core/src/hasher.rs new file mode 100644 index 00000000..fa52f16d --- /dev/null +++ b/core/src/hasher.rs @@ -0,0 +1,13 @@ +/// The hasher used to compare layouts. +#[derive(Debug, Default)] +pub struct Hasher(twox_hash::XxHash64); + +impl core::hash::Hasher for Hasher { + fn write(&mut self, bytes: &[u8]) { + self.0.write(bytes) + } + + fn finish(&self) -> u64 { + self.0.finish() + } +} diff --git a/core/src/image.rs b/core/src/image.rs new file mode 100644 index 00000000..70fbade0 --- /dev/null +++ b/core/src/image.rs @@ -0,0 +1,174 @@ +//! Load and draw raster graphics. +use crate::{Hasher, Rectangle, Size}; + +use std::hash::{Hash, Hasher as _}; +use std::path::PathBuf; +use std::sync::Arc; + +/// A handle of some image data. +#[derive(Debug, Clone)] +pub struct Handle { + id: u64, + data: Data, +} + +impl Handle { + /// Creates an image [`Handle`] pointing to the image of the given path. + /// + /// Makes an educated guess about the image format by examining the data in the file. + pub fn from_path>(path: T) -> Handle { + Self::from_data(Data::Path(path.into())) + } + + /// Creates an image [`Handle`] containing the image pixels directly. This + /// function expects the input data to be provided as a `Vec` of RGBA + /// pixels. + /// + /// This is useful if you have already decoded your image. + pub fn from_pixels( + width: u32, + height: u32, + pixels: impl AsRef<[u8]> + Send + Sync + 'static, + ) -> Handle { + Self::from_data(Data::Rgba { + width, + height, + pixels: Bytes::new(pixels), + }) + } + + /// Creates an image [`Handle`] containing the image data directly. + /// + /// Makes an educated guess about the image format by examining the given data. + /// + /// This is useful if you already have your image loaded in-memory, maybe + /// because you downloaded or generated it procedurally. + pub fn from_memory( + bytes: impl AsRef<[u8]> + Send + Sync + 'static, + ) -> Handle { + Self::from_data(Data::Bytes(Bytes::new(bytes))) + } + + fn from_data(data: Data) -> Handle { + let mut hasher = Hasher::default(); + data.hash(&mut hasher); + + Handle { + id: hasher.finish(), + data, + } + } + + /// Returns the unique identifier of the [`Handle`]. + pub fn id(&self) -> u64 { + self.id + } + + /// Returns a reference to the image [`Data`]. + pub fn data(&self) -> &Data { + &self.data + } +} + +impl From for Handle +where + T: Into, +{ + fn from(path: T) -> Handle { + Handle::from_path(path.into()) + } +} + +impl Hash for Handle { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + +/// A wrapper around raw image data. +/// +/// It behaves like a `&[u8]`. +#[derive(Clone)] +pub struct Bytes(Arc + Send + Sync + 'static>); + +impl Bytes { + /// Creates new [`Bytes`] around `data`. + pub fn new(data: impl AsRef<[u8]> + Send + Sync + 'static) -> Self { + Self(Arc::new(data)) + } +} + +impl std::fmt::Debug for Bytes { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.as_ref().as_ref().fmt(f) + } +} + +impl std::hash::Hash for Bytes { + fn hash(&self, state: &mut H) { + self.0.as_ref().as_ref().hash(state); + } +} + +impl AsRef<[u8]> for Bytes { + fn as_ref(&self) -> &[u8] { + self.0.as_ref().as_ref() + } +} + +impl std::ops::Deref for Bytes { + type Target = [u8]; + + fn deref(&self) -> &[u8] { + self.0.as_ref().as_ref() + } +} + +/// The data of a raster image. +#[derive(Clone, Hash)] +pub enum Data { + /// File data + Path(PathBuf), + + /// In-memory data + Bytes(Bytes), + + /// Decoded image pixels in RGBA format. + Rgba { + /// The width of the image. + width: u32, + /// The height of the image. + height: u32, + /// The pixels. + pixels: Bytes, + }, +} + +impl std::fmt::Debug for Data { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Data::Path(path) => write!(f, "Path({path:?})"), + Data::Bytes(_) => write!(f, "Bytes(...)"), + Data::Rgba { width, height, .. } => { + write!(f, "Pixels({width} * {height})") + } + } + } +} + +/// A [`Renderer`] that can render raster graphics. +/// +/// [renderer]: crate::renderer +pub trait Renderer: crate::Renderer { + /// The image Handle to be displayed. Iced exposes its own default implementation of a [`Handle`] + /// + /// [`Handle`]: Self::Handle + type Handle: Clone + Hash; + + /// Returns the dimensions of an image for the given [`Handle`]. + fn dimensions(&self, handle: &Self::Handle) -> Size; + + /// Draws an image with the given [`Handle`] and inside the provided + /// `bounds`. + fn draw(&mut self, handle: Self::Handle, bounds: Rectangle); +} diff --git a/core/src/layout.rs b/core/src/layout.rs new file mode 100644 index 00000000..04954fb9 --- /dev/null +++ b/core/src/layout.rs @@ -0,0 +1,65 @@ +//! Position your widgets properly. +mod limits; +mod node; + +pub mod flex; + +pub use limits::Limits; +pub use node::Node; + +use crate::{Point, Rectangle, Vector}; + +/// The bounds of a [`Node`] and its children, using absolute coordinates. +#[derive(Debug, Clone, Copy)] +pub struct Layout<'a> { + position: Point, + node: &'a Node, +} + +impl<'a> Layout<'a> { + /// Creates a new [`Layout`] for the given [`Node`] at the origin. + pub fn new(node: &'a Node) -> Self { + Self::with_offset(Vector::new(0.0, 0.0), node) + } + + /// Creates a new [`Layout`] for the given [`Node`] with the provided offset + /// from the origin. + pub fn with_offset(offset: Vector, node: &'a Node) -> Self { + let bounds = node.bounds(); + + Self { + position: Point::new(bounds.x, bounds.y) + offset, + node, + } + } + + /// Returns the position of the [`Layout`]. + pub fn position(&self) -> Point { + self.position + } + + /// Returns the bounds of the [`Layout`]. + /// + /// The returned [`Rectangle`] describes the position and size of a + /// [`Node`]. + pub fn bounds(&self) -> Rectangle { + let bounds = self.node.bounds(); + + Rectangle { + x: self.position.x, + y: self.position.y, + width: bounds.width, + height: bounds.height, + } + } + + /// Returns an iterator over the [`Layout`] of the children of a [`Node`]. + pub fn children(self) -> impl Iterator> { + self.node.children().iter().map(move |node| { + Layout::with_offset( + Vector::new(self.position.x, self.position.y), + node, + ) + }) + } +} diff --git a/core/src/layout/DRUID_LICENSE b/core/src/layout/DRUID_LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/core/src/layout/DRUID_LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/core/src/layout/flex.rs b/core/src/layout/flex.rs new file mode 100644 index 00000000..5d70c2fc --- /dev/null +++ b/core/src/layout/flex.rs @@ -0,0 +1,232 @@ +//! Distribute elements using a flex-based layout. +// This code is heavily inspired by the [`druid`] codebase. +// +// [`druid`]: https://github.com/xi-editor/druid +// +// Copyright 2018 The xi-editor Authors, Héctor Ramón +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use crate::Element; + +use crate::layout::{Limits, Node}; +use crate::{Alignment, Padding, Point, Size}; + +/// The main axis of a flex layout. +#[derive(Debug)] +pub enum Axis { + /// The horizontal axis + Horizontal, + + /// The vertical axis + Vertical, +} + +impl Axis { + fn main(&self, size: Size) -> f32 { + match self { + Axis::Horizontal => size.width, + Axis::Vertical => size.height, + } + } + + fn cross(&self, size: Size) -> f32 { + match self { + Axis::Horizontal => size.height, + Axis::Vertical => size.width, + } + } + + fn pack(&self, main: f32, cross: f32) -> (f32, f32) { + match self { + Axis::Horizontal => (main, cross), + Axis::Vertical => (cross, main), + } + } +} + +/// Computes the flex layout with the given axis and limits, applying spacing, +/// padding and alignment to the items as needed. +/// +/// It returns a new layout [`Node`]. +pub fn resolve( + axis: Axis, + renderer: &Renderer, + limits: &Limits, + padding: Padding, + spacing: f32, + align_items: Alignment, + items: &[Element<'_, Message, Renderer>], +) -> Node +where + Renderer: crate::Renderer, +{ + let limits = limits.pad(padding); + let total_spacing = spacing * items.len().saturating_sub(1) as f32; + let max_cross = axis.cross(limits.max()); + + let mut fill_sum = 0; + let mut cross = axis.cross(limits.min()).max(axis.cross(limits.fill())); + let mut available = axis.main(limits.max()) - total_spacing; + + let mut nodes: Vec = Vec::with_capacity(items.len()); + nodes.resize(items.len(), Node::default()); + + if align_items == Alignment::Fill { + let mut fill_cross = axis.cross(limits.min()); + + items.iter().for_each(|child| { + let cross_fill_factor = match axis { + Axis::Horizontal => child.as_widget().height(), + Axis::Vertical => child.as_widget().width(), + } + .fill_factor(); + + if cross_fill_factor == 0 { + let (max_width, max_height) = axis.pack(available, max_cross); + + let child_limits = + Limits::new(Size::ZERO, Size::new(max_width, max_height)); + + let layout = child.as_widget().layout(renderer, &child_limits); + let size = layout.size(); + + fill_cross = fill_cross.max(axis.cross(size)); + } + }); + + cross = fill_cross; + } + + for (i, child) in items.iter().enumerate() { + let fill_factor = match axis { + Axis::Horizontal => child.as_widget().width(), + Axis::Vertical => child.as_widget().height(), + } + .fill_factor(); + + if fill_factor == 0 { + let (min_width, min_height) = if align_items == Alignment::Fill { + axis.pack(0.0, cross) + } else { + axis.pack(0.0, 0.0) + }; + + let (max_width, max_height) = if align_items == Alignment::Fill { + axis.pack(available, cross) + } else { + axis.pack(available, max_cross) + }; + + let child_limits = Limits::new( + Size::new(min_width, min_height), + Size::new(max_width, max_height), + ); + + let layout = child.as_widget().layout(renderer, &child_limits); + let size = layout.size(); + + available -= axis.main(size); + + if align_items != Alignment::Fill { + cross = cross.max(axis.cross(size)); + } + + nodes[i] = layout; + } else { + fill_sum += fill_factor; + } + } + + let remaining = available.max(0.0); + + for (i, child) in items.iter().enumerate() { + let fill_factor = match axis { + Axis::Horizontal => child.as_widget().width(), + Axis::Vertical => child.as_widget().height(), + } + .fill_factor(); + + if fill_factor != 0 { + let max_main = remaining * fill_factor as f32 / fill_sum as f32; + let min_main = if max_main.is_infinite() { + 0.0 + } else { + max_main + }; + + let (min_width, min_height) = if align_items == Alignment::Fill { + axis.pack(min_main, cross) + } else { + axis.pack(min_main, axis.cross(limits.min())) + }; + + let (max_width, max_height) = if align_items == Alignment::Fill { + axis.pack(max_main, cross) + } else { + axis.pack(max_main, max_cross) + }; + + let child_limits = Limits::new( + Size::new(min_width, min_height), + Size::new(max_width, max_height), + ); + + let layout = child.as_widget().layout(renderer, &child_limits); + + if align_items != Alignment::Fill { + cross = cross.max(axis.cross(layout.size())); + } + + nodes[i] = layout; + } + } + + let pad = axis.pack(padding.left, padding.top); + let mut main = pad.0; + + for (i, node) in nodes.iter_mut().enumerate() { + if i > 0 { + main += spacing; + } + + let (x, y) = axis.pack(main, pad.1); + + node.move_to(Point::new(x, y)); + + match axis { + Axis::Horizontal => { + node.align( + Alignment::Start, + align_items, + Size::new(0.0, cross), + ); + } + Axis::Vertical => { + node.align( + align_items, + Alignment::Start, + Size::new(cross, 0.0), + ); + } + } + + let size = node.size(); + + main += axis.main(size); + } + + let (width, height) = axis.pack(main - pad.0, cross); + let size = limits.resolve(Size::new(width, height)); + + Node::with_children(size.pad(padding), nodes) +} diff --git a/core/src/layout/limits.rs b/core/src/layout/limits.rs new file mode 100644 index 00000000..5d3c1556 --- /dev/null +++ b/core/src/layout/limits.rs @@ -0,0 +1,163 @@ +#![allow(clippy::manual_clamp)] +use crate::{Length, Padding, Size}; + +/// A set of size constraints for layouting. +#[derive(Debug, Clone, Copy)] +pub struct Limits { + min: Size, + max: Size, + fill: Size, +} + +impl Limits { + /// No limits + pub const NONE: Limits = Limits { + min: Size::ZERO, + max: Size::INFINITY, + fill: Size::INFINITY, + }; + + /// Creates new [`Limits`] with the given minimum and maximum [`Size`]. + pub const fn new(min: Size, max: Size) -> Limits { + Limits { + min, + max, + fill: Size::INFINITY, + } + } + + /// Returns the minimum [`Size`] of the [`Limits`]. + pub fn min(&self) -> Size { + self.min + } + + /// Returns the maximum [`Size`] of the [`Limits`]. + pub fn max(&self) -> Size { + self.max + } + + /// Returns the fill [`Size`] of the [`Limits`]. + pub fn fill(&self) -> Size { + self.fill + } + + /// Applies a width constraint to the current [`Limits`]. + pub fn width(mut self, width: impl Into) -> Limits { + match width.into() { + Length::Shrink => { + self.fill.width = self.min.width; + } + Length::Fill | Length::FillPortion(_) => { + self.fill.width = self.fill.width.min(self.max.width); + } + Length::Fixed(amount) => { + let new_width = amount.min(self.max.width).max(self.min.width); + + self.min.width = new_width; + self.max.width = new_width; + self.fill.width = new_width; + } + } + + self + } + + /// Applies a height constraint to the current [`Limits`]. + pub fn height(mut self, height: impl Into) -> Limits { + match height.into() { + Length::Shrink => { + self.fill.height = self.min.height; + } + Length::Fill | Length::FillPortion(_) => { + self.fill.height = self.fill.height.min(self.max.height); + } + Length::Fixed(amount) => { + let new_height = + amount.min(self.max.height).max(self.min.height); + + self.min.height = new_height; + self.max.height = new_height; + self.fill.height = new_height; + } + } + + self + } + + /// Applies a minimum width constraint to the current [`Limits`]. + pub fn min_width(mut self, min_width: f32) -> Limits { + self.min.width = self.min.width.max(min_width).min(self.max.width); + + self + } + + /// Applies a maximum width constraint to the current [`Limits`]. + pub fn max_width(mut self, max_width: f32) -> Limits { + self.max.width = self.max.width.min(max_width).max(self.min.width); + + self + } + + /// Applies a minimum height constraint to the current [`Limits`]. + pub fn min_height(mut self, min_height: f32) -> Limits { + self.min.height = self.min.height.max(min_height).min(self.max.height); + + self + } + + /// Applies a maximum height constraint to the current [`Limits`]. + pub fn max_height(mut self, max_height: f32) -> Limits { + self.max.height = self.max.height.min(max_height).max(self.min.height); + + self + } + + /// Shrinks the current [`Limits`] to account for the given padding. + pub fn pad(&self, padding: Padding) -> Limits { + self.shrink(Size::new(padding.horizontal(), padding.vertical())) + } + + /// Shrinks the current [`Limits`] by the given [`Size`]. + pub fn shrink(&self, size: Size) -> Limits { + let min = Size::new( + (self.min().width - size.width).max(0.0), + (self.min().height - size.height).max(0.0), + ); + + let max = Size::new( + (self.max().width - size.width).max(0.0), + (self.max().height - size.height).max(0.0), + ); + + let fill = Size::new( + (self.fill.width - size.width).max(0.0), + (self.fill.height - size.height).max(0.0), + ); + + Limits { min, max, fill } + } + + /// Removes the minimum width constraint for the current [`Limits`]. + pub fn loose(&self) -> Limits { + Limits { + min: Size::ZERO, + max: self.max, + fill: self.fill, + } + } + + /// Computes the resulting [`Size`] that fits the [`Limits`] given the + /// intrinsic size of some content. + pub fn resolve(&self, intrinsic_size: Size) -> Size { + Size::new( + intrinsic_size + .width + .min(self.max.width) + .max(self.fill.width), + intrinsic_size + .height + .min(self.max.height) + .max(self.fill.height), + ) + } +} diff --git a/core/src/layout/node.rs b/core/src/layout/node.rs new file mode 100644 index 00000000..e0c7dcb2 --- /dev/null +++ b/core/src/layout/node.rs @@ -0,0 +1,91 @@ +use crate::{Alignment, Point, Rectangle, Size, Vector}; + +/// The bounds of an element and its children. +#[derive(Debug, Clone, Default)] +pub struct Node { + bounds: Rectangle, + children: Vec, +} + +impl Node { + /// Creates a new [`Node`] with the given [`Size`]. + pub const fn new(size: Size) -> Self { + Self::with_children(size, Vec::new()) + } + + /// Creates a new [`Node`] with the given [`Size`] and children. + pub const fn with_children(size: Size, children: Vec) -> Self { + Node { + bounds: Rectangle { + x: 0.0, + y: 0.0, + width: size.width, + height: size.height, + }, + children, + } + } + + /// Returns the [`Size`] of the [`Node`]. + pub fn size(&self) -> Size { + Size::new(self.bounds.width, self.bounds.height) + } + + /// Returns the bounds of the [`Node`]. + pub fn bounds(&self) -> Rectangle { + self.bounds + } + + /// Returns the children of the [`Node`]. + pub fn children(&self) -> &[Node] { + &self.children + } + + /// Aligns the [`Node`] in the given space. + pub fn align( + &mut self, + horizontal_alignment: Alignment, + vertical_alignment: Alignment, + space: Size, + ) { + match horizontal_alignment { + Alignment::Start => {} + Alignment::Center => { + self.bounds.x += (space.width - self.bounds.width) / 2.0; + } + Alignment::End => { + self.bounds.x += space.width - self.bounds.width; + } + Alignment::Fill => { + self.bounds.width = space.width; + } + } + + match vertical_alignment { + Alignment::Start => {} + Alignment::Center => { + self.bounds.y += (space.height - self.bounds.height) / 2.0; + } + Alignment::End => { + self.bounds.y += space.height - self.bounds.height; + } + Alignment::Fill => { + self.bounds.height = space.height; + } + } + } + + /// Moves the [`Node`] to the given position. + pub fn move_to(&mut self, position: Point) { + self.bounds.x = position.x; + self.bounds.y = position.y; + } + + /// Translates the [`Node`] by the given translation. + pub fn translate(self, translation: Vector) -> Self { + Self { + bounds: self.bounds + translation, + ..self + } + } +} diff --git a/core/src/lib.rs b/core/src/lib.rs index 1e4f0411..5bdcee6a 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -25,33 +25,57 @@ #![forbid(unsafe_code, rust_2018_idioms)] #![allow(clippy::inherent_to_string, clippy::type_complexity)] pub mod alignment; +pub mod clipboard; +pub mod event; pub mod font; pub mod gradient; +pub mod image; pub mod keyboard; +pub mod layout; pub mod mouse; +pub mod overlay; +pub mod renderer; +pub mod svg; +pub mod text; pub mod time; +pub mod touch; +pub mod widget; +pub mod window; mod background; mod color; mod content_fit; +mod element; +mod hasher; mod length; mod padding; mod pixels; mod point; mod rectangle; +mod shell; mod size; mod vector; pub use alignment::Alignment; pub use background::Background; +pub use clipboard::Clipboard; pub use color::Color; pub use content_fit::ContentFit; +pub use element::Element; +pub use event::Event; pub use font::Font; pub use gradient::Gradient; +pub use hasher::Hasher; +pub use layout::Layout; pub use length::Length; +pub use overlay::Overlay; pub use padding::Padding; pub use pixels::Pixels; pub use point::Point; pub use rectangle::Rectangle; +pub use renderer::Renderer; +pub use shell::Shell; pub use size::Size; +pub use text::Text; pub use vector::Vector; +pub use widget::Widget; diff --git a/core/src/mouse.rs b/core/src/mouse.rs index 48214f65..0c405ce6 100644 --- a/core/src/mouse.rs +++ b/core/src/mouse.rs @@ -1,8 +1,11 @@ //! Handle mouse events. +pub mod click; + mod button; mod event; mod interaction; pub use button::Button; +pub use click::Click; pub use event::{Event, ScrollDelta}; pub use interaction::Interaction; diff --git a/core/src/mouse/click.rs b/core/src/mouse/click.rs new file mode 100644 index 00000000..4a7d796c --- /dev/null +++ b/core/src/mouse/click.rs @@ -0,0 +1,76 @@ +//! Track mouse clicks. +use crate::time::Instant; +use crate::Point; + +/// A mouse click. +#[derive(Debug, Clone, Copy)] +pub struct Click { + kind: Kind, + position: Point, + time: Instant, +} + +/// The kind of mouse click. +#[derive(Debug, Clone, Copy)] +pub enum Kind { + /// A single click + Single, + + /// A double click + Double, + + /// A triple click + Triple, +} + +impl Kind { + fn next(&self) -> Kind { + match self { + Kind::Single => Kind::Double, + Kind::Double => Kind::Triple, + Kind::Triple => Kind::Double, + } + } +} + +impl Click { + /// Creates a new [`Click`] with the given position and previous last + /// [`Click`]. + pub fn new(position: Point, previous: Option) -> Click { + let time = Instant::now(); + + let kind = if let Some(previous) = previous { + if previous.is_consecutive(position, time) { + previous.kind.next() + } else { + Kind::Single + } + } else { + Kind::Single + }; + + Click { + kind, + position, + time, + } + } + + /// Returns the [`Kind`] of [`Click`]. + pub fn kind(&self) -> Kind { + self.kind + } + + fn is_consecutive(&self, new_position: Point, time: Instant) -> bool { + let duration = if time > self.time { + Some(time - self.time) + } else { + None + }; + + self.position == new_position + && duration + .map(|duration| duration.as_millis() <= 300) + .unwrap_or(false) + } +} diff --git a/core/src/overlay.rs b/core/src/overlay.rs new file mode 100644 index 00000000..b9f3c735 --- /dev/null +++ b/core/src/overlay.rs @@ -0,0 +1,122 @@ +//! Display interactive elements on top of other widgets. +mod element; +mod group; + +pub use element::Element; +pub use group::Group; + +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::renderer; +use crate::widget; +use crate::widget::Tree; +use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size}; + +/// An interactive component that can be displayed on top of other widgets. +pub trait Overlay +where + Renderer: crate::Renderer, +{ + /// Returns the layout [`Node`] of the [`Overlay`]. + /// + /// This [`Node`] is used by the runtime to compute the [`Layout`] of the + /// user interface. + /// + /// [`Node`]: layout::Node + fn layout( + &self, + renderer: &Renderer, + bounds: Size, + position: Point, + ) -> layout::Node; + + /// Draws the [`Overlay`] using the associated `Renderer`. + fn draw( + &self, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + ); + + /// Applies a [`widget::Operation`] to the [`Overlay`]. + fn operate( + &mut self, + _layout: Layout<'_>, + _renderer: &Renderer, + _operation: &mut dyn widget::Operation, + ) { + } + + /// Processes a runtime [`Event`]. + /// + /// It receives: + /// * an [`Event`] describing user interaction + /// * the computed [`Layout`] of the [`Overlay`] + /// * the current cursor position + /// * a mutable `Message` list, allowing the [`Overlay`] to produce + /// new messages based on user interaction. + /// * the `Renderer` + /// * a [`Clipboard`], if available + /// + /// By default, it does nothing. + fn on_event( + &mut self, + _event: Event, + _layout: Layout<'_>, + _cursor_position: Point, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + _shell: &mut Shell<'_, Message>, + ) -> event::Status { + event::Status::Ignored + } + + /// Returns the current [`mouse::Interaction`] of the [`Overlay`]. + /// + /// By default, it returns [`mouse::Interaction::Idle`]. + fn mouse_interaction( + &self, + _layout: Layout<'_>, + _cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + mouse::Interaction::Idle + } + + /// Returns true if the cursor is over the [`Overlay`]. + /// + /// By default, it returns true if the bounds of the `layout` contain + /// the `cursor_position`. + fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { + layout.bounds().contains(cursor_position) + } +} + +/// Returns a [`Group`] of overlay [`Element`] children. +/// +/// 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>], + tree: &'a mut Tree, + layout: Layout<'_>, + renderer: &Renderer, +) -> Option> +where + Renderer: crate::Renderer, +{ + let children = children + .iter_mut() + .zip(&mut tree.children) + .zip(layout.children()) + .filter_map(|((child, state), layout)| { + child.as_widget_mut().overlay(state, layout, renderer) + }) + .collect::>(); + + (!children.is_empty()).then(|| Group::with_children(children).overlay()) +} diff --git a/core/src/overlay/element.rs b/core/src/overlay/element.rs new file mode 100644 index 00000000..237d25d1 --- /dev/null +++ b/core/src/overlay/element.rs @@ -0,0 +1,270 @@ +pub use crate::Overlay; + +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::renderer; +use crate::widget; +use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size, Vector}; + +use std::any::Any; + +/// A generic [`Overlay`]. +#[allow(missing_debug_implementations)] +pub struct Element<'a, Message, Renderer> { + position: Point, + overlay: Box + 'a>, +} + +impl<'a, Message, Renderer> Element<'a, Message, Renderer> +where + Renderer: crate::Renderer, +{ + /// Creates a new [`Element`] containing the given [`Overlay`]. + pub fn new( + position: Point, + overlay: Box + 'a>, + ) -> Self { + Self { position, overlay } + } + + /// Returns the position of the [`Element`]. + pub fn position(&self) -> Point { + self.position + } + + /// Translates the [`Element`]. + pub fn translate(mut self, translation: Vector) -> Self { + self.position = self.position + translation; + self + } + + /// Applies a transformation to the produced message of the [`Element`]. + pub fn map(self, f: &'a dyn Fn(Message) -> B) -> Element<'a, B, Renderer> + where + Message: 'a, + Renderer: 'a, + B: 'a, + { + Element { + position: self.position, + overlay: Box::new(Map::new(self.overlay, f)), + } + } + + /// Computes the layout of the [`Element`] in the given bounds. + pub fn layout( + &self, + renderer: &Renderer, + bounds: Size, + translation: Vector, + ) -> layout::Node { + self.overlay + .layout(renderer, bounds, self.position + translation) + } + + /// Processes a runtime [`Event`]. + pub fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + self.overlay.on_event( + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + } + + /// Returns the current [`mouse::Interaction`] of the [`Element`]. + pub fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.overlay.mouse_interaction( + layout, + cursor_position, + viewport, + renderer, + ) + } + + /// Draws the [`Element`] and its children using the given [`Layout`]. + pub fn draw( + &self, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + ) { + self.overlay + .draw(renderer, theme, style, layout, cursor_position) + } + + /// Applies a [`widget::Operation`] to the [`Element`]. + pub fn operate( + &mut self, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation, + ) { + self.overlay.operate(layout, renderer, operation); + } + + /// Returns true if the cursor is over the [`Element`]. + pub fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { + self.overlay.is_over(layout, cursor_position) + } +} + +struct Map<'a, A, B, Renderer> { + content: Box + 'a>, + mapper: &'a dyn Fn(A) -> B, +} + +impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> { + pub fn new( + content: Box + 'a>, + mapper: &'a dyn Fn(A) -> B, + ) -> Map<'a, A, B, Renderer> { + Map { content, mapper } + } +} + +impl<'a, A, B, Renderer> Overlay for Map<'a, A, B, Renderer> +where + Renderer: crate::Renderer, +{ + fn layout( + &self, + renderer: &Renderer, + bounds: Size, + position: Point, + ) -> layout::Node { + self.content.layout(renderer, bounds, position) + } + + fn operate( + &mut self, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation, + ) { + struct MapOperation<'a, B> { + operation: &'a mut dyn widget::Operation, + } + + impl<'a, T, B> widget::Operation for MapOperation<'a, B> { + fn container( + &mut self, + id: Option<&widget::Id>, + operate_on_children: &mut dyn FnMut( + &mut dyn widget::Operation, + ), + ) { + self.operation.container(id, &mut |operation| { + operate_on_children(&mut MapOperation { operation }); + }); + } + + fn focusable( + &mut self, + state: &mut dyn widget::operation::Focusable, + id: Option<&widget::Id>, + ) { + self.operation.focusable(state, id); + } + + fn scrollable( + &mut self, + state: &mut dyn widget::operation::Scrollable, + id: Option<&widget::Id>, + ) { + self.operation.scrollable(state, id); + } + + fn text_input( + &mut self, + state: &mut dyn widget::operation::TextInput, + id: Option<&widget::Id>, + ) { + self.operation.text_input(state, id) + } + + fn custom(&mut self, state: &mut dyn Any, id: Option<&widget::Id>) { + self.operation.custom(state, id); + } + } + + self.content + .operate(layout, renderer, &mut MapOperation { operation }); + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, B>, + ) -> event::Status { + let mut local_messages = Vec::new(); + let mut local_shell = Shell::new(&mut local_messages); + + let event_status = self.content.on_event( + event, + layout, + cursor_position, + renderer, + clipboard, + &mut local_shell, + ); + + shell.merge(local_shell, self.mapper); + + event_status + } + + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.content.mouse_interaction( + layout, + cursor_position, + viewport, + renderer, + ) + } + + fn draw( + &self, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + ) { + self.content + .draw(renderer, theme, style, layout, cursor_position) + } + + fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { + self.content.is_over(layout, cursor_position) + } +} diff --git a/core/src/overlay/group.rs b/core/src/overlay/group.rs new file mode 100644 index 00000000..0c48df34 --- /dev/null +++ b/core/src/overlay/group.rs @@ -0,0 +1,172 @@ +use crate::event; +use crate::layout; +use crate::mouse; +use crate::overlay; +use crate::renderer; +use crate::widget; +use crate::{Clipboard, Event, Layout, Overlay, Point, Rectangle, Shell, Size}; + +/// An [`Overlay`] container that displays multiple overlay [`overlay::Element`] +/// children. +#[allow(missing_debug_implementations)] +pub struct Group<'a, Message, Renderer> { + children: Vec>, +} + +impl<'a, Message, Renderer> Group<'a, Message, Renderer> +where + Renderer: 'a + crate::Renderer, + Message: 'a, +{ + /// Creates an empty [`Group`]. + pub fn new() -> Self { + Self::default() + } + + /// Creates a [`Group`] with the given elements. + pub fn with_children( + children: Vec>, + ) -> Self { + Group { children } + } + + /// Adds an [`overlay::Element`] to the [`Group`]. + pub fn push( + mut self, + child: impl Into>, + ) -> Self { + self.children.push(child.into()); + self + } + + /// Turns the [`Group`] into an overlay [`overlay::Element`]. + pub fn overlay(self) -> overlay::Element<'a, Message, Renderer> { + overlay::Element::new(Point::ORIGIN, Box::new(self)) + } +} + +impl<'a, Message, Renderer> Default for Group<'a, Message, Renderer> +where + Renderer: 'a + crate::Renderer, + Message: 'a, +{ + fn default() -> Self { + Self::with_children(Vec::new()) + } +} + +impl<'a, Message, Renderer> Overlay + for Group<'a, Message, Renderer> +where + Renderer: crate::Renderer, +{ + fn layout( + &self, + renderer: &Renderer, + bounds: Size, + position: Point, + ) -> layout::Node { + let translation = position - Point::ORIGIN; + + layout::Node::with_children( + bounds, + self.children + .iter() + .map(|child| child.layout(renderer, bounds, translation)) + .collect(), + ) + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + self.children + .iter_mut() + .zip(layout.children()) + .map(|(child, layout)| { + child.on_event( + event.clone(), + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + }) + .fold(event::Status::Ignored, event::Status::merge) + } + + fn draw( + &self, + renderer: &mut Renderer, + theme: &::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + ) { + for (child, layout) in self.children.iter().zip(layout.children()) { + child.draw(renderer, theme, style, layout, cursor_position); + } + } + + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.children + .iter() + .zip(layout.children()) + .map(|(child, layout)| { + child.mouse_interaction( + layout, + cursor_position, + viewport, + renderer, + ) + }) + .max() + .unwrap_or_default() + } + + fn operate( + &mut self, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation, + ) { + operation.container(None, &mut |operation| { + self.children.iter_mut().zip(layout.children()).for_each( + |(child, layout)| { + child.operate(layout, renderer, operation); + }, + ) + }); + } + + fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { + self.children + .iter() + .zip(layout.children()) + .any(|(child, layout)| child.is_over(layout, cursor_position)) + } +} + +impl<'a, Message, Renderer> From> + for overlay::Element<'a, Message, Renderer> +where + Renderer: 'a + crate::Renderer, + Message: 'a, +{ + fn from(group: Group<'a, Message, Renderer>) -> Self { + group.overlay() + } +} diff --git a/core/src/renderer.rs b/core/src/renderer.rs new file mode 100644 index 00000000..d6247e39 --- /dev/null +++ b/core/src/renderer.rs @@ -0,0 +1,99 @@ +//! Write your own renderer. +#[cfg(debug_assertions)] +mod null; + +#[cfg(debug_assertions)] +pub use null::Null; + +use crate::layout; +use crate::{Background, Color, Element, Rectangle, Vector}; + +/// A component that can be used by widgets to draw themselves on a screen. +pub trait Renderer: Sized { + /// The supported theme of the [`Renderer`]. + type Theme; + + /// Lays out the elements of a user interface. + /// + /// You should override this if you need to perform any operations before or + /// after layouting. For instance, trimming the measurements cache. + fn layout( + &mut self, + element: &Element<'_, Message, Self>, + limits: &layout::Limits, + ) -> layout::Node { + element.as_widget().layout(self, limits) + } + + /// Draws the primitives recorded in the given closure in a new layer. + /// + /// The layer will clip its contents to the provided `bounds`. + fn with_layer(&mut self, bounds: Rectangle, f: impl FnOnce(&mut Self)); + + /// Applies a `translation` to the primitives recorded in the given closure. + fn with_translation( + &mut self, + translation: Vector, + f: impl FnOnce(&mut Self), + ); + + /// Fills a [`Quad`] with the provided [`Background`]. + fn fill_quad(&mut self, quad: Quad, background: impl Into); + + /// Clears all of the recorded primitives in the [`Renderer`]. + fn clear(&mut self); +} + +/// A polygon with four sides. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Quad { + /// The bounds of the [`Quad`]. + pub bounds: Rectangle, + + /// The border radius of the [`Quad`]. + pub border_radius: BorderRadius, + + /// The border width of the [`Quad`]. + pub border_width: f32, + + /// The border color of the [`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 { + /// The text color + pub text_color: Color, +} + +impl Default for Style { + fn default() -> Self { + Style { + text_color: Color::BLACK, + } + } +} diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs new file mode 100644 index 00000000..d93338ae --- /dev/null +++ b/core/src/renderer/null.rs @@ -0,0 +1,82 @@ +use crate::renderer::{self, Renderer}; +use crate::text::{self, Text}; +use crate::{Background, Font, Point, Rectangle, Size, Vector}; + +use std::borrow::Cow; + +/// A renderer that does nothing. +/// +/// It can be useful if you are writing tests! +#[derive(Debug, Clone, Copy, Default)] +pub struct Null; + +impl Null { + /// Creates a new [`Null`] renderer. + pub fn new() -> Null { + Null + } +} + +impl Renderer for Null { + type Theme = (); + + fn with_layer(&mut self, _bounds: Rectangle, _f: impl FnOnce(&mut Self)) {} + + fn with_translation( + &mut self, + _translation: Vector, + _f: impl FnOnce(&mut Self), + ) { + } + + fn clear(&mut self) {} + + fn fill_quad( + &mut self, + _quad: renderer::Quad, + _background: impl Into, + ) { + } +} + +impl text::Renderer for Null { + type Font = Font; + + const ICON_FONT: Font = Font::SansSerif; + const CHECKMARK_ICON: char = '0'; + const ARROW_DOWN_ICON: char = '0'; + + fn default_font(&self) -> Self::Font { + Font::SansSerif + } + + fn default_size(&self) -> f32 { + 16.0 + } + + fn load_font(&mut self, _font: Cow<'static, [u8]>) {} + + fn measure( + &self, + _content: &str, + _size: f32, + _font: Font, + _bounds: Size, + ) -> (f32, f32) { + (0.0, 20.0) + } + + fn hit_test( + &self, + _contents: &str, + _size: f32, + _font: Self::Font, + _bounds: Size, + _point: Point, + _nearest_only: bool, + ) -> Option { + None + } + + fn fill_text(&mut self, _text: Text<'_, Self::Font>) {} +} diff --git a/core/src/shell.rs b/core/src/shell.rs new file mode 100644 index 00000000..74a5c616 --- /dev/null +++ b/core/src/shell.rs @@ -0,0 +1,108 @@ +use crate::window; + +/// A connection to the state of a shell. +/// +/// A [`Widget`] can leverage a [`Shell`] to trigger changes in an application, +/// like publishing messages or invalidating the current layout. +/// +/// [`Widget`]: crate::Widget +#[derive(Debug)] +pub struct Shell<'a, Message> { + messages: &'a mut Vec, + redraw_request: Option, + is_layout_invalid: bool, + are_widgets_invalid: bool, +} + +impl<'a, Message> Shell<'a, Message> { + /// Creates a new [`Shell`] with the provided buffer of messages. + pub fn new(messages: &'a mut Vec) -> Self { + Self { + messages, + redraw_request: None, + is_layout_invalid: false, + are_widgets_invalid: false, + } + } + + /// Returns true if the [`Shell`] contains no published messages + pub fn is_empty(&self) -> bool { + self.messages.is_empty() + } + + /// Publish the given `Message` for an application to process it. + pub fn publish(&mut self, message: Message) { + self.messages.push(message); + } + + /// Requests a new frame to be drawn at the given [`Instant`]. + pub fn request_redraw(&mut self, request: window::RedrawRequest) { + match self.redraw_request { + None => { + self.redraw_request = Some(request); + } + Some(current) if request < current => { + self.redraw_request = Some(request); + } + _ => {} + } + } + + /// Returns the requested [`Instant`] a redraw should happen, if any. + pub fn redraw_request(&self) -> Option { + self.redraw_request + } + + /// Returns whether the current layout is invalid or not. + pub fn is_layout_invalid(&self) -> bool { + self.is_layout_invalid + } + + /// Invalidates the current application layout. + /// + /// The shell will relayout the application widgets. + pub fn invalidate_layout(&mut self) { + self.is_layout_invalid = true; + } + + /// Triggers the given function if the layout is invalid, cleaning it in the + /// process. + pub fn revalidate_layout(&mut self, f: impl FnOnce()) { + if self.is_layout_invalid { + self.is_layout_invalid = false; + + f() + } + } + + /// Returns whether the widgets of the current application have been + /// invalidated. + pub fn are_widgets_invalid(&self) -> bool { + self.are_widgets_invalid + } + + /// Invalidates the current application widgets. + /// + /// The shell will rebuild and relayout the widget tree. + pub fn invalidate_widgets(&mut self) { + self.are_widgets_invalid = true; + } + + /// Merges the current [`Shell`] with another one by applying the given + /// function to the messages of the latter. + /// + /// This method is useful for composition. + pub fn merge(&mut self, other: Shell<'_, B>, f: impl Fn(B) -> Message) { + self.messages.extend(other.messages.drain(..).map(f)); + + if let Some(at) = other.redraw_request { + self.request_redraw(at); + } + + self.is_layout_invalid = + self.is_layout_invalid || other.is_layout_invalid; + + self.are_widgets_invalid = + self.are_widgets_invalid || other.are_widgets_invalid; + } +} diff --git a/core/src/svg.rs b/core/src/svg.rs new file mode 100644 index 00000000..9b98877a --- /dev/null +++ b/core/src/svg.rs @@ -0,0 +1,89 @@ +//! Load and draw vector graphics. +use crate::{Color, Hasher, Rectangle, Size}; + +use std::borrow::Cow; +use std::hash::{Hash, Hasher as _}; +use std::path::PathBuf; +use std::sync::Arc; + +/// A handle of Svg data. +#[derive(Debug, Clone)] +pub struct Handle { + id: u64, + data: Arc, +} + +impl Handle { + /// Creates an SVG [`Handle`] pointing to the vector image of the given + /// path. + pub fn from_path(path: impl Into) -> Handle { + Self::from_data(Data::Path(path.into())) + } + + /// Creates an SVG [`Handle`] from raw bytes containing either an SVG string + /// or gzip compressed data. + /// + /// This is useful if you already have your SVG data in-memory, maybe + /// because you downloaded or generated it procedurally. + pub fn from_memory(bytes: impl Into>) -> Handle { + Self::from_data(Data::Bytes(bytes.into())) + } + + fn from_data(data: Data) -> Handle { + let mut hasher = Hasher::default(); + data.hash(&mut hasher); + + Handle { + id: hasher.finish(), + data: Arc::new(data), + } + } + + /// Returns the unique identifier of the [`Handle`]. + pub fn id(&self) -> u64 { + self.id + } + + /// Returns a reference to the SVG [`Data`]. + pub fn data(&self) -> &Data { + &self.data + } +} + +impl Hash for Handle { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + +/// The data of a vectorial image. +#[derive(Clone, Hash)] +pub enum Data { + /// File data + Path(PathBuf), + + /// In-memory data + /// + /// Can contain an SVG string or a gzip compressed data. + Bytes(Cow<'static, [u8]>), +} + +impl std::fmt::Debug for Data { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Data::Path(path) => write!(f, "Path({path:?})"), + Data::Bytes(_) => write!(f, "Bytes(...)"), + } + } +} + +/// A [`Renderer`] that can render vector graphics. +/// +/// [renderer]: crate::renderer +pub trait Renderer: crate::Renderer { + /// Returns the default dimensions of an SVG for the given [`Handle`]. + fn dimensions(&self, handle: &Handle) -> Size; + + /// Draws an SVG with the given [`Handle`], an optional [`Color`] filter, and inside the provided `bounds`. + fn draw(&mut self, handle: Handle, color: Option, bounds: Rectangle); +} diff --git a/core/src/text.rs b/core/src/text.rs new file mode 100644 index 00000000..4c72abc3 --- /dev/null +++ b/core/src/text.rs @@ -0,0 +1,111 @@ +//! Draw and interact with text. +use crate::alignment; +use crate::{Color, Point, Rectangle, Size}; + +use std::borrow::Cow; + +/// A paragraph. +#[derive(Debug, Clone, Copy)] +pub struct Text<'a, Font> { + /// The content of the paragraph. + pub content: &'a str, + + /// The bounds of the paragraph. + pub bounds: Rectangle, + + /// The size of the [`Text`]. + pub size: f32, + + /// The color of the [`Text`]. + pub color: Color, + + /// The font of the [`Text`]. + pub font: Font, + + /// The horizontal alignment of the [`Text`]. + pub horizontal_alignment: alignment::Horizontal, + + /// The vertical alignment of the [`Text`]. + pub vertical_alignment: alignment::Vertical, +} + +/// The result of hit testing on text. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Hit { + /// The point was within the bounds of the returned character index. + CharOffset(usize), +} + +impl Hit { + /// Computes the cursor position of the [`Hit`] . + pub fn cursor(self) -> usize { + match self { + Self::CharOffset(i) => i, + } + } +} + +/// A renderer capable of measuring and drawing [`Text`]. +pub trait Renderer: crate::Renderer { + /// The font type used. + type Font: Copy; + + /// The icon font of the backend. + const ICON_FONT: Self::Font; + + /// The `char` representing a ✔ icon in the [`ICON_FONT`]. + /// + /// [`ICON_FONT`]: Self::ICON_FONT + const CHECKMARK_ICON: char; + + /// The `char` representing a ▼ icon in the built-in [`ICON_FONT`]. + /// + /// [`ICON_FONT`]: Self::ICON_FONT + const ARROW_DOWN_ICON: char; + + /// Returns the default [`Self::Font`]. + fn default_font(&self) -> Self::Font; + + /// Returns the default size of [`Text`]. + fn default_size(&self) -> f32; + + /// Measures the text in the given bounds and returns the minimum boundaries + /// that can fit the contents. + fn measure( + &self, + content: &str, + size: f32, + font: Self::Font, + bounds: Size, + ) -> (f32, f32); + + /// Measures the width of the text as if it were laid out in a single line. + fn measure_width(&self, content: &str, size: f32, font: Self::Font) -> f32 { + let (width, _) = self.measure(content, size, font, Size::INFINITY); + + width + } + + /// Tests whether the provided point is within the boundaries of text + /// laid out with the given parameters, returning information about + /// the nearest character. + /// + /// If `nearest_only` is true, the hit test does not consider whether the + /// the point is interior to any glyph bounds, returning only the character + /// with the nearest centeroid. + fn hit_test( + &self, + contents: &str, + size: f32, + font: Self::Font, + bounds: Size, + point: Point, + nearest_only: bool, + ) -> Option; + + /// Loads a [`Self::Font`] from its bytes. + fn load_font(&mut self, font: Cow<'static, [u8]>); + + /// Draws the given [`Text`]. + fn fill_text(&mut self, text: Text<'_, Self::Font>); +} diff --git a/core/src/touch.rs b/core/src/touch.rs new file mode 100644 index 00000000..18120644 --- /dev/null +++ b/core/src/touch.rs @@ -0,0 +1,23 @@ +//! Build touch events. +use crate::Point; + +/// A touch interaction. +#[derive(Debug, Clone, Copy, PartialEq)] +#[allow(missing_docs)] +pub enum Event { + /// A touch interaction was started. + FingerPressed { id: Finger, position: Point }, + + /// An on-going touch interaction was moved. + FingerMoved { id: Finger, position: Point }, + + /// A touch interaction was ended. + FingerLifted { id: Finger, position: Point }, + + /// A touch interaction was canceled. + FingerLost { id: Finger, position: Point }, +} + +/// A unique identifier representing a finger on a touch interaction. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Finger(pub u64); diff --git a/core/src/widget.rs b/core/src/widget.rs new file mode 100644 index 00000000..70e2c2d9 --- /dev/null +++ b/core/src/widget.rs @@ -0,0 +1,145 @@ +//! Create custom widgets and operate on them. +pub mod operation; +pub mod text; +pub mod tree; + +mod id; + +pub use id::Id; +pub use operation::Operation; +pub use text::Text; +pub use tree::Tree; + +use crate::event::{self, Event}; +use crate::layout::{self, Layout}; +use crate::mouse; +use crate::overlay; +use crate::renderer; +use crate::{Clipboard, Length, Point, Rectangle, Shell}; + +/// A component that displays information and allows interaction. +/// +/// If you want to build your own widgets, you will need to implement this +/// trait. +/// +/// # Examples +/// The repository has some [examples] showcasing how to implement a custom +/// widget: +/// +/// - [`bezier_tool`], a Paint-like tool for drawing Bézier curves using +/// [`lyon`]. +/// - [`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`]. +/// +/// [examples]: https://github.com/iced-rs/iced/tree/0.8/examples +/// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.8/examples/bezier_tool +/// [`custom_widget`]: https://github.com/iced-rs/iced/tree/0.8/examples/custom_widget +/// [`geometry`]: https://github.com/iced-rs/iced/tree/0.8/examples/geometry +/// [`lyon`]: https://github.com/nical/lyon +/// [`iced_wgpu`]: https://github.com/iced-rs/iced/tree/0.8/wgpu +pub trait Widget +where + Renderer: crate::Renderer, +{ + /// Returns the width of the [`Widget`]. + fn width(&self) -> Length; + + /// Returns the height of the [`Widget`]. + fn height(&self) -> Length; + + /// Returns the [`layout::Node`] of the [`Widget`]. + /// + /// This [`layout::Node`] is used by the runtime to compute the [`Layout`] of the + /// user interface. + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node; + + /// Draws the [`Widget`] using the associated `Renderer`. + fn draw( + &self, + state: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ); + + /// Returns the [`Tag`] of the [`Widget`]. + /// + /// [`Tag`]: tree::Tag + fn tag(&self) -> tree::Tag { + tree::Tag::stateless() + } + + /// Returns the [`State`] of the [`Widget`]. + /// + /// [`State`]: tree::State + fn state(&self) -> tree::State { + tree::State::None + } + + /// Returns the state [`Tree`] of the children of the [`Widget`]. + fn children(&self) -> Vec { + Vec::new() + } + + /// Reconciliates the [`Widget`] with the provided [`Tree`]. + fn diff(&self, _tree: &mut Tree) {} + + /// Applies an [`Operation`] to the [`Widget`]. + fn operate( + &self, + _state: &mut Tree, + _layout: Layout<'_>, + _renderer: &Renderer, + _operation: &mut dyn Operation, + ) { + } + + /// Processes a runtime [`Event`]. + /// + /// By default, it does nothing. + fn on_event( + &mut self, + _state: &mut Tree, + _event: Event, + _layout: Layout<'_>, + _cursor_position: Point, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + _shell: &mut Shell<'_, Message>, + ) -> event::Status { + event::Status::Ignored + } + + /// Returns the current [`mouse::Interaction`] of the [`Widget`]. + /// + /// By default, it returns [`mouse::Interaction::Idle`]. + fn mouse_interaction( + &self, + _state: &Tree, + _layout: Layout<'_>, + _cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + mouse::Interaction::Idle + } + + /// Returns the overlay of the [`Widget`], if there is any. + fn overlay<'a>( + &'a mut self, + _state: &'a mut Tree, + _layout: Layout<'_>, + _renderer: &Renderer, + ) -> Option> { + None + } +} diff --git a/core/src/widget/id.rs b/core/src/widget/id.rs new file mode 100644 index 00000000..ae739bb7 --- /dev/null +++ b/core/src/widget/id.rs @@ -0,0 +1,43 @@ +use std::borrow; +use std::sync::atomic::{self, AtomicUsize}; + +static NEXT_ID: AtomicUsize = AtomicUsize::new(0); + +/// The identifier of a generic widget. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Id(Internal); + +impl Id { + /// Creates a custom [`Id`]. + pub fn new(id: impl Into>) -> Self { + Self(Internal::Custom(id.into())) + } + + /// Creates a unique [`Id`]. + /// + /// This function produces a different [`Id`] every time it is called. + pub fn unique() -> Self { + let id = NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed); + + Self(Internal::Unique(id)) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +enum Internal { + Unique(usize), + Custom(borrow::Cow<'static, str>), +} + +#[cfg(test)] +mod tests { + use super::Id; + + #[test] + fn unique_generates_different_ids() { + let a = Id::unique(); + let b = Id::unique(); + + assert_ne!(a, b); + } +} diff --git a/core/src/widget/operation.rs b/core/src/widget/operation.rs new file mode 100644 index 00000000..53688a21 --- /dev/null +++ b/core/src/widget/operation.rs @@ -0,0 +1,112 @@ +//! Query or update internal widget state. +pub mod focusable; +pub mod scrollable; +pub mod text_input; + +pub use focusable::Focusable; +pub use scrollable::Scrollable; +pub use text_input::TextInput; + +use crate::widget::Id; + +use std::any::Any; +use std::fmt; + +/// A piece of logic that can traverse the widget tree of an application in +/// order to query or update some widget state. +pub trait Operation { + /// Operates on a widget that contains other widgets. + /// + /// The `operate_on_children` function can be called to return control to + /// the widget tree and keep traversing it. + fn container( + &mut self, + id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ); + + /// Operates on a widget that can be focused. + fn focusable(&mut self, _state: &mut dyn Focusable, _id: Option<&Id>) {} + + /// Operates on a widget that can be scrolled. + fn scrollable(&mut self, _state: &mut dyn Scrollable, _id: Option<&Id>) {} + + /// Operates on a widget that has text input. + fn text_input(&mut self, _state: &mut dyn TextInput, _id: Option<&Id>) {} + + /// Operates on a custom widget with some state. + fn custom(&mut self, _state: &mut dyn Any, _id: Option<&Id>) {} + + /// Finishes the [`Operation`] and returns its [`Outcome`]. + fn finish(&self) -> Outcome { + Outcome::None + } +} + +/// The result of an [`Operation`]. +pub enum Outcome { + /// The [`Operation`] produced no result. + None, + + /// The [`Operation`] produced some result. + Some(T), + + /// The [`Operation`] needs to be followed by another [`Operation`]. + Chain(Box>), +} + +impl fmt::Debug for Outcome +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::None => write!(f, "Outcome::None"), + Self::Some(output) => write!(f, "Outcome::Some({output:?})"), + Self::Chain(_) => write!(f, "Outcome::Chain(...)"), + } + } +} + +/// Produces an [`Operation`] that applies the given [`Operation`] to the +/// children of a container with the given [`Id`]. +pub fn scoped( + target: Id, + operation: impl Operation + 'static, +) -> impl Operation { + struct ScopedOperation { + target: Id, + operation: Box>, + } + + impl Operation for ScopedOperation { + fn container( + &mut self, + id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + if id == Some(&self.target) { + operate_on_children(self.operation.as_mut()); + } else { + operate_on_children(self); + } + } + + fn finish(&self) -> Outcome { + match self.operation.finish() { + Outcome::Chain(next) => { + Outcome::Chain(Box::new(ScopedOperation { + target: self.target.clone(), + operation: next, + })) + } + outcome => outcome, + } + } + } + + ScopedOperation { + target, + operation: Box::new(operation), + } +} diff --git a/core/src/widget/operation/focusable.rs b/core/src/widget/operation/focusable.rs new file mode 100644 index 00000000..312e4894 --- /dev/null +++ b/core/src/widget/operation/focusable.rs @@ -0,0 +1,203 @@ +//! Operate on widgets that can be focused. +use crate::widget::operation::{Operation, Outcome}; +use crate::widget::Id; + +/// The internal state of a widget that can be focused. +pub trait Focusable { + /// Returns whether the widget is focused or not. + fn is_focused(&self) -> bool; + + /// Focuses the widget. + fn focus(&mut self); + + /// Unfocuses the widget. + fn unfocus(&mut self); +} + +/// A summary of the focusable widgets present on a widget tree. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct Count { + /// The index of the current focused widget, if any. + pub focused: Option, + + /// The total amount of focusable widgets. + pub total: usize, +} + +/// Produces an [`Operation`] that focuses the widget with the given [`Id`]. +pub fn focus(target: Id) -> impl Operation { + struct Focus { + target: Id, + } + + impl Operation for Focus { + fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) { + match id { + Some(id) if id == &self.target => { + state.focus(); + } + _ => { + state.unfocus(); + } + } + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self) + } + } + + Focus { target } +} + +/// Produces an [`Operation`] that generates a [`Count`] and chains it with the +/// provided function to build a new [`Operation`]. +pub fn count(f: fn(Count) -> O) -> impl Operation +where + O: Operation + 'static, +{ + struct CountFocusable { + count: Count, + next: fn(Count) -> O, + } + + impl Operation for CountFocusable + where + O: Operation + 'static, + { + fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) { + if state.is_focused() { + self.count.focused = Some(self.count.total); + } + + self.count.total += 1; + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self) + } + + fn finish(&self) -> Outcome { + Outcome::Chain(Box::new((self.next)(self.count))) + } + } + + CountFocusable { + count: Count::default(), + next: f, + } +} + +/// Produces an [`Operation`] that searches for the current focused widget, and +/// - if found, focuses the previous focusable widget. +/// - if not found, focuses the last focusable widget. +pub fn focus_previous() -> impl Operation { + struct FocusPrevious { + count: Count, + current: usize, + } + + impl Operation for FocusPrevious { + fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) { + if self.count.total == 0 { + return; + } + + match self.count.focused { + None if self.current == self.count.total - 1 => state.focus(), + Some(0) if self.current == 0 => state.unfocus(), + Some(0) => {} + Some(focused) if focused == self.current => state.unfocus(), + Some(focused) if focused - 1 == self.current => state.focus(), + _ => {} + } + + self.current += 1; + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self) + } + } + + count(|count| FocusPrevious { count, current: 0 }) +} + +/// Produces an [`Operation`] that searches for the current focused widget, and +/// - if found, focuses the next focusable widget. +/// - if not found, focuses the first focusable widget. +pub fn focus_next() -> impl Operation { + struct FocusNext { + count: Count, + current: usize, + } + + impl Operation for FocusNext { + fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) { + match self.count.focused { + None if self.current == 0 => state.focus(), + Some(focused) if focused == self.current => state.unfocus(), + Some(focused) if focused + 1 == self.current => state.focus(), + _ => {} + } + + self.current += 1; + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self) + } + } + + count(|count| FocusNext { count, current: 0 }) +} + +/// Produces an [`Operation`] that searches for the current focused widget +/// and stores its ID. This ignores widgets that do not have an ID. +pub fn find_focused() -> impl Operation { + struct FindFocused { + focused: Option, + } + + impl Operation for FindFocused { + fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) { + if state.is_focused() && id.is_some() { + self.focused = id.cloned(); + } + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self) + } + + fn finish(&self) -> Outcome { + if let Some(id) = &self.focused { + Outcome::Some(id.clone()) + } else { + Outcome::None + } + } + } + + FindFocused { focused: None } +} diff --git a/core/src/widget/operation/scrollable.rs b/core/src/widget/operation/scrollable.rs new file mode 100644 index 00000000..3b20631f --- /dev/null +++ b/core/src/widget/operation/scrollable.rs @@ -0,0 +1,54 @@ +//! Operate on widgets that can be scrolled. +use crate::widget::{Id, Operation}; + +/// The internal state of a widget that can be scrolled. +pub trait Scrollable { + /// Snaps the scroll of the widget to the given `percentage` along the horizontal & vertical axis. + fn snap_to(&mut self, offset: RelativeOffset); +} + +/// Produces an [`Operation`] that snaps the widget with the given [`Id`] to +/// the provided `percentage`. +pub fn snap_to(target: Id, offset: RelativeOffset) -> impl Operation { + struct SnapTo { + target: Id, + offset: RelativeOffset, + } + + impl Operation for SnapTo { + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self) + } + + fn scrollable(&mut self, state: &mut dyn Scrollable, id: Option<&Id>) { + if Some(&self.target) == id { + state.snap_to(self.offset); + } + } + } + + SnapTo { target, offset } +} + +/// The amount of offset in each direction of a [`Scrollable`]. +/// +/// A value of `0.0` means start, while `1.0` means end. +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub struct RelativeOffset { + /// The amount of horizontal offset + pub x: f32, + /// The amount of vertical offset + pub y: f32, +} + +impl RelativeOffset { + /// A relative offset that points to the top-left of a [`Scrollable`]. + pub const START: Self = Self { x: 0.0, y: 0.0 }; + + /// A relative offset that points to the bottom-right of a [`Scrollable`]. + pub const END: Self = Self { x: 1.0, y: 1.0 }; +} diff --git a/core/src/widget/operation/text_input.rs b/core/src/widget/operation/text_input.rs new file mode 100644 index 00000000..4c773e99 --- /dev/null +++ b/core/src/widget/operation/text_input.rs @@ -0,0 +1,131 @@ +//! Operate on widgets that have text input. +use crate::widget::operation::Operation; +use crate::widget::Id; + +/// The internal state of a widget that has text input. +pub trait TextInput { + /// Moves the cursor of the text input to the front of the input text. + fn move_cursor_to_front(&mut self); + /// Moves the cursor of the text input to the end of the input text. + fn move_cursor_to_end(&mut self); + /// Moves the cursor of the text input to an arbitrary location. + fn move_cursor_to(&mut self, position: usize); + /// Selects all the content of the text input. + fn select_all(&mut self); +} + +/// Produces an [`Operation`] that moves the cursor of the widget with the given [`Id`] to the +/// front. +pub fn move_cursor_to_front(target: Id) -> impl Operation { + struct MoveCursor { + target: Id, + } + + impl Operation for MoveCursor { + fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { + match id { + Some(id) if id == &self.target => { + state.move_cursor_to_front(); + } + _ => {} + } + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self) + } + } + + MoveCursor { target } +} + +/// Produces an [`Operation`] that moves the cursor of the widget with the given [`Id`] to the +/// end. +pub fn move_cursor_to_end(target: Id) -> impl Operation { + struct MoveCursor { + target: Id, + } + + impl Operation for MoveCursor { + fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { + match id { + Some(id) if id == &self.target => { + state.move_cursor_to_end(); + } + _ => {} + } + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self) + } + } + + MoveCursor { target } +} + +/// Produces an [`Operation`] that moves the cursor of the widget with the given [`Id`] to the +/// provided position. +pub fn move_cursor_to(target: Id, position: usize) -> impl Operation { + struct MoveCursor { + target: Id, + position: usize, + } + + impl Operation for MoveCursor { + fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { + match id { + Some(id) if id == &self.target => { + state.move_cursor_to(self.position); + } + _ => {} + } + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self) + } + } + + MoveCursor { target, position } +} + +/// Produces an [`Operation`] that selects all the content of the widget with the given [`Id`]. +pub fn select_all(target: Id) -> impl Operation { + struct MoveCursor { + target: Id, + } + + impl Operation for MoveCursor { + fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { + match id { + Some(id) if id == &self.target => { + state.select_all(); + } + _ => {} + } + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self) + } + } + + MoveCursor { target } +} diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs new file mode 100644 index 00000000..485bb542 --- /dev/null +++ b/core/src/widget/text.rs @@ -0,0 +1,277 @@ +//! Write some text for your users to read. +use crate::alignment; +use crate::layout; +use crate::renderer; +use crate::text; +use crate::widget::Tree; +use crate::{ + Color, Element, Layout, Length, Pixels, Point, Rectangle, Size, Widget, +}; + +use std::borrow::Cow; + +/// A paragraph of text. +#[allow(missing_debug_implementations)] +pub struct Text<'a, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + content: Cow<'a, str>, + size: Option, + width: Length, + height: Length, + horizontal_alignment: alignment::Horizontal, + vertical_alignment: alignment::Vertical, + font: Option, + style: ::Style, +} + +impl<'a, Renderer> Text<'a, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + /// Create a new fragment of [`Text`] with the given contents. + pub fn new(content: impl Into>) -> Self { + Text { + content: content.into(), + size: None, + font: None, + width: Length::Shrink, + height: Length::Shrink, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + style: Default::default(), + } + } + + /// Sets the size of the [`Text`]. + pub fn size(mut self, size: impl Into) -> Self { + self.size = Some(size.into().0); + self + } + + /// Sets the [`Font`] of the [`Text`]. + /// + /// [`Font`]: crate::text::Renderer::Font + pub fn font(mut self, font: impl Into) -> Self { + self.font = Some(font.into()); + self + } + + /// Sets the style of the [`Text`]. + pub fn style( + mut self, + style: impl Into<::Style>, + ) -> Self { + self.style = style.into(); + self + } + + /// Sets the width of the [`Text`] boundaries. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Text`] boundaries. + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } + + /// Sets the [`alignment::Horizontal`] of the [`Text`]. + pub fn horizontal_alignment( + mut self, + alignment: alignment::Horizontal, + ) -> Self { + self.horizontal_alignment = alignment; + self + } + + /// Sets the [`alignment::Vertical`] of the [`Text`]. + pub fn vertical_alignment( + mut self, + alignment: alignment::Vertical, + ) -> Self { + self.vertical_alignment = alignment; + self + } +} + +impl<'a, Message, Renderer> Widget for Text<'a, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + + let size = self.size.unwrap_or_else(|| renderer.default_size()); + + let bounds = limits.max(); + + let (width, height) = renderer.measure( + &self.content, + size, + self.font.unwrap_or_else(|| renderer.default_font()), + bounds, + ); + + let size = limits.resolve(Size::new(width, height)); + + layout::Node::new(size) + } + + fn draw( + &self, + _state: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + _cursor_position: Point, + _viewport: &Rectangle, + ) { + draw( + renderer, + style, + layout, + &self.content, + self.size, + self.font, + theme.appearance(self.style), + self.horizontal_alignment, + self.vertical_alignment, + ); + } +} + +/// Draws text using the same logic as the [`Text`] widget. +/// +/// Specifically: +/// +/// * If no `size` is provided, the default text size of the `Renderer` will be +/// used. +/// * If no `color` is provided, the [`renderer::Style::text_color`] will be +/// used. +/// * The alignment attributes do not affect the position of the bounds of the +/// [`Layout`]. +pub fn draw( + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + content: &str, + size: Option, + font: Option, + appearance: Appearance, + horizontal_alignment: alignment::Horizontal, + vertical_alignment: alignment::Vertical, +) where + Renderer: text::Renderer, +{ + let bounds = layout.bounds(); + + let x = match horizontal_alignment { + alignment::Horizontal::Left => bounds.x, + alignment::Horizontal::Center => bounds.center_x(), + alignment::Horizontal::Right => bounds.x + bounds.width, + }; + + let y = match vertical_alignment { + alignment::Vertical::Top => bounds.y, + alignment::Vertical::Center => bounds.center_y(), + alignment::Vertical::Bottom => bounds.y + bounds.height, + }; + + renderer.fill_text(crate::Text { + content, + size: size.unwrap_or_else(|| renderer.default_size()), + bounds: Rectangle { x, y, ..bounds }, + color: appearance.color.unwrap_or(style.text_color), + font: font.unwrap_or_else(|| renderer.default_font()), + horizontal_alignment, + vertical_alignment, + }); +} + +impl<'a, Message, Renderer> From> + for Element<'a, Message, Renderer> +where + Renderer: text::Renderer + 'a, + Renderer::Theme: StyleSheet, +{ + fn from(text: Text<'a, Renderer>) -> Element<'a, Message, Renderer> { + Element::new(text) + } +} + +impl<'a, Renderer> Clone for Text<'a, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + fn clone(&self) -> Self { + Self { + content: self.content.clone(), + size: self.size, + width: self.width, + height: self.height, + horizontal_alignment: self.horizontal_alignment, + vertical_alignment: self.vertical_alignment, + font: self.font, + style: self.style, + } + } +} + +impl<'a, Renderer> From<&'a str> for Text<'a, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + fn from(content: &'a str) -> Self { + Self::new(content) + } +} + +impl<'a, Message, Renderer> From<&'a str> for Element<'a, Message, Renderer> +where + Renderer: text::Renderer + 'a, + Renderer::Theme: StyleSheet, +{ + fn from(content: &'a str) -> Self { + Text::from(content).into() + } +} + +/// The style sheet of some text. +pub trait StyleSheet { + /// The supported style of the [`StyleSheet`]. + type Style: Default + Copy; + + /// Produces the [`Appearance`] of some text. + fn appearance(&self, style: Self::Style) -> Appearance; +} + +/// The apperance of some text. +#[derive(Debug, Clone, Copy, Default)] +pub struct Appearance { + /// The [`Color`] of the text. + /// + /// The default, `None`, means using the inherited color. + pub color: Option, +} diff --git a/core/src/widget/tree.rs b/core/src/widget/tree.rs new file mode 100644 index 00000000..0af40c33 --- /dev/null +++ b/core/src/widget/tree.rs @@ -0,0 +1,187 @@ +//! Store internal widget state in a state tree to ensure continuity. +use crate::Widget; + +use std::any::{self, Any}; +use std::borrow::Borrow; +use std::fmt; + +/// A persistent state widget tree. +/// +/// A [`Tree`] is normally associated with a specific widget in the widget tree. +#[derive(Debug)] +pub struct Tree { + /// The tag of the [`Tree`]. + pub tag: Tag, + + /// The [`State`] of the [`Tree`]. + pub state: State, + + /// The children of the root widget of the [`Tree`]. + pub children: Vec, +} + +impl Tree { + /// Creates an empty, stateless [`Tree`] with no children. + pub fn empty() -> Self { + Self { + tag: Tag::stateless(), + state: State::None, + children: Vec::new(), + } + } + + /// Creates a new [`Tree`] for the provided [`Widget`]. + pub fn new<'a, Message, Renderer>( + widget: impl Borrow + 'a>, + ) -> Self + where + Renderer: crate::Renderer, + { + let widget = widget.borrow(); + + Self { + tag: widget.tag(), + state: widget.state(), + children: widget.children(), + } + } + + /// Reconciliates the current tree with the provided [`Widget`]. + /// + /// If the tag of the [`Widget`] matches the tag of the [`Tree`], then the + /// [`Widget`] proceeds with the reconciliation (i.e. [`Widget::diff`] is called). + /// + /// Otherwise, the whole [`Tree`] is recreated. + /// + /// [`Widget::diff`]: crate::Widget::diff + pub fn diff<'a, Message, Renderer>( + &mut self, + new: impl Borrow + 'a>, + ) where + Renderer: crate::Renderer, + { + if self.tag == new.borrow().tag() { + new.borrow().diff(self) + } else { + *self = Self::new(new); + } + } + + /// Reconciliates the children of the tree with the provided list of widgets. + pub fn diff_children<'a, Message, Renderer>( + &mut self, + new_children: &[impl Borrow + 'a>], + ) where + Renderer: crate::Renderer, + { + self.diff_children_custom( + new_children, + |tree, widget| tree.diff(widget.borrow()), + |widget| Self::new(widget.borrow()), + ) + } + + /// Reconciliates the children of the tree with the provided list of widgets using custom + /// logic both for diffing and creating new widget state. + pub fn diff_children_custom( + &mut self, + new_children: &[T], + diff: impl Fn(&mut Tree, &T), + new_state: impl Fn(&T) -> Self, + ) { + if self.children.len() > new_children.len() { + self.children.truncate(new_children.len()); + } + + for (child_state, new) in + self.children.iter_mut().zip(new_children.iter()) + { + diff(child_state, new); + } + + if self.children.len() < new_children.len() { + self.children.extend( + new_children[self.children.len()..].iter().map(new_state), + ); + } + } +} + +/// The identifier of some widget state. +#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] +pub struct Tag(any::TypeId); + +impl Tag { + /// Creates a [`Tag`] for a state of type `T`. + pub fn of() -> Self + where + T: 'static, + { + Self(any::TypeId::of::()) + } + + /// Creates a [`Tag`] for a stateless widget. + pub fn stateless() -> Self { + Self::of::<()>() + } +} + +/// The internal [`State`] of a widget. +pub enum State { + /// No meaningful internal state. + None, + + /// Some meaningful internal state. + Some(Box), +} + +impl State { + /// Creates a new [`State`]. + pub fn new(state: T) -> Self + where + T: 'static, + { + State::Some(Box::new(state)) + } + + /// Downcasts the [`State`] to `T` and returns a reference to it. + /// + /// # Panics + /// This method will panic if the downcast fails or the [`State`] is [`State::None`]. + pub fn downcast_ref(&self) -> &T + where + T: 'static, + { + match self { + State::None => panic!("Downcast on stateless state"), + State::Some(state) => { + state.downcast_ref().expect("Downcast widget state") + } + } + } + + /// Downcasts the [`State`] to `T` and returns a mutable reference to it. + /// + /// # Panics + /// This method will panic if the downcast fails or the [`State`] is [`State::None`]. + pub fn downcast_mut(&mut self) -> &mut T + where + T: 'static, + { + match self { + State::None => panic!("Downcast on stateless state"), + State::Some(state) => { + state.downcast_mut().expect("Downcast widget state") + } + } + } +} + +impl fmt::Debug for State { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::None => write!(f, "State::None"), + Self::Some(_) => write!(f, "State::Some"), + } + } +} diff --git a/core/src/window.rs b/core/src/window.rs new file mode 100644 index 00000000..d829a4b4 --- /dev/null +++ b/core/src/window.rs @@ -0,0 +1,10 @@ +//! Build window-based GUI applications. +mod event; +mod mode; +mod redraw_request; +mod user_attention; + +pub use event::Event; +pub use mode::Mode; +pub use redraw_request::RedrawRequest; +pub use user_attention::UserAttention; diff --git a/core/src/window/event.rs b/core/src/window/event.rs new file mode 100644 index 00000000..e2fb5e66 --- /dev/null +++ b/core/src/window/event.rs @@ -0,0 +1,58 @@ +use crate::time::Instant; + +use std::path::PathBuf; + +/// A window-related event. +#[derive(PartialEq, Eq, Clone, Debug)] +pub enum Event { + /// A window was moved. + Moved { + /// The new logical x location of the window + x: i32, + /// The new logical y location of the window + y: i32, + }, + + /// A window was resized. + Resized { + /// The new logical width of the window + width: u32, + /// The new logical height of the window + height: u32, + }, + + /// A window redraw was requested. + /// + /// The [`Instant`] contains the current time. + RedrawRequested(Instant), + + /// The user has requested for the window to close. + /// + /// Usually, you will want to terminate the execution whenever this event + /// occurs. + CloseRequested, + + /// A window was focused. + Focused, + + /// A window was unfocused. + Unfocused, + + /// A file is being hovered over the window. + /// + /// When the user hovers multiple files at once, this event will be emitted + /// for each file separately. + FileHovered(PathBuf), + + /// A file has beend dropped into the window. + /// + /// When the user drops multiple files at once, this event will be emitted + /// for each file separately. + FileDropped(PathBuf), + + /// A file was hovered, but has exited the window. + /// + /// There will be a single `FilesHoveredLeft` event triggered even if + /// multiple files were hovered. + FilesHoveredLeft, +} diff --git a/core/src/window/mode.rs b/core/src/window/mode.rs new file mode 100644 index 00000000..fdce8e23 --- /dev/null +++ b/core/src/window/mode.rs @@ -0,0 +1,12 @@ +/// The mode of a window-based application. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Mode { + /// The application appears in its own window. + Windowed, + + /// The application takes the whole screen of its current monitor. + Fullscreen, + + /// The application is hidden + Hidden, +} diff --git a/core/src/window/redraw_request.rs b/core/src/window/redraw_request.rs new file mode 100644 index 00000000..3b4f0fd3 --- /dev/null +++ b/core/src/window/redraw_request.rs @@ -0,0 +1,38 @@ +use crate::time::Instant; + +/// A request to redraw a window. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum RedrawRequest { + /// Redraw the next frame. + NextFrame, + + /// Redraw at the given time. + At(Instant), +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::{Duration, Instant}; + + #[test] + fn ordering() { + let now = Instant::now(); + let later = now + Duration::from_millis(10); + + assert_eq!(RedrawRequest::NextFrame, RedrawRequest::NextFrame); + assert_eq!(RedrawRequest::At(now), RedrawRequest::At(now)); + + assert!(RedrawRequest::NextFrame < RedrawRequest::At(now)); + assert!(RedrawRequest::At(now) > RedrawRequest::NextFrame); + assert!(RedrawRequest::At(now) < RedrawRequest::At(later)); + assert!(RedrawRequest::At(later) > RedrawRequest::At(now)); + + assert!(RedrawRequest::NextFrame <= RedrawRequest::NextFrame); + assert!(RedrawRequest::NextFrame <= RedrawRequest::At(now)); + assert!(RedrawRequest::At(now) >= RedrawRequest::NextFrame); + assert!(RedrawRequest::At(now) <= RedrawRequest::At(now)); + assert!(RedrawRequest::At(now) <= RedrawRequest::At(later)); + assert!(RedrawRequest::At(later) >= RedrawRequest::At(now)); + } +} diff --git a/core/src/window/user_attention.rs b/core/src/window/user_attention.rs new file mode 100644 index 00000000..b03dfeef --- /dev/null +++ b/core/src/window/user_attention.rs @@ -0,0 +1,21 @@ +/// The type of user attention to request. +/// +/// ## Platform-specific +/// +/// - **X11:** Sets the WM's `XUrgencyHint`. No distinction between [`Critical`] and [`Informational`]. +/// +/// [`Critical`]: Self::Critical +/// [`Informational`]: Self::Informational +#[derive(Debug, Clone, Copy)] +pub enum UserAttention { + /// ## 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, +} diff --git a/examples/component/Cargo.toml b/examples/component/Cargo.toml index dd435201..9db1e6b4 100644 --- a/examples/component/Cargo.toml +++ b/examples/component/Cargo.toml @@ -6,6 +6,4 @@ edition = "2021" publish = false [dependencies] -iced = { path = "../..", features = ["debug"] } -iced_native = { path = "../../native" } -iced_lazy = { path = "../../lazy" } +iced = { path = "../..", features = ["debug", "lazy"] } diff --git a/examples/component/src/main.rs b/examples/component/src/main.rs index c407bb06..e59588b1 100644 --- a/examples/component/src/main.rs +++ b/examples/component/src/main.rs @@ -47,9 +47,8 @@ impl Sandbox for Component { mod numeric_input { use iced::alignment::{self, Alignment}; - use iced::widget::{self, button, row, text, text_input}; - use iced::{Element, Length}; - use iced_lazy::{self, Component}; + use iced::widget::{button, component, row, text, text_input, Component}; + use iced::{Element, Length, Renderer}; pub struct NumericInput { value: Option, @@ -82,13 +81,7 @@ mod numeric_input { } } - impl Component for NumericInput - where - Renderer: iced_native::text::Renderer + 'static, - Renderer::Theme: widget::button::StyleSheet - + widget::text_input::StyleSheet - + widget::text::StyleSheet, - { + impl Component for NumericInput { type State = (); type Event = Event; @@ -151,17 +144,12 @@ mod numeric_input { } } - impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> + impl<'a, Message> From> for Element<'a, Message, Renderer> where Message: 'a, - Renderer: 'static + iced_native::text::Renderer, - Renderer::Theme: widget::button::StyleSheet - + widget::text_input::StyleSheet - + widget::text::StyleSheet, { fn from(numeric_input: NumericInput) -> Self { - iced_lazy::component(numeric_input) + component(numeric_input) } } } diff --git a/examples/custom_quad/Cargo.toml b/examples/custom_quad/Cargo.toml index 39154786..f097c2dd 100644 --- a/examples/custom_quad/Cargo.toml +++ b/examples/custom_quad/Cargo.toml @@ -6,5 +6,4 @@ edition = "2021" publish = false [dependencies] -iced = { path = "../.." } -iced_native = { path = "../../native" } +iced = { path = "../..", features = ["advanced"] } diff --git a/examples/custom_quad/src/main.rs b/examples/custom_quad/src/main.rs index 6509887c..b07f42ce 100644 --- a/examples/custom_quad/src/main.rs +++ b/examples/custom_quad/src/main.rs @@ -1,9 +1,9 @@ //! This example showcases a drawing a quad. mod quad { - use iced_native::layout::{self, Layout}; - use iced_native::renderer; - use iced_native::widget::{self, Widget}; - use iced_native::{Color, Element, Length, Point, Rectangle, Size}; + use iced::advanced::layout::{self, Layout}; + use iced::advanced::renderer; + use iced::advanced::widget::{self, Widget}; + use iced::{Color, Element, Length, Point, Rectangle, Size}; pub struct CustomQuad { size: f32, diff --git a/examples/custom_widget/Cargo.toml b/examples/custom_widget/Cargo.toml index 067aab4a..dda0efe8 100644 --- a/examples/custom_widget/Cargo.toml +++ b/examples/custom_widget/Cargo.toml @@ -6,5 +6,4 @@ edition = "2021" publish = false [dependencies] -iced = { path = "../.." } -iced_native = { path = "../../native" } +iced = { path = "../..", features = ["advanced"] } diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs index f6bb3b1e..7854548c 100644 --- a/examples/custom_widget/src/main.rs +++ b/examples/custom_widget/src/main.rs @@ -9,10 +9,10 @@ mod circle { // Of course, you can choose to make the implementation renderer-agnostic, // if you wish to, by creating your own `Renderer` trait, which could be // implemented by `iced_wgpu` and other renderers. - use iced_native::layout::{self, Layout}; - use iced_native::renderer; - use iced_native::widget::{self, Widget}; - use iced_native::{Color, Element, Length, Point, Rectangle, Size}; + use iced::advanced::layout::{self, Layout}; + use iced::advanced::renderer; + use iced::advanced::widget::{self, Widget}; + use iced::{Color, Element, Length, Point, Rectangle, Size}; pub struct Circle { radius: f32, diff --git a/examples/events/Cargo.toml b/examples/events/Cargo.toml index 8c56e471..15ffc0af 100644 --- a/examples/events/Cargo.toml +++ b/examples/events/Cargo.toml @@ -7,4 +7,3 @@ publish = false [dependencies] iced = { path = "../..", features = ["debug"] } -iced_native = { path = "../../native" } diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs index 1b97018e..7f3a5e1d 100644 --- a/examples/events/src/main.rs +++ b/examples/events/src/main.rs @@ -1,12 +1,13 @@ use iced::alignment; use iced::executor; +use iced::subscription; use iced::widget::{button, checkbox, container, text, Column}; use iced::window; +use iced::Event; use iced::{ Alignment, Application, Command, Element, Length, Settings, Subscription, Theme, }; -use iced_native::Event; pub fn main() -> iced::Result { Events::run(Settings { @@ -17,13 +18,13 @@ pub fn main() -> iced::Result { #[derive(Debug, Default)] struct Events { - last: Vec, + last: Vec, enabled: bool, } #[derive(Debug, Clone)] enum Message { - EventOccurred(iced_native::Event), + EventOccurred(Event), Toggled(bool), Exit, } @@ -70,7 +71,7 @@ impl Application for Events { } fn subscription(&self) -> Subscription { - iced_native::subscription::events().map(Message::EventOccurred) + subscription::events().map(Message::EventOccurred) } fn view(&self) -> Element { diff --git a/examples/geometry/Cargo.toml b/examples/geometry/Cargo.toml index 22ede0e0..79fe52d5 100644 --- a/examples/geometry/Cargo.toml +++ b/examples/geometry/Cargo.toml @@ -6,6 +6,5 @@ edition = "2021" publish = false [dependencies] -iced = { path = "../.." } -iced_native = { path = "../../native" } +iced = { path = "../..", features = ["advanced"] } iced_graphics = { path = "../../graphics" } diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index a77772d5..5cb41184 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -1,23 +1,13 @@ //! This example showcases a simple native custom widget that renders using //! arbitrary low-level geometry. mod rainbow { - // For now, to implement a custom native widget you will need to add - // `iced_native` and `iced_wgpu` to your dependencies. - // - // Then, you simply need to define your widget type and implement the - // `iced_native::Widget` trait with the `iced_wgpu::Renderer`. - // - // Of course, you can choose to make the implementation renderer-agnostic, - // if you wish to, by creating your own `Renderer` trait, which could be - // implemented by `iced_wgpu` and other renderers. use iced_graphics::primitive::{ColoredVertex2D, Primitive}; - use iced_graphics::renderer::{self, Renderer}; - use iced_graphics::Backend; - use iced_native::layout; - use iced_native::widget::{self, Widget}; - use iced_native::{ - Element, Layout, Length, Point, Rectangle, Size, Vector, + use iced::advanced::layout::{self, Layout}; + use iced::advanced::renderer; + use iced::advanced::widget::{self, Widget}; + use iced::{ + Element, Length, Point, Rectangle, Renderer, Size, Theme, Vector, }; #[derive(Debug, Clone, Copy, Default)] @@ -27,10 +17,7 @@ mod rainbow { Rainbow } - impl Widget> for Rainbow - where - B: Backend, - { + impl Widget for Rainbow { fn width(&self) -> Length { Length::Fill } @@ -41,7 +28,7 @@ mod rainbow { fn layout( &self, - _renderer: &Renderer, + _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { let size = limits.width(Length::Fill).resolve(Size::ZERO); @@ -52,15 +39,15 @@ mod rainbow { fn draw( &self, _tree: &widget::Tree, - renderer: &mut Renderer, - _theme: &T, + renderer: &mut Renderer, + _theme: &Theme, _style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, _viewport: &Rectangle, ) { + use iced::advanced::Renderer as _; use iced_graphics::primitive::Mesh2D; - use iced_native::Renderer as _; let b = layout.bounds(); @@ -151,10 +138,7 @@ mod rainbow { } } - impl<'a, Message, B, T> From for Element<'a, Message, Renderer> - where - B: Backend, - { + impl<'a, Message> From for Element<'a, Message, Renderer> { fn from(rainbow: Rainbow) -> Self { Self::new(rainbow) } diff --git a/examples/integration/Cargo.toml b/examples/integration/Cargo.toml index 200306aa..60390242 100644 --- a/examples/integration/Cargo.toml +++ b/examples/integration/Cargo.toml @@ -8,6 +8,7 @@ publish = false [dependencies] iced_winit = { path = "../../winit" } iced_wgpu = { path = "../../wgpu", features = ["webgl"] } +iced_widget = { path = "../../widget" } env_logger = "0.8" [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/examples/integration/src/controls.rs b/examples/integration/src/controls.rs index 533cb6e2..16e21709 100644 --- a/examples/integration/src/controls.rs +++ b/examples/integration/src/controls.rs @@ -1,6 +1,8 @@ use iced_wgpu::Renderer; -use iced_winit::widget::{slider, text_input, Column, Row, Text}; -use iced_winit::{Alignment, Color, Command, Element, Length, Program}; +use iced_widget::{slider, text_input, Column, Row, Text}; +use iced_winit::core::{Alignment, Color, Element, Length}; +use iced_winit::native::{Command, Program}; +use iced_winit::style::Theme; pub struct Controls { background_color: Color, @@ -27,7 +29,7 @@ impl Controls { } impl Program for Controls { - type Renderer = Renderer; + type Renderer = Renderer; type Message = Message; fn update(&mut self, message: Message) -> Command { @@ -43,7 +45,7 @@ impl Program for Controls { Command::none() } - fn view(&self) -> Element { + fn view(&self) -> Element> { let background_color = self.background_color; let text = &self.text; diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index a7a90ced..c1f1f076 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -4,11 +4,14 @@ mod scene; use controls::Controls; use scene::Scene; -use iced_wgpu::{wgpu, Backend, Renderer, Settings, Viewport}; -use iced_winit::{ - conversion, futures, program, renderer, winit, Clipboard, Color, Debug, - Size, -}; +use iced_wgpu::graphics::Viewport; +use iced_wgpu::{wgpu, Backend, Renderer, Settings}; +use iced_winit::core::renderer; +use iced_winit::core::{Color, Size}; +use iced_winit::native::program; +use iced_winit::native::Debug; +use iced_winit::style::Theme; +use iced_winit::{conversion, futures, winit, Clipboard}; use winit::{ dpi::PhysicalPosition, @@ -73,43 +76,45 @@ pub fn main() { let instance = wgpu::Instance::new(backend); let surface = unsafe { instance.create_surface(&window) }; - let (format, (device, queue)) = futures::executor::block_on(async { - let adapter = wgpu::util::initialize_adapter_from_env_or_default( - &instance, - backend, - Some(&surface), - ) - .await - .expect("No suitable GPU adapters found on the system!"); - - let adapter_features = adapter.features(); - - #[cfg(target_arch = "wasm32")] - let needed_limits = wgpu::Limits::downlevel_webgl2_defaults() - .using_resolution(adapter.limits()); - - #[cfg(not(target_arch = "wasm32"))] - let needed_limits = wgpu::Limits::default(); - - ( - surface - .get_supported_formats(&adapter) - .first() - .copied() - .expect("Get preferred format"), - adapter - .request_device( - &wgpu::DeviceDescriptor { - label: None, - features: adapter_features & wgpu::Features::default(), - limits: needed_limits, - }, - None, - ) - .await - .expect("Request device"), - ) - }); + let (format, (device, queue)) = + futures::futures::executor::block_on(async { + let adapter = wgpu::util::initialize_adapter_from_env_or_default( + &instance, + backend, + Some(&surface), + ) + .await + .expect("No suitable GPU adapters found on the system!"); + + let adapter_features = adapter.features(); + + #[cfg(target_arch = "wasm32")] + let needed_limits = wgpu::Limits::downlevel_webgl2_defaults() + .using_resolution(adapter.limits()); + + #[cfg(not(target_arch = "wasm32"))] + let needed_limits = wgpu::Limits::default(); + + ( + surface + .get_supported_formats(&adapter) + .first() + .copied() + .expect("Get preferred format"), + adapter + .request_device( + &wgpu::DeviceDescriptor { + label: None, + features: adapter_features + & wgpu::Features::default(), + limits: needed_limits, + }, + None, + ) + .await + .expect("Request device"), + ) + }); surface.configure( &device, @@ -188,7 +193,7 @@ pub fn main() { viewport.scale_factor(), ), &mut renderer, - &iced_wgpu::Theme::Dark, + &Theme::Dark, &renderer::Style { text_color: Color::WHITE }, &mut clipboard, &mut debug, diff --git a/examples/integration/src/scene.rs b/examples/integration/src/scene.rs index 3e41fbda..90c7efbf 100644 --- a/examples/integration/src/scene.rs +++ b/examples/integration/src/scene.rs @@ -1,5 +1,5 @@ use iced_wgpu::wgpu; -use iced_winit::Color; +use iced_winit::core::Color; pub struct Scene { pipeline: wgpu::RenderPipeline, diff --git a/examples/lazy/Cargo.toml b/examples/lazy/Cargo.toml index 79255c25..e03e89a9 100644 --- a/examples/lazy/Cargo.toml +++ b/examples/lazy/Cargo.toml @@ -6,5 +6,4 @@ edition = "2021" publish = false [dependencies] -iced = { path = "../..", features = ["debug"] } -iced_lazy = { path = "../../lazy" } +iced = { path = "../..", features = ["debug", "lazy"] } diff --git a/examples/lazy/src/main.rs b/examples/lazy/src/main.rs index 6512106f..e1cdaefe 100644 --- a/examples/lazy/src/main.rs +++ b/examples/lazy/src/main.rs @@ -1,10 +1,9 @@ use iced::theme; use iced::widget::{ - button, column, horizontal_space, pick_list, row, scrollable, text, + button, column, horizontal_space, lazy, pick_list, row, scrollable, text, text_input, }; use iced::{Element, Length, Sandbox, Settings}; -use iced_lazy::lazy; use std::collections::HashSet; use std::hash::Hash; diff --git a/examples/modal/Cargo.toml b/examples/modal/Cargo.toml index 8770acac..3ac61e6a 100644 --- a/examples/modal/Cargo.toml +++ b/examples/modal/Cargo.toml @@ -6,5 +6,4 @@ edition = "2021" publish = false [dependencies] -iced = { path = "../..", features = [] } -iced_native = { path = "../../native" } +iced = { path = "../..", features = ["advanced"] } diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index 54555684..214ec97e 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -178,12 +178,15 @@ impl App { } mod modal { - use iced_native::alignment::Alignment; - use iced_native::widget::{self, Tree}; - use iced_native::{ - event, layout, mouse, overlay, renderer, Clipboard, Color, Element, - Event, Layout, Length, Point, Rectangle, Shell, Size, Widget, - }; + use iced::advanced::layout::{self, Layout}; + use iced::advanced::overlay; + use iced::advanced::renderer; + use iced::advanced::widget::{self, Widget}; + use iced::advanced::{self, Clipboard, Shell}; + use iced::alignment::Alignment; + use iced::event; + use iced::mouse; + use iced::{Color, Element, Event, Length, Point, Rectangle, Size}; /// A widget that centers a modal element over some base element pub struct Modal<'a, Message, Renderer> { @@ -218,14 +221,17 @@ mod modal { impl<'a, Message, Renderer> Widget for Modal<'a, Message, Renderer> where - Renderer: iced_native::Renderer, + Renderer: advanced::Renderer, Message: Clone, { - fn children(&self) -> Vec { - vec![Tree::new(&self.base), Tree::new(&self.modal)] + fn children(&self) -> Vec { + vec![ + widget::Tree::new(&self.base), + widget::Tree::new(&self.modal), + ] } - fn diff(&self, tree: &mut Tree) { + fn diff(&self, tree: &mut widget::Tree) { tree.diff_children(&[&self.base, &self.modal]); } @@ -247,7 +253,7 @@ mod modal { fn on_event( &mut self, - state: &mut Tree, + state: &mut widget::Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -268,9 +274,9 @@ mod modal { fn draw( &self, - state: &Tree, + state: &widget::Tree, renderer: &mut Renderer, - theme: &::Theme, + theme: &::Theme, style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, @@ -289,7 +295,7 @@ mod modal { fn overlay<'b>( &'b mut self, - state: &'b mut Tree, + state: &'b mut widget::Tree, layout: Layout<'_>, _renderer: &Renderer, ) -> Option> { @@ -306,7 +312,7 @@ mod modal { fn mouse_interaction( &self, - state: &Tree, + state: &widget::Tree, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, @@ -323,7 +329,7 @@ mod modal { fn operate( &self, - state: &mut Tree, + state: &mut widget::Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn widget::Operation, @@ -339,7 +345,7 @@ mod modal { struct Overlay<'a, 'b, Message, Renderer> { content: &'b mut Element<'a, Message, Renderer>, - tree: &'b mut Tree, + tree: &'b mut widget::Tree, size: Size, on_blur: Option, } @@ -347,7 +353,7 @@ mod modal { impl<'a, 'b, Message, Renderer> overlay::Overlay for Overlay<'a, 'b, Message, Renderer> where - Renderer: iced_native::Renderer, + Renderer: advanced::Renderer, Message: Clone, { fn layout( @@ -469,7 +475,7 @@ mod modal { impl<'a, Message, Renderer> From> for Element<'a, Message, Renderer> where - Renderer: 'a + iced_native::Renderer, + Renderer: 'a + advanced::Renderer, Message: 'a + Clone, { fn from(modal: Modal<'a, Message, Renderer>) -> Self { diff --git a/examples/pane_grid/Cargo.toml b/examples/pane_grid/Cargo.toml index dfd6dfa9..4c0bf072 100644 --- a/examples/pane_grid/Cargo.toml +++ b/examples/pane_grid/Cargo.toml @@ -6,6 +6,4 @@ edition = "2021" publish = false [dependencies] -iced = { path = "../..", features = ["debug"] } -iced_native = { path = "../../native" } -iced_lazy = { path = "../../lazy" } +iced = { path = "../..", features = ["debug", "lazy"] } diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs index c9f1376c..dfb80853 100644 --- a/examples/pane_grid/src/main.rs +++ b/examples/pane_grid/src/main.rs @@ -1,14 +1,16 @@ use iced::alignment::{self, Alignment}; +use iced::event::{self, Event}; use iced::executor; use iced::keyboard; +use iced::subscription; use iced::theme::{self, Theme}; use iced::widget::pane_grid::{self, PaneGrid}; -use iced::widget::{button, column, container, row, scrollable, text}; +use iced::widget::{ + button, column, container, responsive, row, scrollable, text, +}; use iced::{ Application, Color, Command, Element, Length, Settings, Size, Subscription, }; -use iced_lazy::responsive; -use iced_native::{event, subscription, Event}; pub fn main() -> iced::Result { Example::run(Settings::default()) diff --git a/examples/toast/Cargo.toml b/examples/toast/Cargo.toml index f1f986aa..f703572c 100644 --- a/examples/toast/Cargo.toml +++ b/examples/toast/Cargo.toml @@ -6,5 +6,4 @@ edition = "2021" publish = false [dependencies] -iced = { path = "../..", features = [] } -iced_native = { path = "../../native" } +iced = { path = "../..", features = ["advanced"] } diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index e74b3ee6..78fb9de1 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -176,17 +176,23 @@ mod toast { use std::fmt; use std::time::{Duration, Instant}; + use iced::advanced; + use iced::advanced::layout::{self, Layout}; + use iced::advanced::overlay; + use iced::advanced::renderer; + use iced::advanced::widget::{self, Operation, Tree}; + use iced::advanced::{Clipboard, Shell, Widget}; + use iced::event::{self, Event}; + use iced::mouse; use iced::theme; use iced::widget::{ button, column, container, horizontal_rule, horizontal_space, row, text, }; + use iced::window; use iced::{ Alignment, Element, Length, Point, Rectangle, Renderer, Size, Theme, Vector, }; - use iced_native::widget::{tree, Operation, Tree}; - use iced_native::{event, layout, mouse, overlay, renderer, window}; - use iced_native::{Clipboard, Event, Layout, Shell, Widget}; pub const DEFAULT_TIMEOUT: u64 = 5; @@ -324,13 +330,13 @@ mod toast { self.content.as_widget().layout(renderer, limits) } - fn tag(&self) -> tree::Tag { + fn tag(&self) -> widget::tree::Tag { struct Marker(Vec); - iced_native::widget::tree::Tag::of::() + widget::tree::Tag::of::() } - fn state(&self) -> tree::State { - iced_native::widget::tree::State::new(Vec::>::new()) + fn state(&self) -> widget::tree::State { + widget::tree::State::new(Vec::>::new()) } fn children(&self) -> Vec { @@ -584,7 +590,7 @@ mod toast { fn draw( &self, renderer: &mut Renderer, - theme: &::Theme, + theme: &::Theme, style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, @@ -613,7 +619,7 @@ mod toast { &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn iced_native::widget::Operation, + operation: &mut dyn widget::Operation, ) { operation.container(None, &mut |operation| { self.toasts diff --git a/examples/url_handler/Cargo.toml b/examples/url_handler/Cargo.toml index 63c7ec27..4dcff92d 100644 --- a/examples/url_handler/Cargo.toml +++ b/examples/url_handler/Cargo.toml @@ -7,4 +7,3 @@ publish = false [dependencies] iced = { path = "../.." } -iced_native = { path = "../../native" } diff --git a/examples/url_handler/src/main.rs b/examples/url_handler/src/main.rs index 3257b519..f63fa06a 100644 --- a/examples/url_handler/src/main.rs +++ b/examples/url_handler/src/main.rs @@ -1,12 +1,10 @@ +use iced::event::{Event, MacOS, PlatformSpecific}; use iced::executor; +use iced::subscription; use iced::widget::{container, text}; use iced::{ Application, Command, Element, Length, Settings, Subscription, Theme, }; -use iced_native::{ - event::{MacOS, PlatformSpecific}, - Event, -}; pub fn main() -> iced::Result { App::run(Settings::default()) @@ -19,7 +17,7 @@ struct App { #[derive(Debug, Clone)] enum Message { - EventOccurred(iced_native::Event), + EventOccurred(Event), } impl Application for App { @@ -52,7 +50,7 @@ impl Application for App { } fn subscription(&self) -> Subscription { - iced_native::subscription::events().map(Message::EventOccurred) + subscription::events().map(Message::EventOccurred) } fn view(&self) -> Element { diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index 98e6f474..4bb22b6c 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -39,13 +39,9 @@ bitflags = "1.2" version = "1.4" features = ["derive"] -[dependencies.iced_native] -version = "0.9" -path = "../native" - -[dependencies.iced_style] -version = "0.7" -path = "../style" +[dependencies.iced_core] +version = "0.8" +path = "../core" [dependencies.tiny-skia] version = "0.8" diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs index 8658cffe..dd2888ab 100644 --- a/graphics/src/backend.rs +++ b/graphics/src/backend.rs @@ -1,8 +1,8 @@ //! Write a graphics backend. -use iced_native::image; -use iced_native::svg; -use iced_native::text; -use iced_native::{Font, Point, Size}; +use iced_core::image; +use iced_core::svg; +use iced_core::text; +use iced_core::{Font, Point, Size}; use std::borrow::Cow; diff --git a/graphics/src/compositor.rs b/graphics/src/compositor.rs new file mode 100644 index 00000000..d55e801a --- /dev/null +++ b/graphics/src/compositor.rs @@ -0,0 +1,92 @@ +//! A compositor is responsible for initializing a renderer and managing window +//! surfaces. +use crate::{Error, Viewport}; + +use iced_core::Color; + +use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; +use thiserror::Error; + +/// A graphics compositor that can draw to windows. +pub trait Compositor: Sized { + /// The settings of the backend. + type Settings: Default; + + /// The iced renderer of the backend. + type Renderer: iced_core::Renderer; + + /// The surface of the backend. + type Surface; + + /// Creates a new [`Compositor`]. + fn new( + settings: Self::Settings, + compatible_window: Option<&W>, + ) -> Result<(Self, Self::Renderer), Error>; + + /// Crates a new [`Surface`] for the given window. + /// + /// [`Surface`]: Self::Surface + fn create_surface( + &mut self, + window: &W, + width: u32, + height: u32, + ) -> Self::Surface; + + /// Configures a new [`Surface`] with the given dimensions. + /// + /// [`Surface`]: Self::Surface + fn configure_surface( + &mut self, + surface: &mut Self::Surface, + width: u32, + height: u32, + ); + + /// Returns [`Information`] used by this [`Compositor`]. + fn fetch_information(&self) -> 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: Color, + overlay: &[T], + ) -> Result<(), SurfaceError>; +} + +/// Result of an unsuccessful call to [`Compositor::present`]. +#[derive(Clone, PartialEq, Eq, Debug, Error)] +pub enum SurfaceError { + /// A timeout was encountered while trying to acquire the next frame. + #[error( + "A timeout was encountered while trying to acquire the next frame" + )] + Timeout, + /// The underlying surface has changed, and therefore the surface must be updated. + #[error( + "The underlying surface has changed, and therefore the surface must be updated." + )] + Outdated, + /// The swap chain has been lost and needs to be recreated. + #[error("The surface has been lost and needs to be recreated")] + Lost, + /// There is no more memory left to allocate a new frame. + #[error("There is no more memory left to allocate a new frame")] + OutOfMemory, +} + +/// Contains informations about the graphics (e.g. graphics adapter, graphics backend). +#[derive(Debug)] +pub struct Information { + /// Contains the graphics adapter. + pub adapter: String, + /// Contains the graphics backend. + pub backend: String, +} diff --git a/graphics/src/geometry.rs b/graphics/src/geometry.rs index 29ac84d6..8db1594a 100644 --- a/graphics/src/geometry.rs +++ b/graphics/src/geometry.rs @@ -16,7 +16,7 @@ pub use stroke::{LineCap, LineDash, LineJoin, Stroke}; pub use style::Style; pub use text::Text; -pub use iced_native::gradient::{self, Gradient}; +pub use iced_core::gradient::{self, Gradient}; use crate::Primitive; @@ -29,7 +29,7 @@ impl From for Primitive { } } -pub trait Renderer: iced_native::Renderer { +pub trait Renderer: iced_core::Renderer { type Geometry; fn draw(&mut self, geometry: Vec); diff --git a/graphics/src/geometry/fill.rs b/graphics/src/geometry/fill.rs index 109d5e99..2e8c1669 100644 --- a/graphics/src/geometry/fill.rs +++ b/graphics/src/geometry/fill.rs @@ -1,5 +1,5 @@ //! Fill [crate::widget::canvas::Geometry] with a certain style. -use crate::{Color, Gradient}; +use iced_core::{Color, Gradient}; pub use crate::geometry::Style; diff --git a/graphics/src/geometry/path.rs b/graphics/src/geometry/path.rs index 30c387c5..c3127bdf 100644 --- a/graphics/src/geometry/path.rs +++ b/graphics/src/geometry/path.rs @@ -9,7 +9,7 @@ pub use builder::Builder; pub use lyon_path; -use crate::{Point, Size}; +use iced_core::{Point, Size}; /// An immutable set of points that may or may not be connected. /// diff --git a/graphics/src/geometry/path/arc.rs b/graphics/src/geometry/path/arc.rs index e0747d3e..2cdebb66 100644 --- a/graphics/src/geometry/path/arc.rs +++ b/graphics/src/geometry/path/arc.rs @@ -1,5 +1,5 @@ //! Build and draw curves. -use crate::{Point, Vector}; +use iced_core::{Point, Vector}; /// A segment of a differentiable curve. #[derive(Debug, Clone, Copy)] diff --git a/graphics/src/geometry/path/builder.rs b/graphics/src/geometry/path/builder.rs index 4a9c5e36..794dd3bc 100644 --- a/graphics/src/geometry/path/builder.rs +++ b/graphics/src/geometry/path/builder.rs @@ -1,5 +1,6 @@ use crate::geometry::path::{arc, Arc, Path}; -use crate::{Point, Size}; + +use iced_core::{Point, Size}; use lyon_path::builder::{self, SvgPathBuilder}; use lyon_path::geom; diff --git a/graphics/src/geometry/stroke.rs b/graphics/src/geometry/stroke.rs index b551a9c9..2d760a6c 100644 --- a/graphics/src/geometry/stroke.rs +++ b/graphics/src/geometry/stroke.rs @@ -1,7 +1,7 @@ //! Create lines from a [crate::widget::canvas::Path] and assigns them various attributes/styles. pub use crate::geometry::Style; -use crate::Color; +use iced_core::Color; /// The style of a stroke. #[derive(Debug, Clone)] diff --git a/graphics/src/geometry/style.rs b/graphics/src/geometry/style.rs index 6794f2e7..be9ee376 100644 --- a/graphics/src/geometry/style.rs +++ b/graphics/src/geometry/style.rs @@ -1,4 +1,4 @@ -use crate::{Color, Gradient}; +use iced_core::{Color, Gradient}; /// The coloring style of some drawing. #[derive(Debug, Clone, PartialEq)] diff --git a/graphics/src/geometry/text.rs b/graphics/src/geometry/text.rs index 8c0b2dfb..06e0b4d0 100644 --- a/graphics/src/geometry/text.rs +++ b/graphics/src/geometry/text.rs @@ -1,5 +1,5 @@ -use crate::alignment; -use crate::{Color, Font, Point}; +use iced_core::alignment; +use iced_core::{Color, Font, Point}; /// A bunch of text that can be drawn to a canvas #[derive(Debug, Clone)] diff --git a/graphics/src/image/raster.rs b/graphics/src/image/raster.rs index da46c30f..03211160 100644 --- a/graphics/src/image/raster.rs +++ b/graphics/src/image/raster.rs @@ -1,8 +1,8 @@ //! Raster image loading and caching. use crate::image::Storage; -use crate::Size; -use iced_native::image; +use iced_core::image; +use iced_core::Size; use bitflags::bitflags; use std::collections::{HashMap, HashSet}; diff --git a/graphics/src/image/storage.rs b/graphics/src/image/storage.rs index 1b5b5c35..4caa6141 100644 --- a/graphics/src/image/storage.rs +++ b/graphics/src/image/storage.rs @@ -1,5 +1,5 @@ //! Store images. -use crate::Size; +use iced_core::Size; use std::fmt::Debug; diff --git a/graphics/src/image/vector.rs b/graphics/src/image/vector.rs index c950ccd6..32729acd 100644 --- a/graphics/src/image/vector.rs +++ b/graphics/src/image/vector.rs @@ -1,9 +1,8 @@ //! Vector image loading and caching use crate::image::Storage; -use crate::Color; -use iced_native::svg; -use iced_native::Size; +use iced_core::svg; +use iced_core::{Color, Size}; use resvg::tiny_skia; use resvg::usvg; diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index e56f8ad8..c6f9cf57 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -27,17 +27,17 @@ mod transformation; mod viewport; pub mod backend; +pub mod compositor; pub mod image; -pub mod overlay; pub mod primitive; pub mod renderer; -pub mod window; #[cfg(feature = "geometry")] pub mod geometry; pub use antialiasing::Antialiasing; pub use backend::Backend; +pub use compositor::Compositor; pub use error::Error; pub use primitive::Primitive; pub use renderer::Renderer; @@ -47,9 +47,4 @@ pub use viewport::Viewport; #[cfg(feature = "geometry")] pub use geometry::Geometry; -pub use iced_native::alignment; -pub use iced_native::text; -pub use iced_native::{ - Alignment, Background, Color, Font, Gradient, Point, Rectangle, Size, - Vector, -}; +pub use iced_core as core; diff --git a/graphics/src/overlay.rs b/graphics/src/overlay.rs deleted file mode 100644 index bc0ed744..00000000 --- a/graphics/src/overlay.rs +++ /dev/null @@ -1,2 +0,0 @@ -//! Display interactive elements on top of other widgets. -pub mod menu; diff --git a/graphics/src/overlay/menu.rs b/graphics/src/overlay/menu.rs deleted file mode 100644 index 8b489e5e..00000000 --- a/graphics/src/overlay/menu.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Build and show dropdown menus. - -pub use iced_style::menu::{Appearance, StyleSheet}; diff --git a/graphics/src/primitive.rs b/graphics/src/primitive.rs index f900b3fd..195b62da 100644 --- a/graphics/src/primitive.rs +++ b/graphics/src/primitive.rs @@ -1,8 +1,7 @@ -use crate::alignment; - -use iced_native::image; -use iced_native::svg; -use iced_native::{Background, Color, Font, Gradient, Rectangle, Size, Vector}; +use iced_core::alignment; +use iced_core::image; +use iced_core::svg; +use iced_core::{Background, Color, Font, Gradient, Rectangle, Size, Vector}; use bytemuck::{Pod, Zeroable}; use std::sync::Arc; diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index cb57f429..7bc462ef 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -1,15 +1,15 @@ //! Create a renderer from a [`Backend`]. use crate::backend::{self, Backend}; -use crate::{Primitive, Vector}; +use crate::Primitive; -use iced_native::image; -use iced_native::layout; -use iced_native::renderer; -use iced_native::svg; -use iced_native::text::{self, Text}; -use iced_native::{Background, Color, Element, Font, Point, Rectangle, Size}; - -pub use iced_native::renderer::Style; +use iced_core::image; +use iced_core::layout; +use iced_core::renderer; +use iced_core::svg; +use iced_core::text::{self, Text}; +use iced_core::{ + Background, Color, Element, Font, Point, Rectangle, Size, Vector, +}; use std::borrow::Cow; use std::marker::PhantomData; @@ -52,7 +52,7 @@ impl Renderer { } } -impl iced_native::Renderer for Renderer +impl iced_core::Renderer for Renderer where B: Backend, { diff --git a/graphics/src/viewport.rs b/graphics/src/viewport.rs index 2c0b541a..5792555d 100644 --- a/graphics/src/viewport.rs +++ b/graphics/src/viewport.rs @@ -1,4 +1,6 @@ -use crate::{Size, Transformation}; +use crate::Transformation; + +use iced_core::Size; /// A viewing region for displaying computer graphics. #[derive(Debug, Clone)] diff --git a/graphics/src/window.rs b/graphics/src/window.rs deleted file mode 100644 index a38b81f3..00000000 --- a/graphics/src/window.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Draw graphics to window surfaces. -pub mod compositor; - -#[cfg(feature = "opengl")] -pub mod gl_compositor; - -pub use compositor::Compositor; - -#[cfg(feature = "opengl")] -pub use gl_compositor::GLCompositor; diff --git a/graphics/src/window/compositor.rs b/graphics/src/window/compositor.rs deleted file mode 100644 index 15f8dab5..00000000 --- a/graphics/src/window/compositor.rs +++ /dev/null @@ -1,90 +0,0 @@ -//! A compositor is responsible for initializing a renderer and managing window -//! surfaces. -use crate::{Color, Error, Viewport}; - -use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; -use thiserror::Error; - -/// A graphics compositor that can draw to windows. -pub trait Compositor: Sized { - /// The settings of the backend. - type Settings: Default; - - /// The iced renderer of the backend. - type Renderer: iced_native::Renderer; - - /// The surface of the backend. - type Surface; - - /// Creates a new [`Compositor`]. - fn new( - settings: Self::Settings, - compatible_window: Option<&W>, - ) -> Result<(Self, Self::Renderer), Error>; - - /// Crates a new [`Surface`] for the given window. - /// - /// [`Surface`]: Self::Surface - fn create_surface( - &mut self, - window: &W, - width: u32, - height: u32, - ) -> Self::Surface; - - /// Configures a new [`Surface`] with the given dimensions. - /// - /// [`Surface`]: Self::Surface - fn configure_surface( - &mut self, - surface: &mut Self::Surface, - width: u32, - height: u32, - ); - - /// Returns [`Information`] used by this [`Compositor`]. - fn fetch_information(&self) -> 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: Color, - overlay: &[T], - ) -> Result<(), SurfaceError>; -} - -/// Result of an unsuccessful call to [`Compositor::present`]. -#[derive(Clone, PartialEq, Eq, Debug, Error)] -pub enum SurfaceError { - /// A timeout was encountered while trying to acquire the next frame. - #[error( - "A timeout was encountered while trying to acquire the next frame" - )] - Timeout, - /// The underlying surface has changed, and therefore the surface must be updated. - #[error( - "The underlying surface has changed, and therefore the surface must be updated." - )] - Outdated, - /// The swap chain has been lost and needs to be recreated. - #[error("The surface has been lost and needs to be recreated")] - Lost, - /// There is no more memory left to allocate a new frame. - #[error("There is no more memory left to allocate a new frame")] - OutOfMemory, -} - -/// Contains informations about the graphics (e.g. graphics adapter, graphics backend). -#[derive(Debug)] -pub struct Information { - /// Contains the graphics adapter. - pub adapter: String, - /// Contains the graphics backend. - pub backend: String, -} diff --git a/graphics/src/window/gl_compositor.rs b/graphics/src/window/gl_compositor.rs deleted file mode 100644 index 3e6dfd9e..00000000 --- a/graphics/src/window/gl_compositor.rs +++ /dev/null @@ -1,71 +0,0 @@ -//! A compositor is responsible for initializing a renderer and managing window -//! surfaces. -use crate::window::compositor::Information; -use crate::{Color, Error, Size, Viewport}; - -use core::ffi::c_void; - -/// A basic OpenGL compositor. -/// -/// A compositor is responsible for initializing a renderer and managing window -/// surfaces. -/// -/// For now, this compositor only deals with a single global surface -/// for drawing. However, the trait will most likely change in the near future -/// to handle multiple surfaces at once. -/// -/// If you implement an OpenGL renderer, you can implement this trait to ease -/// integration with existing windowing shells, like `iced_glutin`. -pub trait GLCompositor: Sized { - /// The renderer of the [`GLCompositor`]. - /// - /// This should point to your renderer type, which could be a type alias - /// of the [`Renderer`] provided in this crate with with a specific - /// [`Backend`]. - /// - /// [`Renderer`]: crate::Renderer - /// [`Backend`]: crate::Backend - type Renderer: iced_native::Renderer; - - /// The settings of the [`GLCompositor`]. - /// - /// It's up to you to decide the configuration supported by your renderer! - type Settings: Default; - - /// Creates a new [`GLCompositor`] and [`Renderer`] with the given - /// [`Settings`] and an OpenGL address loader function. - /// - /// # Safety - /// The `loader_function` should resolve to valid OpenGL bindings. - /// - /// [`Renderer`]: crate::Renderer - /// [`Backend`]: crate::Backend - /// [`Settings`]: Self::Settings - #[allow(unsafe_code)] - unsafe fn new( - settings: Self::Settings, - loader_function: impl FnMut(&str) -> *const c_void, - ) -> Result<(Self, Self::Renderer), Error>; - - /// Returns the amount of samples that should be used when configuring - /// an OpenGL context for this [`GLCompositor`]. - fn sample_count(settings: &Self::Settings) -> u32; - - /// Resizes the viewport of the [`GLCompositor`]. - fn resize_viewport(&mut self, physical_size: Size); - - /// Returns [`Information`] used by this [`GLCompositor`]. - fn fetch_information(&self) -> Information; - - /// Presents the primitives of the [`Renderer`] to the next frame of the - /// [`GLCompositor`]. - /// - /// [`Renderer`]: crate::Renderer - fn present>( - &mut self, - renderer: &mut Self::Renderer, - viewport: &Viewport, - background_color: Color, - overlay: &[T], - ); -} diff --git a/lazy/Cargo.toml b/lazy/Cargo.toml deleted file mode 100644 index c739b312..00000000 --- a/lazy/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "iced_lazy" -version = "0.5.0" -authors = ["Héctor Ramón Jiménez "] -edition = "2021" -description = "Lazy widgets for Iced" -license = "MIT" -repository = "https://github.com/iced-rs/iced" -documentation = "https://docs.rs/iced_lazy" -keywords = ["gui", "ui", "graphics", "interface", "widgets"] -categories = ["gui"] - -[dependencies] -ouroboros = "0.13" - -[dependencies.iced_native] -version = "0.9" -path = "../native" diff --git a/lazy/src/cache.rs b/lazy/src/cache.rs deleted file mode 100644 index 5b4a39f6..00000000 --- a/lazy/src/cache.rs +++ /dev/null @@ -1,13 +0,0 @@ -use iced_native::overlay; -use iced_native::Element; - -use ouroboros::self_referencing; - -#[self_referencing(pub_extras)] -pub struct Cache<'a, Message: 'a, Renderer: 'a> { - pub element: Element<'a, Message, Renderer>, - - #[borrows(mut element)] - #[covariant] - overlay: Option>, -} diff --git a/lazy/src/component.rs b/lazy/src/component.rs deleted file mode 100644 index b23da9f7..00000000 --- a/lazy/src/component.rs +++ /dev/null @@ -1,575 +0,0 @@ -//! Build and reuse custom widgets using The Elm Architecture. -use iced_native::event; -use iced_native::layout::{self, Layout}; -use iced_native::mouse; -use iced_native::overlay; -use iced_native::renderer; -use iced_native::widget; -use iced_native::widget::tree::{self, Tree}; -use iced_native::{ - Clipboard, Element, Length, Point, Rectangle, Shell, Size, Widget, -}; - -use ouroboros::self_referencing; -use std::cell::RefCell; -use std::marker::PhantomData; - -/// A reusable, custom widget that uses The Elm Architecture. -/// -/// A [`Component`] allows you to implement custom widgets as if they were -/// `iced` applications with encapsulated state. -/// -/// In other words, a [`Component`] allows you to turn `iced` applications into -/// custom widgets and embed them without cumbersome wiring. -/// -/// A [`Component`] produces widgets that may fire an [`Event`](Component::Event) -/// and update the internal state of the [`Component`]. -/// -/// Additionally, a [`Component`] is capable of producing a `Message` to notify -/// the parent application of any relevant interactions. -pub trait Component { - /// The internal state of this [`Component`]. - type State: Default; - - /// The type of event this [`Component`] handles internally. - type Event; - - /// Processes an [`Event`](Component::Event) and updates the [`Component`] state accordingly. - /// - /// It can produce a `Message` for the parent application. - fn update( - &mut self, - state: &mut Self::State, - event: Self::Event, - ) -> Option; - - /// Produces the widgets of the [`Component`], which may trigger an [`Event`](Component::Event) - /// on user interaction. - fn view(&self, state: &Self::State) -> Element<'_, Self::Event, Renderer>; - - /// Update the [`Component`] state based on the provided [`Operation`](widget::Operation) - /// - /// By default, it does nothing. - fn operate( - &self, - _state: &mut Self::State, - _operation: &mut dyn widget::Operation, - ) { - } -} - -/// Turns an implementor of [`Component`] into an [`Element`] that can be -/// embedded in any application. -pub fn view<'a, C, Message, Renderer>( - component: C, -) -> Element<'a, Message, Renderer> -where - C: Component + 'a, - C::State: 'static, - Message: 'a, - Renderer: iced_native::Renderer + 'a, -{ - Element::new(Instance { - state: RefCell::new(Some( - StateBuilder { - component: Box::new(component), - message: PhantomData, - state: PhantomData, - element_builder: |_| None, - } - .build(), - )), - }) -} - -struct Instance<'a, Message, Renderer, Event, S> { - state: RefCell>>, -} - -#[self_referencing] -struct State<'a, Message: 'a, Renderer: 'a, Event: 'a, S: 'a> { - component: - Box + 'a>, - message: PhantomData, - state: PhantomData, - - #[borrows(component)] - #[covariant] - element: Option>, -} - -impl<'a, Message, Renderer, Event, S> Instance<'a, Message, Renderer, Event, S> -where - S: Default, -{ - fn rebuild_element(&self, state: &S) { - let heads = self.state.borrow_mut().take().unwrap().into_heads(); - - *self.state.borrow_mut() = Some( - StateBuilder { - component: heads.component, - message: PhantomData, - state: PhantomData, - element_builder: |component| Some(component.view(state)), - } - .build(), - ); - } - - fn rebuild_element_with_operation( - &self, - state: &mut S, - operation: &mut dyn widget::Operation, - ) { - let heads = self.state.borrow_mut().take().unwrap().into_heads(); - - heads.component.operate(state, operation); - - *self.state.borrow_mut() = Some( - StateBuilder { - component: heads.component, - message: PhantomData, - state: PhantomData, - element_builder: |component| Some(component.view(state)), - } - .build(), - ); - } - - fn with_element( - &self, - f: impl FnOnce(&Element<'_, Event, Renderer>) -> T, - ) -> T { - self.with_element_mut(|element| f(element)) - } - - fn with_element_mut( - &self, - f: impl FnOnce(&mut Element<'_, Event, Renderer>) -> T, - ) -> T { - self.state - .borrow_mut() - .as_mut() - .unwrap() - .with_element_mut(|element| f(element.as_mut().unwrap())) - } -} - -impl<'a, Message, Renderer, Event, S> Widget - for Instance<'a, Message, Renderer, Event, S> -where - S: 'static + Default, - Renderer: iced_native::Renderer, -{ - fn tag(&self) -> tree::Tag { - struct Tag(T); - tree::Tag::of::>() - } - - fn state(&self) -> tree::State { - tree::State::new(S::default()) - } - - fn children(&self) -> Vec { - self.rebuild_element(&S::default()); - self.with_element(|element| vec![Tree::new(element)]) - } - - fn diff(&self, tree: &mut Tree) { - self.rebuild_element(tree.state.downcast_ref()); - self.with_element(|element| { - tree.diff_children(std::slice::from_ref(&element)) - }) - } - - fn width(&self) -> Length { - self.with_element(|element| element.as_widget().width()) - } - - fn height(&self) -> Length { - self.with_element(|element| element.as_widget().height()) - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - self.with_element(|element| { - element.as_widget().layout(renderer, limits) - }) - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: iced_native::Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - let mut local_messages = Vec::new(); - let mut local_shell = Shell::new(&mut local_messages); - - let event_status = self.with_element_mut(|element| { - element.as_widget_mut().on_event( - &mut tree.children[0], - event, - layout, - cursor_position, - renderer, - clipboard, - &mut local_shell, - ) - }); - - local_shell.revalidate_layout(|| shell.invalidate_layout()); - - if let Some(redraw_request) = local_shell.redraw_request() { - shell.request_redraw(redraw_request); - } - - if !local_messages.is_empty() { - let mut heads = self.state.take().unwrap().into_heads(); - - for message in local_messages.into_iter().filter_map(|message| { - heads - .component - .update(tree.state.downcast_mut::(), message) - }) { - shell.publish(message); - } - - self.state = RefCell::new(Some( - StateBuilder { - component: heads.component, - message: PhantomData, - state: PhantomData, - element_builder: |state| { - Some(state.view(tree.state.downcast_ref::())) - }, - } - .build(), - )); - - self.with_element(|element| { - tree.diff_children(std::slice::from_ref(&element)) - }); - - shell.invalidate_layout(); - } - - event_status - } - - fn operate( - &self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn widget::Operation, - ) { - self.rebuild_element_with_operation( - tree.state.downcast_mut(), - operation, - ); - - struct MapOperation<'a, B> { - operation: &'a mut dyn widget::Operation, - } - - impl<'a, T, B> widget::Operation for MapOperation<'a, B> { - fn container( - &mut self, - id: Option<&widget::Id>, - operate_on_children: &mut dyn FnMut( - &mut dyn widget::Operation, - ), - ) { - self.operation.container(id, &mut |operation| { - operate_on_children(&mut MapOperation { operation }); - }); - } - - fn focusable( - &mut self, - state: &mut dyn widget::operation::Focusable, - id: Option<&widget::Id>, - ) { - self.operation.focusable(state, id); - } - - fn text_input( - &mut self, - state: &mut dyn widget::operation::TextInput, - id: Option<&widget::Id>, - ) { - self.operation.text_input(state, id); - } - } - - self.with_element(|element| { - tree.diff_children(std::slice::from_ref(&element)); - - element.as_widget().operate( - &mut tree.children[0], - layout, - renderer, - &mut MapOperation { operation }, - ); - }); - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - self.with_element(|element| { - element.as_widget().draw( - &tree.children[0], - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ); - }); - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.with_element(|element| { - element.as_widget().mouse_interaction( - &tree.children[0], - layout, - cursor_position, - viewport, - renderer, - ) - }) - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - let overlay = OverlayBuilder { - instance: self, - 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, - ) - }, - ) - }, - } - .build(); - - let has_overlay = overlay.with_overlay(|overlay| { - overlay.as_ref().map(overlay::Element::position) - }); - - has_overlay.map(|position| { - overlay::Element::new( - position, - Box::new(OverlayInstance { - overlay: Some(overlay), - }), - ) - }) - } -} - -#[self_referencing] -struct Overlay<'a, 'b, Message, Renderer, Event, S> { - instance: &'a mut Instance<'b, Message, Renderer, Event, S>, - tree: &'a mut Tree, - types: PhantomData<(Message, Event, S)>, - - #[borrows(mut instance, mut tree)] - #[covariant] - overlay: Option>, -} - -struct OverlayInstance<'a, 'b, Message, Renderer, Event, S> { - overlay: Option>, -} - -impl<'a, 'b, Message, Renderer, Event, S> - OverlayInstance<'a, 'b, Message, Renderer, Event, S> -{ - fn with_overlay_maybe( - &self, - f: impl FnOnce(&overlay::Element<'_, Event, Renderer>) -> T, - ) -> Option { - self.overlay - .as_ref() - .unwrap() - .borrow_overlay() - .as_ref() - .map(f) - } - - fn with_overlay_mut_maybe( - &mut self, - f: impl FnOnce(&mut overlay::Element<'_, Event, Renderer>) -> T, - ) -> Option { - self.overlay - .as_mut() - .unwrap() - .with_overlay_mut(|overlay| overlay.as_mut().map(f)) - } -} - -impl<'a, 'b, Message, Renderer, Event, S> overlay::Overlay - for OverlayInstance<'a, 'b, Message, Renderer, Event, S> -where - Renderer: iced_native::Renderer, - S: 'static + Default, -{ - fn layout( - &self, - renderer: &Renderer, - bounds: Size, - position: Point, - ) -> layout::Node { - self.with_overlay_maybe(|overlay| { - let translation = position - overlay.position(); - - overlay.layout(renderer, bounds, translation) - }) - .unwrap_or_default() - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - ) { - let _ = self.with_overlay_maybe(|overlay| { - overlay.draw(renderer, theme, style, layout, cursor_position); - }); - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.with_overlay_maybe(|overlay| { - overlay.mouse_interaction( - layout, - cursor_position, - viewport, - renderer, - ) - }) - .unwrap_or_default() - } - - fn on_event( - &mut self, - event: iced_native::Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> iced_native::event::Status { - let mut local_messages = Vec::new(); - let mut local_shell = Shell::new(&mut local_messages); - - let event_status = self - .with_overlay_mut_maybe(|overlay| { - overlay.on_event( - event, - layout, - cursor_position, - renderer, - clipboard, - &mut local_shell, - ) - }) - .unwrap_or(iced_native::event::Status::Ignored); - - local_shell.revalidate_layout(|| shell.invalidate_layout()); - - if !local_messages.is_empty() { - let overlay = self.overlay.take().unwrap().into_heads(); - let mut heads = overlay.instance.state.take().unwrap().into_heads(); - - for message in local_messages.into_iter().filter_map(|message| { - heads - .component - .update(overlay.tree.state.downcast_mut::(), message) - }) { - shell.publish(message); - } - - *overlay.instance.state.borrow_mut() = Some( - StateBuilder { - component: heads.component, - message: PhantomData, - state: PhantomData, - element_builder: |state| { - Some(state.view(overlay.tree.state.downcast_ref::())) - }, - } - .build(), - ); - - overlay.instance.with_element(|element| { - overlay.tree.diff_children(std::slice::from_ref(&element)) - }); - - self.overlay = Some( - OverlayBuilder { - instance: overlay.instance, - tree: overlay.tree, - types: PhantomData, - overlay_builder: |_, _| None, - } - .build(), - ); - - shell.invalidate_layout(); - } - - event_status - } - - fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { - self.with_overlay_maybe(|overlay| { - overlay.is_over(layout, cursor_position) - }) - .unwrap_or_default() - } -} diff --git a/lazy/src/lazy.rs b/lazy/src/lazy.rs deleted file mode 100644 index 5e909a49..00000000 --- a/lazy/src/lazy.rs +++ /dev/null @@ -1,396 +0,0 @@ -use iced_native::event; -use iced_native::layout::{self, Layout}; -use iced_native::mouse; -use iced_native::overlay; -use iced_native::renderer; -use iced_native::widget::tree::{self, Tree}; -use iced_native::widget::{self, Widget}; -use iced_native::Element; -use iced_native::{Clipboard, Hasher, Length, Point, Rectangle, Shell, Size}; - -use ouroboros::self_referencing; -use std::cell::RefCell; -use std::hash::{Hash, Hasher as H}; -use std::rc::Rc; - -#[allow(missing_debug_implementations)] -pub struct Lazy<'a, Message, Renderer, Dependency, View> { - dependency: Dependency, - view: Box View + 'a>, - element: RefCell< - Option>>>>, - >, -} - -impl<'a, Message, Renderer, Dependency, View> - Lazy<'a, Message, Renderer, Dependency, View> -where - Dependency: Hash + 'a, - View: Into>, -{ - pub fn new( - dependency: Dependency, - view: impl Fn(&Dependency) -> View + 'a, - ) -> Self { - Self { - dependency, - view: Box::new(view), - element: RefCell::new(None), - } - } - - fn with_element( - &self, - f: impl FnOnce(&Element) -> T, - ) -> T { - f(self - .element - .borrow() - .as_ref() - .unwrap() - .borrow() - .as_ref() - .unwrap()) - } - - fn with_element_mut( - &self, - f: impl FnOnce(&mut Element) -> T, - ) -> T { - f(self - .element - .borrow() - .as_ref() - .unwrap() - .borrow_mut() - .as_mut() - .unwrap()) - } -} - -struct Internal { - element: Rc>>>, - hash: u64, -} - -impl<'a, Message, Renderer, Dependency, View> Widget - for Lazy<'a, Message, Renderer, Dependency, View> -where - View: Into> + 'static, - Dependency: Hash + 'a, - Message: 'static, - Renderer: iced_native::Renderer + 'static, -{ - fn tag(&self) -> tree::Tag { - struct Tag(T); - tree::Tag::of::>() - } - - fn state(&self) -> tree::State { - let mut hasher = Hasher::default(); - self.dependency.hash(&mut hasher); - let hash = hasher.finish(); - - let element = - Rc::new(RefCell::new(Some((self.view)(&self.dependency).into()))); - - (*self.element.borrow_mut()) = Some(element.clone()); - - tree::State::new(Internal { element, hash }) - } - - fn children(&self) -> Vec { - self.with_element(|element| vec![Tree::new(element.as_widget())]) - } - - fn diff(&self, tree: &mut Tree) { - let current = tree.state.downcast_mut::>(); - - let mut hasher = Hasher::default(); - self.dependency.hash(&mut hasher); - let new_hash = hasher.finish(); - - if current.hash != new_hash { - current.hash = new_hash; - - let element = (self.view)(&self.dependency).into(); - current.element = Rc::new(RefCell::new(Some(element))); - - (*self.element.borrow_mut()) = Some(current.element.clone()); - self.with_element(|element| { - tree.diff_children(std::slice::from_ref(&element.as_widget())) - }); - } else { - (*self.element.borrow_mut()) = Some(current.element.clone()); - } - } - - fn width(&self) -> Length { - self.with_element(|element| element.as_widget().width()) - } - - fn height(&self) -> Length { - self.with_element(|element| element.as_widget().height()) - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - self.with_element(|element| { - element.as_widget().layout(renderer, limits) - }) - } - - fn operate( - &self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn widget::Operation, - ) { - self.with_element(|element| { - element.as_widget().operate( - &mut tree.children[0], - layout, - renderer, - operation, - ); - }); - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: iced_native::Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.with_element_mut(|element| { - element.as_widget_mut().on_event( - &mut tree.children[0], - event, - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - }) - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.with_element(|element| { - element.as_widget().mouse_interaction( - &tree.children[0], - layout, - cursor_position, - viewport, - renderer, - ) - }) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - self.with_element(|element| { - element.as_widget().draw( - &tree.children[0], - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ) - }) - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - let overlay = Overlay(Some( - InnerBuilder { - cell: self.element.borrow().as_ref().unwrap().clone(), - element: self - .element - .borrow() - .as_ref() - .unwrap() - .borrow_mut() - .take() - .unwrap(), - tree: &mut tree.children[0], - overlay_builder: |element, tree| { - element.as_widget_mut().overlay(tree, layout, renderer) - }, - } - .build(), - )); - - let has_overlay = overlay - .with_overlay_maybe(|overlay| overlay::Element::position(overlay)); - - has_overlay - .map(|position| overlay::Element::new(position, Box::new(overlay))) - } -} - -#[self_referencing] -struct Inner<'a, Message, Renderer> -where - Message: 'a, - Renderer: 'a, -{ - cell: Rc>>>, - element: Element<'static, Message, Renderer>, - tree: &'a mut Tree, - - #[borrows(mut element, mut tree)] - #[covariant] - overlay: Option>, -} - -struct Overlay<'a, Message, Renderer>(Option>); - -impl<'a, Message, Renderer> Drop for Overlay<'a, Message, Renderer> { - fn drop(&mut self) { - let heads = self.0.take().unwrap().into_heads(); - (*heads.cell.borrow_mut()) = Some(heads.element); - } -} - -impl<'a, Message, Renderer> Overlay<'a, Message, Renderer> { - fn with_overlay_maybe( - &self, - f: impl FnOnce(&overlay::Element<'_, Message, Renderer>) -> T, - ) -> Option { - self.0.as_ref().unwrap().borrow_overlay().as_ref().map(f) - } - - fn with_overlay_mut_maybe( - &mut self, - f: impl FnOnce(&mut overlay::Element<'_, Message, Renderer>) -> T, - ) -> Option { - self.0 - .as_mut() - .unwrap() - .with_overlay_mut(|overlay| overlay.as_mut().map(f)) - } -} - -impl<'a, Message, Renderer> overlay::Overlay - for Overlay<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, -{ - fn layout( - &self, - renderer: &Renderer, - bounds: Size, - position: Point, - ) -> layout::Node { - self.with_overlay_maybe(|overlay| { - let translation = position - overlay.position(); - - overlay.layout(renderer, bounds, translation) - }) - .unwrap_or_default() - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - ) { - let _ = self.with_overlay_maybe(|overlay| { - overlay.draw(renderer, theme, style, layout, cursor_position); - }); - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.with_overlay_maybe(|overlay| { - overlay.mouse_interaction( - layout, - cursor_position, - viewport, - renderer, - ) - }) - .unwrap_or_default() - } - - fn on_event( - &mut self, - event: iced_native::Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.with_overlay_mut_maybe(|overlay| { - overlay.on_event( - event, - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - }) - .unwrap_or(iced_native::event::Status::Ignored) - } - - fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { - self.with_overlay_maybe(|overlay| { - overlay.is_over(layout, cursor_position) - }) - .unwrap_or_default() - } -} - -impl<'a, Message, Renderer, Dependency, View> - From> - for Element<'a, Message, Renderer> -where - View: Into> + 'static, - Renderer: iced_native::Renderer + 'static, - Message: 'static, - Dependency: Hash + 'a, -{ - fn from(lazy: Lazy<'a, Message, Renderer, Dependency, View>) -> Self { - Self::new(lazy) - } -} diff --git a/lazy/src/lib.rs b/lazy/src/lib.rs deleted file mode 100644 index 41a28773..00000000 --- a/lazy/src/lib.rs +++ /dev/null @@ -1,66 +0,0 @@ -#![doc( - html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" -)] -#![deny( - missing_debug_implementations, - unused_results, - clippy::extra_unused_lifetimes, - clippy::from_over_into, - clippy::needless_borrow, - clippy::new_without_default, - clippy::useless_conversion -)] -#![forbid(unsafe_code)] -#![allow( - clippy::await_holding_refcell_ref, - clippy::inherent_to_string, - clippy::type_complexity -)] -#![cfg_attr(docsrs, feature(doc_cfg))] -mod lazy; - -pub mod component; -pub mod responsive; - -pub use component::Component; -pub use lazy::Lazy; -pub use responsive::Responsive; - -mod cache; - -use iced_native::{Element, Size}; -use std::hash::Hash; - -pub fn lazy<'a, Message, Renderer, Dependency, View>( - dependency: Dependency, - view: impl Fn(&Dependency) -> View + 'a, -) -> Lazy<'a, Message, Renderer, Dependency, View> -where - Dependency: Hash + 'a, - View: Into>, -{ - Lazy::new(dependency, view) -} - -/// Turns an implementor of [`Component`] into an [`Element`] that can be -/// embedded in any application. -pub fn component<'a, C, Message, Renderer>( - component: C, -) -> Element<'a, Message, Renderer> -where - C: Component + 'a, - C::State: 'static, - Message: 'a, - Renderer: iced_native::Renderer + 'a, -{ - component::view(component) -} - -pub fn responsive<'a, Message, Renderer>( - f: impl Fn(Size) -> Element<'a, Message, Renderer> + 'a, -) -> Responsive<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, -{ - Responsive::new(f) -} diff --git a/lazy/src/responsive.rs b/lazy/src/responsive.rs deleted file mode 100644 index 57c07de1..00000000 --- a/lazy/src/responsive.rs +++ /dev/null @@ -1,425 +0,0 @@ -use iced_native::event; -use iced_native::layout::{self, Layout}; -use iced_native::mouse; -use iced_native::overlay; -use iced_native::renderer; -use iced_native::widget::tree::{self, Tree}; -use iced_native::widget::{self, horizontal_space}; -use iced_native::{ - Clipboard, Element, Length, Point, Rectangle, Shell, Size, Widget, -}; - -use ouroboros::self_referencing; -use std::cell::{RefCell, RefMut}; -use std::marker::PhantomData; -use std::ops::Deref; - -/// A widget that is aware of its dimensions. -/// -/// A [`Responsive`] widget will always try to fill all the available space of -/// its parent. -#[allow(missing_debug_implementations)] -pub struct Responsive<'a, Message, Renderer> { - view: Box Element<'a, Message, Renderer> + 'a>, - content: RefCell>, -} - -impl<'a, Message, Renderer> Responsive<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, -{ - /// Creates a new [`Responsive`] widget with a closure that produces its - /// contents. - /// - /// The `view` closure will be provided with the current [`Size`] of - /// the [`Responsive`] widget and, therefore, can be used to build the - /// contents of the widget in a responsive way. - pub fn new( - view: impl Fn(Size) -> Element<'a, Message, Renderer> + 'a, - ) -> Self { - Self { - view: Box::new(view), - content: RefCell::new(Content { - size: Size::ZERO, - layout: layout::Node::new(Size::ZERO), - element: Element::new(horizontal_space(0)), - }), - } - } -} - -struct Content<'a, Message, Renderer> { - size: Size, - layout: layout::Node, - element: Element<'a, Message, Renderer>, -} - -impl<'a, Message, Renderer> Content<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, -{ - fn update( - &mut self, - tree: &mut Tree, - renderer: &Renderer, - new_size: Size, - view: &dyn Fn(Size) -> Element<'a, Message, Renderer>, - ) { - if self.size == new_size { - return; - } - - self.element = view(new_size); - self.size = new_size; - - tree.diff(&self.element); - - self.layout = self - .element - .as_widget() - .layout(renderer, &layout::Limits::new(Size::ZERO, self.size)); - } - - fn resolve( - &mut self, - tree: &mut Tree, - renderer: R, - layout: Layout<'_>, - view: &dyn Fn(Size) -> Element<'a, Message, Renderer>, - f: impl FnOnce( - &mut Tree, - R, - Layout<'_>, - &mut Element<'a, Message, Renderer>, - ) -> T, - ) -> T - where - R: Deref, - { - self.update(tree, renderer.deref(), layout.bounds().size(), view); - - let content_layout = Layout::with_offset( - layout.position() - Point::ORIGIN, - &self.layout, - ); - - f(tree, renderer, content_layout, &mut self.element) - } -} - -struct State { - tree: RefCell, -} - -impl<'a, Message, Renderer> Widget - for Responsive<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(State { - tree: RefCell::new(Tree::empty()), - }) - } - - fn width(&self) -> Length { - Length::Fill - } - - fn height(&self) -> Length { - Length::Fill - } - - fn layout( - &self, - _renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - layout::Node::new(limits.max()) - } - - fn operate( - &self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn widget::Operation, - ) { - let state = tree.state.downcast_mut::(); - let mut content = self.content.borrow_mut(); - - content.resolve( - &mut state.tree.borrow_mut(), - renderer, - layout, - &self.view, - |tree, renderer, layout, element| { - element - .as_widget() - .operate(tree, layout, renderer, operation); - }, - ); - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: iced_native::Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - let state = tree.state.downcast_mut::(); - let mut content = self.content.borrow_mut(); - - content.resolve( - &mut state.tree.borrow_mut(), - renderer, - layout, - &self.view, - |tree, renderer, layout, element| { - element.as_widget_mut().on_event( - tree, - event, - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - }, - ) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - let state = tree.state.downcast_ref::(); - let mut content = self.content.borrow_mut(); - - content.resolve( - &mut state.tree.borrow_mut(), - renderer, - layout, - &self.view, - |tree, renderer, layout, element| { - element.as_widget().draw( - tree, - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ) - }, - ) - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - let state = tree.state.downcast_ref::(); - let mut content = self.content.borrow_mut(); - - content.resolve( - &mut state.tree.borrow_mut(), - renderer, - layout, - &self.view, - |tree, renderer, layout, element| { - element.as_widget().mouse_interaction( - tree, - layout, - cursor_position, - viewport, - renderer, - ) - }, - ) - } - - fn overlay<'b>( - &'b mut 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| { - content.update( - tree, - renderer, - layout.bounds().size(), - &self.view, - ); - - let Content { - element, - layout: content_layout, - .. - } = content.deref_mut(); - - let content_layout = Layout::with_offset( - layout.bounds().position() - Point::ORIGIN, - content_layout, - ); - - element - .as_widget_mut() - .overlay(tree, content_layout, renderer) - }, - } - .build(); - - let has_overlay = overlay.with_overlay(|overlay| { - overlay.as_ref().map(overlay::Element::position) - }); - - has_overlay - .map(|position| overlay::Element::new(position, Box::new(overlay))) - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Renderer: iced_native::Renderer + 'a, - Message: 'a, -{ - fn from(responsive: Responsive<'a, Message, Renderer>) -> Self { - Self::new(responsive) - } -} - -#[self_referencing] -struct Overlay<'a, 'b, Message, Renderer> { - content: RefMut<'a, Content<'b, Message, Renderer>>, - tree: RefMut<'a, Tree>, - types: PhantomData, - - #[borrows(mut content, mut tree)] - #[covariant] - overlay: Option>, -} - -impl<'a, 'b, Message, Renderer> Overlay<'a, 'b, Message, Renderer> { - fn with_overlay_maybe( - &self, - f: impl FnOnce(&overlay::Element<'_, Message, Renderer>) -> T, - ) -> Option { - self.borrow_overlay().as_ref().map(f) - } - - fn with_overlay_mut_maybe( - &mut self, - f: impl FnOnce(&mut overlay::Element<'_, Message, Renderer>) -> T, - ) -> Option { - self.with_overlay_mut(|overlay| overlay.as_mut().map(f)) - } -} - -impl<'a, 'b, Message, Renderer> overlay::Overlay - for Overlay<'a, 'b, Message, Renderer> -where - Renderer: iced_native::Renderer, -{ - fn layout( - &self, - renderer: &Renderer, - bounds: Size, - position: Point, - ) -> layout::Node { - self.with_overlay_maybe(|overlay| { - let translation = position - overlay.position(); - - overlay.layout(renderer, bounds, translation) - }) - .unwrap_or_default() - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - ) { - let _ = self.with_overlay_maybe(|overlay| { - overlay.draw(renderer, theme, style, layout, cursor_position); - }); - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.with_overlay_maybe(|overlay| { - overlay.mouse_interaction( - layout, - cursor_position, - viewport, - renderer, - ) - }) - .unwrap_or_default() - } - - fn on_event( - &mut self, - event: iced_native::Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.with_overlay_mut_maybe(|overlay| { - overlay.on_event( - event, - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - }) - .unwrap_or(iced_native::event::Status::Ignored) - } - - fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { - self.with_overlay_maybe(|overlay| { - overlay.is_over(layout, cursor_position) - }) - .unwrap_or_default() - } -} diff --git a/native/Cargo.toml b/native/Cargo.toml index 1eedf0da..bc4e7ca1 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -11,9 +11,6 @@ repository = "https://github.com/iced-rs/iced" debug = [] [dependencies] -twox-hash = { version = "1.5", default-features = false } -unicode-segmentation = "1.6" -num-traits = "0.2" thiserror = "1" [dependencies.iced_core] @@ -24,7 +21,3 @@ path = "../core" version = "0.6" path = "../futures" features = ["thread-pool"] - -[dependencies.iced_style] -version = "0.7" -path = "../style" diff --git a/native/src/clipboard.rs b/native/src/clipboard.rs index c9105bc0..e727c4a7 100644 --- a/native/src/clipboard.rs +++ b/native/src/clipboard.rs @@ -3,28 +3,6 @@ use iced_futures::MaybeSend; use std::fmt; -/// A buffer for short-term storage and transfer within and between -/// applications. -pub trait Clipboard { - /// Reads the current content of the [`Clipboard`] as text. - fn read(&self) -> Option; - - /// Writes the given text contents to the [`Clipboard`]. - fn write(&mut self, contents: String); -} - -/// A null implementation of the [`Clipboard`] trait. -#[derive(Debug, Clone, Copy)] -pub struct Null; - -impl Clipboard for Null { - fn read(&self) -> Option { - None - } - - fn write(&mut self, _contents: String) {} -} - /// A clipboard action to be performed by some [`Command`]. /// /// [`Command`]: crate::Command diff --git a/native/src/command.rs b/native/src/command.rs index ca9d0b64..39bee8f6 100644 --- a/native/src/command.rs +++ b/native/src/command.rs @@ -28,7 +28,9 @@ impl Command { } /// Creates a [`Command`] that performs a [`widget::Operation`]. - pub fn widget(operation: impl widget::Operation + 'static) -> Self { + pub fn widget( + operation: impl iced_core::widget::Operation + 'static, + ) -> Self { Self(iced_futures::Command::single(Action::Widget( widget::Action::new(operation), ))) diff --git a/native/src/debug/basic.rs b/native/src/debug/basic.rs index 92f614da..32f725a1 100644 --- a/native/src/debug/basic.rs +++ b/native/src/debug/basic.rs @@ -1,5 +1,5 @@ #![allow(missing_docs)] -use crate::time; +use crate::core::time; use std::collections::VecDeque; diff --git a/native/src/element.rs b/native/src/element.rs deleted file mode 100644 index 0a677d20..00000000 --- a/native/src/element.rs +++ /dev/null @@ -1,583 +0,0 @@ -use crate::event::{self, Event}; -use crate::layout; -use crate::mouse; -use crate::overlay; -use crate::renderer; -use crate::widget; -use crate::widget::tree::{self, Tree}; -use crate::{ - Clipboard, Color, Layout, Length, Point, Rectangle, Shell, Widget, -}; - -use std::any::Any; -use std::borrow::Borrow; - -/// A generic [`Widget`]. -/// -/// It is useful to build composable user interfaces that do not leak -/// implementation details in their __view logic__. -/// -/// If you have a [built-in widget], you should be able to use `Into` -/// to turn it into an [`Element`]. -/// -/// [built-in widget]: crate::widget -#[allow(missing_debug_implementations)] -pub struct Element<'a, Message, Renderer> { - widget: Box + 'a>, -} - -impl<'a, Message, Renderer> Element<'a, Message, Renderer> { - /// Creates a new [`Element`] containing the given [`Widget`]. - pub fn new(widget: impl Widget + 'a) -> Self - where - Renderer: crate::Renderer, - { - Self { - widget: Box::new(widget), - } - } - - /// Returns a reference to the [`Widget`] of the [`Element`], - pub fn as_widget(&self) -> &dyn Widget { - self.widget.as_ref() - } - - /// Returns a mutable reference to the [`Widget`] of the [`Element`], - pub fn as_widget_mut(&mut self) -> &mut dyn Widget { - self.widget.as_mut() - } - - /// Applies a transformation to the produced message of the [`Element`]. - /// - /// This method is useful when you want to decouple different parts of your - /// UI and make them __composable__. - /// - /// # Example - /// Imagine we want to use [our counter](index.html#usage). But instead of - /// showing a single counter, we want to display many of them. We can reuse - /// the `Counter` type as it is! - /// - /// We use composition to model the __state__ of our new application: - /// - /// ``` - /// # mod counter { - /// # pub struct Counter; - /// # } - /// use counter::Counter; - /// - /// struct ManyCounters { - /// counters: Vec, - /// } - /// ``` - /// - /// We can store the state of multiple counters now. However, the - /// __messages__ we implemented before describe the user interactions - /// of a __single__ counter. Right now, we need to also identify which - /// counter is receiving user interactions. Can we use composition again? - /// Yes. - /// - /// ``` - /// # mod counter { - /// # #[derive(Debug, Clone, Copy)] - /// # pub enum Message {} - /// # } - /// #[derive(Debug, Clone, Copy)] - /// pub enum Message { - /// Counter(usize, counter::Message) - /// } - /// ``` - /// - /// We compose the previous __messages__ with the index of the counter - /// producing them. Let's implement our __view logic__ now: - /// - /// ``` - /// # mod counter { - /// # type Text<'a> = iced_native::widget::Text<'a, iced_native::renderer::Null>; - /// # - /// # #[derive(Debug, Clone, Copy)] - /// # pub enum Message {} - /// # pub struct Counter; - /// # - /// # impl Counter { - /// # pub fn view(&mut self) -> Text { - /// # Text::new("") - /// # } - /// # } - /// # } - /// # - /// # mod iced_wgpu { - /// # pub use iced_native::renderer::Null as Renderer; - /// # } - /// # - /// # use counter::Counter; - /// # - /// # struct ManyCounters { - /// # counters: Vec, - /// # } - /// # - /// # #[derive(Debug, Clone, Copy)] - /// # pub enum Message { - /// # Counter(usize, counter::Message) - /// # } - /// use iced_native::Element; - /// use iced_native::widget::Row; - /// use iced_wgpu::Renderer; - /// - /// impl ManyCounters { - /// pub fn view(&mut self) -> Row { - /// // We can quickly populate a `Row` by folding over our counters - /// self.counters.iter_mut().enumerate().fold( - /// Row::new().spacing(20), - /// |row, (index, counter)| { - /// // We display the counter - /// let element: Element = - /// counter.view().into(); - /// - /// row.push( - /// // Here we turn our `Element` into - /// // an `Element` by combining the `index` and the - /// // message of the `element`. - /// element.map(move |message| Message::Counter(index, message)) - /// ) - /// } - /// ) - /// } - /// } - /// ``` - /// - /// Finally, our __update logic__ is pretty straightforward: simple - /// delegation. - /// - /// ``` - /// # mod counter { - /// # #[derive(Debug, Clone, Copy)] - /// # pub enum Message {} - /// # pub struct Counter; - /// # - /// # impl Counter { - /// # pub fn update(&mut self, _message: Message) {} - /// # } - /// # } - /// # - /// # use counter::Counter; - /// # - /// # struct ManyCounters { - /// # counters: Vec, - /// # } - /// # - /// # #[derive(Debug, Clone, Copy)] - /// # pub enum Message { - /// # Counter(usize, counter::Message) - /// # } - /// impl ManyCounters { - /// pub fn update(&mut self, message: Message) { - /// match message { - /// Message::Counter(index, counter_msg) => { - /// if let Some(counter) = self.counters.get_mut(index) { - /// counter.update(counter_msg); - /// } - /// } - /// } - /// } - /// } - /// ``` - pub fn map( - self, - f: impl Fn(Message) -> B + 'a, - ) -> Element<'a, B, Renderer> - where - Message: 'a, - Renderer: crate::Renderer + 'a, - B: 'a, - { - Element::new(Map::new(self.widget, f)) - } - - /// Marks the [`Element`] as _to-be-explained_. - /// - /// The [`Renderer`] will explain the layout of the [`Element`] graphically. - /// This can be very useful for debugging your layout! - /// - /// [`Renderer`]: crate::Renderer - pub fn explain>( - self, - color: C, - ) -> Element<'a, Message, Renderer> - where - Message: 'static, - Renderer: crate::Renderer + 'a, - { - Element { - widget: Box::new(Explain::new(self, color.into())), - } - } -} - -impl<'a, Message, Renderer> Borrow + 'a> - for Element<'a, Message, Renderer> -{ - fn borrow(&self) -> &(dyn Widget + 'a) { - self.widget.borrow() - } -} - -impl<'a, Message, Renderer> Borrow + 'a> - for &Element<'a, Message, Renderer> -{ - fn borrow(&self) -> &(dyn Widget + 'a) { - self.widget.borrow() - } -} - -struct Map<'a, A, B, Renderer> { - widget: Box + 'a>, - mapper: Box B + 'a>, -} - -impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> { - pub fn new( - widget: Box + 'a>, - mapper: F, - ) -> Map<'a, A, B, Renderer> - where - F: 'a + Fn(A) -> B, - { - Map { - widget, - mapper: Box::new(mapper), - } - } -} - -impl<'a, A, B, Renderer> Widget for Map<'a, A, B, Renderer> -where - Renderer: crate::Renderer + 'a, - A: 'a, - B: 'a, -{ - fn tag(&self) -> tree::Tag { - self.widget.tag() - } - - fn state(&self) -> tree::State { - self.widget.state() - } - - fn children(&self) -> Vec { - self.widget.children() - } - - fn diff(&self, tree: &mut Tree) { - self.widget.diff(tree) - } - - fn width(&self) -> Length { - self.widget.width() - } - - fn height(&self) -> Length { - self.widget.height() - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - self.widget.layout(renderer, limits) - } - - fn operate( - &self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn widget::Operation, - ) { - struct MapOperation<'a, B> { - operation: &'a mut dyn widget::Operation, - } - - impl<'a, T, B> widget::Operation for MapOperation<'a, B> { - fn container( - &mut self, - id: Option<&widget::Id>, - operate_on_children: &mut dyn FnMut( - &mut dyn widget::Operation, - ), - ) { - self.operation.container(id, &mut |operation| { - operate_on_children(&mut MapOperation { operation }); - }); - } - - fn focusable( - &mut self, - state: &mut dyn widget::operation::Focusable, - id: Option<&widget::Id>, - ) { - self.operation.focusable(state, id); - } - - fn scrollable( - &mut self, - state: &mut dyn widget::operation::Scrollable, - id: Option<&widget::Id>, - ) { - self.operation.scrollable(state, id); - } - - fn text_input( - &mut self, - state: &mut dyn widget::operation::TextInput, - id: Option<&widget::Id>, - ) { - self.operation.text_input(state, id); - } - - fn custom(&mut self, state: &mut dyn Any, id: Option<&widget::Id>) { - self.operation.custom(state, id); - } - } - - self.widget.operate( - tree, - layout, - renderer, - &mut MapOperation { 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<'_, B>, - ) -> event::Status { - let mut local_messages = Vec::new(); - let mut local_shell = Shell::new(&mut local_messages); - - let status = self.widget.on_event( - tree, - event, - layout, - cursor_position, - renderer, - clipboard, - &mut local_shell, - ); - - shell.merge(local_shell, &self.mapper); - - status - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - self.widget.draw( - tree, - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ) - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.widget.mouse_interaction( - tree, - layout, - cursor_position, - viewport, - renderer, - ) - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - let mapper = &self.mapper; - - self.widget - .overlay(tree, layout, renderer) - .map(move |overlay| overlay.map(mapper)) - } -} - -struct Explain<'a, Message, Renderer: crate::Renderer> { - element: Element<'a, Message, Renderer>, - color: Color, -} - -impl<'a, Message, Renderer> Explain<'a, Message, Renderer> -where - Renderer: crate::Renderer, -{ - fn new(element: Element<'a, Message, Renderer>, color: Color) -> Self { - Explain { element, color } - } -} - -impl<'a, Message, Renderer> Widget - for Explain<'a, Message, Renderer> -where - Renderer: crate::Renderer, -{ - fn width(&self) -> Length { - self.element.widget.width() - } - - fn height(&self) -> Length { - self.element.widget.height() - } - - fn tag(&self) -> tree::Tag { - self.element.widget.tag() - } - - fn state(&self) -> tree::State { - self.element.widget.state() - } - - fn children(&self) -> Vec { - self.element.widget.children() - } - - fn diff(&self, tree: &mut Tree) { - self.element.widget.diff(tree); - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - self.element.widget.layout(renderer, limits) - } - - fn operate( - &self, - state: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn widget::Operation, - ) { - self.element - .widget - .operate(state, layout, renderer, operation) - } - - fn on_event( - &mut self, - state: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.element.widget.on_event( - state, - event, - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - } - - fn draw( - &self, - state: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - fn explain_layout( - renderer: &mut Renderer, - color: Color, - layout: Layout<'_>, - ) { - renderer.fill_quad( - renderer::Quad { - bounds: layout.bounds(), - border_color: color, - border_width: 1.0, - border_radius: 0.0.into(), - }, - Color::TRANSPARENT, - ); - - for child in layout.children() { - explain_layout(renderer, color, child); - } - } - - self.element.widget.draw( - state, - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ); - - explain_layout(renderer, self.color, layout); - } - - fn mouse_interaction( - &self, - state: &Tree, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.element.widget.mouse_interaction( - state, - layout, - cursor_position, - viewport, - renderer, - ) - } - - fn overlay<'b>( - &'b mut self, - state: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - self.element.widget.overlay(state, layout, renderer) - } -} diff --git a/native/src/event.rs b/native/src/event.rs deleted file mode 100644 index bcfaf891..00000000 --- a/native/src/event.rs +++ /dev/null @@ -1,78 +0,0 @@ -//! Handle events of a user interface. -use crate::keyboard; -use crate::mouse; -use crate::touch; -use crate::window; - -/// A user interface event. -/// -/// _**Note:** This type is largely incomplete! If you need to track -/// additional events, feel free to [open an issue] and share your use case!_ -/// -/// [open an issue]: https://github.com/iced-rs/iced/issues -#[derive(Debug, Clone, PartialEq)] -pub enum Event { - /// A keyboard event - Keyboard(keyboard::Event), - - /// A mouse event - Mouse(mouse::Event), - - /// A window event - Window(window::Event), - - /// A touch event - Touch(touch::Event), - - /// A platform specific event - PlatformSpecific(PlatformSpecific), -} - -/// A platform specific event -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum PlatformSpecific { - /// A MacOS specific event - MacOS(MacOS), -} - -/// Describes an event specific to MacOS -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum MacOS { - /// Triggered when the app receives an URL from the system - /// - /// _**Note:** For this event to be triggered, the executable needs to be properly [bundled]!_ - /// - /// [bundled]: https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW19 - ReceivedUrl(String), -} - -/// The status of an [`Event`] after being processed. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Status { - /// The [`Event`] was **NOT** handled by any widget. - Ignored, - - /// The [`Event`] was handled and processed by a widget. - Captured, -} - -impl Status { - /// Merges two [`Status`] into one. - /// - /// `Captured` takes precedence over `Ignored`: - /// - /// ``` - /// use iced_native::event::Status; - /// - /// assert_eq!(Status::Ignored.merge(Status::Ignored), Status::Ignored); - /// assert_eq!(Status::Ignored.merge(Status::Captured), Status::Captured); - /// assert_eq!(Status::Captured.merge(Status::Ignored), Status::Captured); - /// assert_eq!(Status::Captured.merge(Status::Captured), Status::Captured); - /// ``` - pub fn merge(self, b: Self) -> Self { - match self { - Status::Ignored => b, - Status::Captured => Status::Captured, - } - } -} diff --git a/native/src/hasher.rs b/native/src/hasher.rs deleted file mode 100644 index fa52f16d..00000000 --- a/native/src/hasher.rs +++ /dev/null @@ -1,13 +0,0 @@ -/// The hasher used to compare layouts. -#[derive(Debug, Default)] -pub struct Hasher(twox_hash::XxHash64); - -impl core::hash::Hasher for Hasher { - fn write(&mut self, bytes: &[u8]) { - self.0.write(bytes) - } - - fn finish(&self) -> u64 { - self.0.finish() - } -} diff --git a/native/src/image.rs b/native/src/image.rs deleted file mode 100644 index 70fbade0..00000000 --- a/native/src/image.rs +++ /dev/null @@ -1,174 +0,0 @@ -//! Load and draw raster graphics. -use crate::{Hasher, Rectangle, Size}; - -use std::hash::{Hash, Hasher as _}; -use std::path::PathBuf; -use std::sync::Arc; - -/// A handle of some image data. -#[derive(Debug, Clone)] -pub struct Handle { - id: u64, - data: Data, -} - -impl Handle { - /// Creates an image [`Handle`] pointing to the image of the given path. - /// - /// Makes an educated guess about the image format by examining the data in the file. - pub fn from_path>(path: T) -> Handle { - Self::from_data(Data::Path(path.into())) - } - - /// Creates an image [`Handle`] containing the image pixels directly. This - /// function expects the input data to be provided as a `Vec` of RGBA - /// pixels. - /// - /// This is useful if you have already decoded your image. - pub fn from_pixels( - width: u32, - height: u32, - pixels: impl AsRef<[u8]> + Send + Sync + 'static, - ) -> Handle { - Self::from_data(Data::Rgba { - width, - height, - pixels: Bytes::new(pixels), - }) - } - - /// Creates an image [`Handle`] containing the image data directly. - /// - /// Makes an educated guess about the image format by examining the given data. - /// - /// This is useful if you already have your image loaded in-memory, maybe - /// because you downloaded or generated it procedurally. - pub fn from_memory( - bytes: impl AsRef<[u8]> + Send + Sync + 'static, - ) -> Handle { - Self::from_data(Data::Bytes(Bytes::new(bytes))) - } - - fn from_data(data: Data) -> Handle { - let mut hasher = Hasher::default(); - data.hash(&mut hasher); - - Handle { - id: hasher.finish(), - data, - } - } - - /// Returns the unique identifier of the [`Handle`]. - pub fn id(&self) -> u64 { - self.id - } - - /// Returns a reference to the image [`Data`]. - pub fn data(&self) -> &Data { - &self.data - } -} - -impl From for Handle -where - T: Into, -{ - fn from(path: T) -> Handle { - Handle::from_path(path.into()) - } -} - -impl Hash for Handle { - fn hash(&self, state: &mut H) { - self.id.hash(state); - } -} - -/// A wrapper around raw image data. -/// -/// It behaves like a `&[u8]`. -#[derive(Clone)] -pub struct Bytes(Arc + Send + Sync + 'static>); - -impl Bytes { - /// Creates new [`Bytes`] around `data`. - pub fn new(data: impl AsRef<[u8]> + Send + Sync + 'static) -> Self { - Self(Arc::new(data)) - } -} - -impl std::fmt::Debug for Bytes { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.as_ref().as_ref().fmt(f) - } -} - -impl std::hash::Hash for Bytes { - fn hash(&self, state: &mut H) { - self.0.as_ref().as_ref().hash(state); - } -} - -impl AsRef<[u8]> for Bytes { - fn as_ref(&self) -> &[u8] { - self.0.as_ref().as_ref() - } -} - -impl std::ops::Deref for Bytes { - type Target = [u8]; - - fn deref(&self) -> &[u8] { - self.0.as_ref().as_ref() - } -} - -/// The data of a raster image. -#[derive(Clone, Hash)] -pub enum Data { - /// File data - Path(PathBuf), - - /// In-memory data - Bytes(Bytes), - - /// Decoded image pixels in RGBA format. - Rgba { - /// The width of the image. - width: u32, - /// The height of the image. - height: u32, - /// The pixels. - pixels: Bytes, - }, -} - -impl std::fmt::Debug for Data { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Data::Path(path) => write!(f, "Path({path:?})"), - Data::Bytes(_) => write!(f, "Bytes(...)"), - Data::Rgba { width, height, .. } => { - write!(f, "Pixels({width} * {height})") - } - } - } -} - -/// A [`Renderer`] that can render raster graphics. -/// -/// [renderer]: crate::renderer -pub trait Renderer: crate::Renderer { - /// The image Handle to be displayed. Iced exposes its own default implementation of a [`Handle`] - /// - /// [`Handle`]: Self::Handle - type Handle: Clone + Hash; - - /// Returns the dimensions of an image for the given [`Handle`]. - fn dimensions(&self, handle: &Self::Handle) -> Size; - - /// Draws an image with the given [`Handle`] and inside the provided - /// `bounds`. - fn draw(&mut self, handle: Self::Handle, bounds: Rectangle); -} diff --git a/native/src/layout.rs b/native/src/layout.rs deleted file mode 100644 index 04954fb9..00000000 --- a/native/src/layout.rs +++ /dev/null @@ -1,65 +0,0 @@ -//! Position your widgets properly. -mod limits; -mod node; - -pub mod flex; - -pub use limits::Limits; -pub use node::Node; - -use crate::{Point, Rectangle, Vector}; - -/// The bounds of a [`Node`] and its children, using absolute coordinates. -#[derive(Debug, Clone, Copy)] -pub struct Layout<'a> { - position: Point, - node: &'a Node, -} - -impl<'a> Layout<'a> { - /// Creates a new [`Layout`] for the given [`Node`] at the origin. - pub fn new(node: &'a Node) -> Self { - Self::with_offset(Vector::new(0.0, 0.0), node) - } - - /// Creates a new [`Layout`] for the given [`Node`] with the provided offset - /// from the origin. - pub fn with_offset(offset: Vector, node: &'a Node) -> Self { - let bounds = node.bounds(); - - Self { - position: Point::new(bounds.x, bounds.y) + offset, - node, - } - } - - /// Returns the position of the [`Layout`]. - pub fn position(&self) -> Point { - self.position - } - - /// Returns the bounds of the [`Layout`]. - /// - /// The returned [`Rectangle`] describes the position and size of a - /// [`Node`]. - pub fn bounds(&self) -> Rectangle { - let bounds = self.node.bounds(); - - Rectangle { - x: self.position.x, - y: self.position.y, - width: bounds.width, - height: bounds.height, - } - } - - /// Returns an iterator over the [`Layout`] of the children of a [`Node`]. - pub fn children(self) -> impl Iterator> { - self.node.children().iter().map(move |node| { - Layout::with_offset( - Vector::new(self.position.x, self.position.y), - node, - ) - }) - } -} diff --git a/native/src/layout/DRUID_LICENSE b/native/src/layout/DRUID_LICENSE deleted file mode 100644 index d6456956..00000000 --- a/native/src/layout/DRUID_LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/native/src/layout/flex.rs b/native/src/layout/flex.rs deleted file mode 100644 index 5d70c2fc..00000000 --- a/native/src/layout/flex.rs +++ /dev/null @@ -1,232 +0,0 @@ -//! Distribute elements using a flex-based layout. -// This code is heavily inspired by the [`druid`] codebase. -// -// [`druid`]: https://github.com/xi-editor/druid -// -// Copyright 2018 The xi-editor Authors, Héctor Ramón -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -use crate::Element; - -use crate::layout::{Limits, Node}; -use crate::{Alignment, Padding, Point, Size}; - -/// The main axis of a flex layout. -#[derive(Debug)] -pub enum Axis { - /// The horizontal axis - Horizontal, - - /// The vertical axis - Vertical, -} - -impl Axis { - fn main(&self, size: Size) -> f32 { - match self { - Axis::Horizontal => size.width, - Axis::Vertical => size.height, - } - } - - fn cross(&self, size: Size) -> f32 { - match self { - Axis::Horizontal => size.height, - Axis::Vertical => size.width, - } - } - - fn pack(&self, main: f32, cross: f32) -> (f32, f32) { - match self { - Axis::Horizontal => (main, cross), - Axis::Vertical => (cross, main), - } - } -} - -/// Computes the flex layout with the given axis and limits, applying spacing, -/// padding and alignment to the items as needed. -/// -/// It returns a new layout [`Node`]. -pub fn resolve( - axis: Axis, - renderer: &Renderer, - limits: &Limits, - padding: Padding, - spacing: f32, - align_items: Alignment, - items: &[Element<'_, Message, Renderer>], -) -> Node -where - Renderer: crate::Renderer, -{ - let limits = limits.pad(padding); - let total_spacing = spacing * items.len().saturating_sub(1) as f32; - let max_cross = axis.cross(limits.max()); - - let mut fill_sum = 0; - let mut cross = axis.cross(limits.min()).max(axis.cross(limits.fill())); - let mut available = axis.main(limits.max()) - total_spacing; - - let mut nodes: Vec = Vec::with_capacity(items.len()); - nodes.resize(items.len(), Node::default()); - - if align_items == Alignment::Fill { - let mut fill_cross = axis.cross(limits.min()); - - items.iter().for_each(|child| { - let cross_fill_factor = match axis { - Axis::Horizontal => child.as_widget().height(), - Axis::Vertical => child.as_widget().width(), - } - .fill_factor(); - - if cross_fill_factor == 0 { - let (max_width, max_height) = axis.pack(available, max_cross); - - let child_limits = - Limits::new(Size::ZERO, Size::new(max_width, max_height)); - - let layout = child.as_widget().layout(renderer, &child_limits); - let size = layout.size(); - - fill_cross = fill_cross.max(axis.cross(size)); - } - }); - - cross = fill_cross; - } - - for (i, child) in items.iter().enumerate() { - let fill_factor = match axis { - Axis::Horizontal => child.as_widget().width(), - Axis::Vertical => child.as_widget().height(), - } - .fill_factor(); - - if fill_factor == 0 { - let (min_width, min_height) = if align_items == Alignment::Fill { - axis.pack(0.0, cross) - } else { - axis.pack(0.0, 0.0) - }; - - let (max_width, max_height) = if align_items == Alignment::Fill { - axis.pack(available, cross) - } else { - axis.pack(available, max_cross) - }; - - let child_limits = Limits::new( - Size::new(min_width, min_height), - Size::new(max_width, max_height), - ); - - let layout = child.as_widget().layout(renderer, &child_limits); - let size = layout.size(); - - available -= axis.main(size); - - if align_items != Alignment::Fill { - cross = cross.max(axis.cross(size)); - } - - nodes[i] = layout; - } else { - fill_sum += fill_factor; - } - } - - let remaining = available.max(0.0); - - for (i, child) in items.iter().enumerate() { - let fill_factor = match axis { - Axis::Horizontal => child.as_widget().width(), - Axis::Vertical => child.as_widget().height(), - } - .fill_factor(); - - if fill_factor != 0 { - let max_main = remaining * fill_factor as f32 / fill_sum as f32; - let min_main = if max_main.is_infinite() { - 0.0 - } else { - max_main - }; - - let (min_width, min_height) = if align_items == Alignment::Fill { - axis.pack(min_main, cross) - } else { - axis.pack(min_main, axis.cross(limits.min())) - }; - - let (max_width, max_height) = if align_items == Alignment::Fill { - axis.pack(max_main, cross) - } else { - axis.pack(max_main, max_cross) - }; - - let child_limits = Limits::new( - Size::new(min_width, min_height), - Size::new(max_width, max_height), - ); - - let layout = child.as_widget().layout(renderer, &child_limits); - - if align_items != Alignment::Fill { - cross = cross.max(axis.cross(layout.size())); - } - - nodes[i] = layout; - } - } - - let pad = axis.pack(padding.left, padding.top); - let mut main = pad.0; - - for (i, node) in nodes.iter_mut().enumerate() { - if i > 0 { - main += spacing; - } - - let (x, y) = axis.pack(main, pad.1); - - node.move_to(Point::new(x, y)); - - match axis { - Axis::Horizontal => { - node.align( - Alignment::Start, - align_items, - Size::new(0.0, cross), - ); - } - Axis::Vertical => { - node.align( - align_items, - Alignment::Start, - Size::new(cross, 0.0), - ); - } - } - - let size = node.size(); - - main += axis.main(size); - } - - let (width, height) = axis.pack(main - pad.0, cross); - let size = limits.resolve(Size::new(width, height)); - - Node::with_children(size.pad(padding), nodes) -} diff --git a/native/src/layout/limits.rs b/native/src/layout/limits.rs deleted file mode 100644 index 5d3c1556..00000000 --- a/native/src/layout/limits.rs +++ /dev/null @@ -1,163 +0,0 @@ -#![allow(clippy::manual_clamp)] -use crate::{Length, Padding, Size}; - -/// A set of size constraints for layouting. -#[derive(Debug, Clone, Copy)] -pub struct Limits { - min: Size, - max: Size, - fill: Size, -} - -impl Limits { - /// No limits - pub const NONE: Limits = Limits { - min: Size::ZERO, - max: Size::INFINITY, - fill: Size::INFINITY, - }; - - /// Creates new [`Limits`] with the given minimum and maximum [`Size`]. - pub const fn new(min: Size, max: Size) -> Limits { - Limits { - min, - max, - fill: Size::INFINITY, - } - } - - /// Returns the minimum [`Size`] of the [`Limits`]. - pub fn min(&self) -> Size { - self.min - } - - /// Returns the maximum [`Size`] of the [`Limits`]. - pub fn max(&self) -> Size { - self.max - } - - /// Returns the fill [`Size`] of the [`Limits`]. - pub fn fill(&self) -> Size { - self.fill - } - - /// Applies a width constraint to the current [`Limits`]. - pub fn width(mut self, width: impl Into) -> Limits { - match width.into() { - Length::Shrink => { - self.fill.width = self.min.width; - } - Length::Fill | Length::FillPortion(_) => { - self.fill.width = self.fill.width.min(self.max.width); - } - Length::Fixed(amount) => { - let new_width = amount.min(self.max.width).max(self.min.width); - - self.min.width = new_width; - self.max.width = new_width; - self.fill.width = new_width; - } - } - - self - } - - /// Applies a height constraint to the current [`Limits`]. - pub fn height(mut self, height: impl Into) -> Limits { - match height.into() { - Length::Shrink => { - self.fill.height = self.min.height; - } - Length::Fill | Length::FillPortion(_) => { - self.fill.height = self.fill.height.min(self.max.height); - } - Length::Fixed(amount) => { - let new_height = - amount.min(self.max.height).max(self.min.height); - - self.min.height = new_height; - self.max.height = new_height; - self.fill.height = new_height; - } - } - - self - } - - /// Applies a minimum width constraint to the current [`Limits`]. - pub fn min_width(mut self, min_width: f32) -> Limits { - self.min.width = self.min.width.max(min_width).min(self.max.width); - - self - } - - /// Applies a maximum width constraint to the current [`Limits`]. - pub fn max_width(mut self, max_width: f32) -> Limits { - self.max.width = self.max.width.min(max_width).max(self.min.width); - - self - } - - /// Applies a minimum height constraint to the current [`Limits`]. - pub fn min_height(mut self, min_height: f32) -> Limits { - self.min.height = self.min.height.max(min_height).min(self.max.height); - - self - } - - /// Applies a maximum height constraint to the current [`Limits`]. - pub fn max_height(mut self, max_height: f32) -> Limits { - self.max.height = self.max.height.min(max_height).max(self.min.height); - - self - } - - /// Shrinks the current [`Limits`] to account for the given padding. - pub fn pad(&self, padding: Padding) -> Limits { - self.shrink(Size::new(padding.horizontal(), padding.vertical())) - } - - /// Shrinks the current [`Limits`] by the given [`Size`]. - pub fn shrink(&self, size: Size) -> Limits { - let min = Size::new( - (self.min().width - size.width).max(0.0), - (self.min().height - size.height).max(0.0), - ); - - let max = Size::new( - (self.max().width - size.width).max(0.0), - (self.max().height - size.height).max(0.0), - ); - - let fill = Size::new( - (self.fill.width - size.width).max(0.0), - (self.fill.height - size.height).max(0.0), - ); - - Limits { min, max, fill } - } - - /// Removes the minimum width constraint for the current [`Limits`]. - pub fn loose(&self) -> Limits { - Limits { - min: Size::ZERO, - max: self.max, - fill: self.fill, - } - } - - /// Computes the resulting [`Size`] that fits the [`Limits`] given the - /// intrinsic size of some content. - pub fn resolve(&self, intrinsic_size: Size) -> Size { - Size::new( - intrinsic_size - .width - .min(self.max.width) - .max(self.fill.width), - intrinsic_size - .height - .min(self.max.height) - .max(self.fill.height), - ) - } -} diff --git a/native/src/layout/node.rs b/native/src/layout/node.rs deleted file mode 100644 index e0c7dcb2..00000000 --- a/native/src/layout/node.rs +++ /dev/null @@ -1,91 +0,0 @@ -use crate::{Alignment, Point, Rectangle, Size, Vector}; - -/// The bounds of an element and its children. -#[derive(Debug, Clone, Default)] -pub struct Node { - bounds: Rectangle, - children: Vec, -} - -impl Node { - /// Creates a new [`Node`] with the given [`Size`]. - pub const fn new(size: Size) -> Self { - Self::with_children(size, Vec::new()) - } - - /// Creates a new [`Node`] with the given [`Size`] and children. - pub const fn with_children(size: Size, children: Vec) -> Self { - Node { - bounds: Rectangle { - x: 0.0, - y: 0.0, - width: size.width, - height: size.height, - }, - children, - } - } - - /// Returns the [`Size`] of the [`Node`]. - pub fn size(&self) -> Size { - Size::new(self.bounds.width, self.bounds.height) - } - - /// Returns the bounds of the [`Node`]. - pub fn bounds(&self) -> Rectangle { - self.bounds - } - - /// Returns the children of the [`Node`]. - pub fn children(&self) -> &[Node] { - &self.children - } - - /// Aligns the [`Node`] in the given space. - pub fn align( - &mut self, - horizontal_alignment: Alignment, - vertical_alignment: Alignment, - space: Size, - ) { - match horizontal_alignment { - Alignment::Start => {} - Alignment::Center => { - self.bounds.x += (space.width - self.bounds.width) / 2.0; - } - Alignment::End => { - self.bounds.x += space.width - self.bounds.width; - } - Alignment::Fill => { - self.bounds.width = space.width; - } - } - - match vertical_alignment { - Alignment::Start => {} - Alignment::Center => { - self.bounds.y += (space.height - self.bounds.height) / 2.0; - } - Alignment::End => { - self.bounds.y += space.height - self.bounds.height; - } - Alignment::Fill => { - self.bounds.height = space.height; - } - } - } - - /// Moves the [`Node`] to the given position. - pub fn move_to(&mut self, position: Point) { - self.bounds.x = position.x; - self.bounds.y = position.y; - } - - /// Translates the [`Node`] by the given translation. - pub fn translate(self, translation: Vector) -> Self { - Self { - bounds: self.bounds + translation, - ..self - } - } -} diff --git a/native/src/lib.rs b/native/src/lib.rs index c98827e7..0fc4f324 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -42,32 +42,19 @@ clippy::useless_conversion )] #![forbid(unsafe_code, rust_2018_idioms)] -#![allow(clippy::inherent_to_string, clippy::type_complexity)] #![cfg_attr(docsrs, feature(doc_cfg))] pub mod clipboard; pub mod command; -pub mod event; pub mod font; -pub mod image; pub mod keyboard; -pub mod layout; -pub mod mouse; -pub mod overlay; pub mod program; -pub mod renderer; pub mod subscription; -pub mod svg; pub mod system; -pub mod text; -pub mod touch; pub mod user_interface; pub mod widget; pub mod window; -mod element; -mod hasher; mod runtime; -mod shell; // We disable debug capabilities on release builds unless the `debug` feature // is explicitly enabled. @@ -78,35 +65,13 @@ mod debug; #[path = "debug/null.rs"] mod debug; -pub use iced_core::alignment; -pub use iced_core::gradient; -pub use iced_core::time; -pub use iced_core::{ - color, Alignment, Background, Color, ContentFit, Length, Padding, Pixels, - Point, Rectangle, Size, Vector, -}; -pub use iced_futures::{executor, futures}; -pub use iced_style::application; -pub use iced_style::theme; +pub use iced_core as core; +pub use iced_futures as futures; -#[doc(no_inline)] -pub use executor::Executor; - -pub use clipboard::Clipboard; pub use command::Command; pub use debug::Debug; -pub use element::Element; -pub use event::Event; pub use font::Font; -pub use gradient::Gradient; -pub use hasher::Hasher; -pub use layout::Layout; -pub use overlay::Overlay; pub use program::Program; -pub use renderer::Renderer; pub use runtime::Runtime; -pub use shell::Shell; pub use subscription::Subscription; -pub use theme::Theme; pub use user_interface::UserInterface; -pub use widget::Widget; diff --git a/native/src/mouse.rs b/native/src/mouse.rs deleted file mode 100644 index 9ee406cf..00000000 --- a/native/src/mouse.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! Track mouse events. - -pub mod click; - -pub use click::Click; -pub use iced_core::mouse::*; diff --git a/native/src/mouse/click.rs b/native/src/mouse/click.rs deleted file mode 100644 index 4a7d796c..00000000 --- a/native/src/mouse/click.rs +++ /dev/null @@ -1,76 +0,0 @@ -//! Track mouse clicks. -use crate::time::Instant; -use crate::Point; - -/// A mouse click. -#[derive(Debug, Clone, Copy)] -pub struct Click { - kind: Kind, - position: Point, - time: Instant, -} - -/// The kind of mouse click. -#[derive(Debug, Clone, Copy)] -pub enum Kind { - /// A single click - Single, - - /// A double click - Double, - - /// A triple click - Triple, -} - -impl Kind { - fn next(&self) -> Kind { - match self { - Kind::Single => Kind::Double, - Kind::Double => Kind::Triple, - Kind::Triple => Kind::Double, - } - } -} - -impl Click { - /// Creates a new [`Click`] with the given position and previous last - /// [`Click`]. - pub fn new(position: Point, previous: Option) -> Click { - let time = Instant::now(); - - let kind = if let Some(previous) = previous { - if previous.is_consecutive(position, time) { - previous.kind.next() - } else { - Kind::Single - } - } else { - Kind::Single - }; - - Click { - kind, - position, - time, - } - } - - /// Returns the [`Kind`] of [`Click`]. - pub fn kind(&self) -> Kind { - self.kind - } - - fn is_consecutive(&self, new_position: Point, time: Instant) -> bool { - let duration = if time > self.time { - Some(time - self.time) - } else { - None - }; - - self.position == new_position - && duration - .map(|duration| duration.as_millis() <= 300) - .unwrap_or(false) - } -} diff --git a/native/src/overlay.rs b/native/src/overlay.rs deleted file mode 100644 index 6cada416..00000000 --- a/native/src/overlay.rs +++ /dev/null @@ -1,125 +0,0 @@ -//! Display interactive elements on top of other widgets. -mod element; -mod group; - -pub mod menu; - -pub use element::Element; -pub use group::Group; -pub use menu::Menu; - -use crate::event::{self, Event}; -use crate::layout; -use crate::mouse; -use crate::renderer; -use crate::widget; -use crate::widget::Tree; -use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size}; - -/// An interactive component that can be displayed on top of other widgets. -pub trait Overlay -where - Renderer: crate::Renderer, -{ - /// Returns the layout [`Node`] of the [`Overlay`]. - /// - /// This [`Node`] is used by the runtime to compute the [`Layout`] of the - /// user interface. - /// - /// [`Node`]: layout::Node - fn layout( - &self, - renderer: &Renderer, - bounds: Size, - position: Point, - ) -> layout::Node; - - /// Draws the [`Overlay`] using the associated `Renderer`. - fn draw( - &self, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - ); - - /// Applies a [`widget::Operation`] to the [`Overlay`]. - fn operate( - &mut self, - _layout: Layout<'_>, - _renderer: &Renderer, - _operation: &mut dyn widget::Operation, - ) { - } - - /// Processes a runtime [`Event`]. - /// - /// It receives: - /// * an [`Event`] describing user interaction - /// * the computed [`Layout`] of the [`Overlay`] - /// * the current cursor position - /// * a mutable `Message` list, allowing the [`Overlay`] to produce - /// new messages based on user interaction. - /// * the `Renderer` - /// * a [`Clipboard`], if available - /// - /// By default, it does nothing. - fn on_event( - &mut self, - _event: Event, - _layout: Layout<'_>, - _cursor_position: Point, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - _shell: &mut Shell<'_, Message>, - ) -> event::Status { - event::Status::Ignored - } - - /// Returns the current [`mouse::Interaction`] of the [`Overlay`]. - /// - /// By default, it returns [`mouse::Interaction::Idle`]. - fn mouse_interaction( - &self, - _layout: Layout<'_>, - _cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - mouse::Interaction::Idle - } - - /// Returns true if the cursor is over the [`Overlay`]. - /// - /// By default, it returns true if the bounds of the `layout` contain - /// the `cursor_position`. - fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { - layout.bounds().contains(cursor_position) - } -} - -/// Returns a [`Group`] of overlay [`Element`] children. -/// -/// 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>], - tree: &'a mut Tree, - layout: Layout<'_>, - renderer: &Renderer, -) -> Option> -where - Renderer: crate::Renderer, -{ - let children = children - .iter_mut() - .zip(&mut tree.children) - .zip(layout.children()) - .filter_map(|((child, state), layout)| { - child.as_widget_mut().overlay(state, layout, renderer) - }) - .collect::>(); - - (!children.is_empty()).then(|| Group::with_children(children).overlay()) -} diff --git a/native/src/overlay/element.rs b/native/src/overlay/element.rs deleted file mode 100644 index 237d25d1..00000000 --- a/native/src/overlay/element.rs +++ /dev/null @@ -1,270 +0,0 @@ -pub use crate::Overlay; - -use crate::event::{self, Event}; -use crate::layout; -use crate::mouse; -use crate::renderer; -use crate::widget; -use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size, Vector}; - -use std::any::Any; - -/// A generic [`Overlay`]. -#[allow(missing_debug_implementations)] -pub struct Element<'a, Message, Renderer> { - position: Point, - overlay: Box + 'a>, -} - -impl<'a, Message, Renderer> Element<'a, Message, Renderer> -where - Renderer: crate::Renderer, -{ - /// Creates a new [`Element`] containing the given [`Overlay`]. - pub fn new( - position: Point, - overlay: Box + 'a>, - ) -> Self { - Self { position, overlay } - } - - /// Returns the position of the [`Element`]. - pub fn position(&self) -> Point { - self.position - } - - /// Translates the [`Element`]. - pub fn translate(mut self, translation: Vector) -> Self { - self.position = self.position + translation; - self - } - - /// Applies a transformation to the produced message of the [`Element`]. - pub fn map(self, f: &'a dyn Fn(Message) -> B) -> Element<'a, B, Renderer> - where - Message: 'a, - Renderer: 'a, - B: 'a, - { - Element { - position: self.position, - overlay: Box::new(Map::new(self.overlay, f)), - } - } - - /// Computes the layout of the [`Element`] in the given bounds. - pub fn layout( - &self, - renderer: &Renderer, - bounds: Size, - translation: Vector, - ) -> layout::Node { - self.overlay - .layout(renderer, bounds, self.position + translation) - } - - /// Processes a runtime [`Event`]. - pub fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.overlay.on_event( - event, - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - } - - /// Returns the current [`mouse::Interaction`] of the [`Element`]. - pub fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.overlay.mouse_interaction( - layout, - cursor_position, - viewport, - renderer, - ) - } - - /// Draws the [`Element`] and its children using the given [`Layout`]. - pub fn draw( - &self, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - ) { - self.overlay - .draw(renderer, theme, style, layout, cursor_position) - } - - /// Applies a [`widget::Operation`] to the [`Element`]. - pub fn operate( - &mut self, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn widget::Operation, - ) { - self.overlay.operate(layout, renderer, operation); - } - - /// Returns true if the cursor is over the [`Element`]. - pub fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { - self.overlay.is_over(layout, cursor_position) - } -} - -struct Map<'a, A, B, Renderer> { - content: Box + 'a>, - mapper: &'a dyn Fn(A) -> B, -} - -impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> { - pub fn new( - content: Box + 'a>, - mapper: &'a dyn Fn(A) -> B, - ) -> Map<'a, A, B, Renderer> { - Map { content, mapper } - } -} - -impl<'a, A, B, Renderer> Overlay for Map<'a, A, B, Renderer> -where - Renderer: crate::Renderer, -{ - fn layout( - &self, - renderer: &Renderer, - bounds: Size, - position: Point, - ) -> layout::Node { - self.content.layout(renderer, bounds, position) - } - - fn operate( - &mut self, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn widget::Operation, - ) { - struct MapOperation<'a, B> { - operation: &'a mut dyn widget::Operation, - } - - impl<'a, T, B> widget::Operation for MapOperation<'a, B> { - fn container( - &mut self, - id: Option<&widget::Id>, - operate_on_children: &mut dyn FnMut( - &mut dyn widget::Operation, - ), - ) { - self.operation.container(id, &mut |operation| { - operate_on_children(&mut MapOperation { operation }); - }); - } - - fn focusable( - &mut self, - state: &mut dyn widget::operation::Focusable, - id: Option<&widget::Id>, - ) { - self.operation.focusable(state, id); - } - - fn scrollable( - &mut self, - state: &mut dyn widget::operation::Scrollable, - id: Option<&widget::Id>, - ) { - self.operation.scrollable(state, id); - } - - fn text_input( - &mut self, - state: &mut dyn widget::operation::TextInput, - id: Option<&widget::Id>, - ) { - self.operation.text_input(state, id) - } - - fn custom(&mut self, state: &mut dyn Any, id: Option<&widget::Id>) { - self.operation.custom(state, id); - } - } - - self.content - .operate(layout, renderer, &mut MapOperation { operation }); - } - - fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, B>, - ) -> event::Status { - let mut local_messages = Vec::new(); - let mut local_shell = Shell::new(&mut local_messages); - - let event_status = self.content.on_event( - event, - layout, - cursor_position, - renderer, - clipboard, - &mut local_shell, - ); - - shell.merge(local_shell, self.mapper); - - event_status - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.content.mouse_interaction( - layout, - cursor_position, - viewport, - renderer, - ) - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - ) { - self.content - .draw(renderer, theme, style, layout, cursor_position) - } - - fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { - self.content.is_over(layout, cursor_position) - } -} diff --git a/native/src/overlay/group.rs b/native/src/overlay/group.rs deleted file mode 100644 index 1126f0cf..00000000 --- a/native/src/overlay/group.rs +++ /dev/null @@ -1,174 +0,0 @@ -use iced_core::{Point, Rectangle, Size}; - -use crate::event; -use crate::layout; -use crate::mouse; -use crate::overlay; -use crate::renderer; -use crate::widget; -use crate::{Clipboard, Event, Layout, Overlay, Shell}; - -/// An [`Overlay`] container that displays multiple overlay [`overlay::Element`] -/// children. -#[allow(missing_debug_implementations)] -pub struct Group<'a, Message, Renderer> { - children: Vec>, -} - -impl<'a, Message, Renderer> Group<'a, Message, Renderer> -where - Renderer: 'a + crate::Renderer, - Message: 'a, -{ - /// Creates an empty [`Group`]. - pub fn new() -> Self { - Self::default() - } - - /// Creates a [`Group`] with the given elements. - pub fn with_children( - children: Vec>, - ) -> Self { - Group { children } - } - - /// Adds an [`overlay::Element`] to the [`Group`]. - pub fn push( - mut self, - child: impl Into>, - ) -> Self { - self.children.push(child.into()); - self - } - - /// Turns the [`Group`] into an overlay [`overlay::Element`]. - pub fn overlay(self) -> overlay::Element<'a, Message, Renderer> { - overlay::Element::new(Point::ORIGIN, Box::new(self)) - } -} - -impl<'a, Message, Renderer> Default for Group<'a, Message, Renderer> -where - Renderer: 'a + crate::Renderer, - Message: 'a, -{ - fn default() -> Self { - Self::with_children(Vec::new()) - } -} - -impl<'a, Message, Renderer> Overlay - for Group<'a, Message, Renderer> -where - Renderer: crate::Renderer, -{ - fn layout( - &self, - renderer: &Renderer, - bounds: Size, - position: Point, - ) -> layout::Node { - let translation = position - Point::ORIGIN; - - layout::Node::with_children( - bounds, - self.children - .iter() - .map(|child| child.layout(renderer, bounds, translation)) - .collect(), - ) - } - - fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.children - .iter_mut() - .zip(layout.children()) - .map(|(child, layout)| { - child.on_event( - event.clone(), - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - }) - .fold(event::Status::Ignored, event::Status::merge) - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - ) { - for (child, layout) in self.children.iter().zip(layout.children()) { - child.draw(renderer, theme, style, layout, cursor_position); - } - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.children - .iter() - .zip(layout.children()) - .map(|(child, layout)| { - child.mouse_interaction( - layout, - cursor_position, - viewport, - renderer, - ) - }) - .max() - .unwrap_or_default() - } - - fn operate( - &mut self, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn widget::Operation, - ) { - operation.container(None, &mut |operation| { - self.children.iter_mut().zip(layout.children()).for_each( - |(child, layout)| { - child.operate(layout, renderer, operation); - }, - ) - }); - } - - fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { - self.children - .iter() - .zip(layout.children()) - .any(|(child, layout)| child.is_over(layout, cursor_position)) - } -} - -impl<'a, Message, Renderer> From> - for overlay::Element<'a, Message, Renderer> -where - Renderer: 'a + crate::Renderer, - Message: 'a, -{ - fn from(group: Group<'a, Message, Renderer>) -> Self { - group.overlay() - } -} diff --git a/native/src/overlay/menu.rs b/native/src/overlay/menu.rs deleted file mode 100644 index bd58a122..00000000 --- a/native/src/overlay/menu.rs +++ /dev/null @@ -1,519 +0,0 @@ -//! Build and show dropdown menus. -use crate::alignment; -use crate::event::{self, Event}; -use crate::layout; -use crate::mouse; -use crate::overlay; -use crate::renderer; -use crate::text::{self, Text}; -use crate::touch; -use crate::widget::container::{self, Container}; -use crate::widget::scrollable::{self, Scrollable}; -use crate::widget::Tree; -use crate::{ - Clipboard, Color, Element, Layout, Length, Padding, Pixels, Point, - Rectangle, Shell, Size, Vector, Widget, -}; - -pub use iced_style::menu::{Appearance, StyleSheet}; - -/// A list of selectable options. -#[allow(missing_debug_implementations)] -pub struct Menu<'a, T, Renderer> -where - Renderer: text::Renderer, - Renderer::Theme: StyleSheet, -{ - state: &'a mut State, - options: &'a [T], - hovered_option: &'a mut Option, - last_selection: &'a mut Option, - width: f32, - padding: Padding, - text_size: Option, - font: Option, - style: ::Style, -} - -impl<'a, T, Renderer> Menu<'a, T, Renderer> -where - T: ToString + Clone, - Renderer: text::Renderer + 'a, - Renderer::Theme: - StyleSheet + container::StyleSheet + scrollable::StyleSheet, -{ - /// Creates a new [`Menu`] with the given [`State`], a list of options, and - /// the message to produced when an option is selected. - pub fn new( - state: &'a mut State, - options: &'a [T], - hovered_option: &'a mut Option, - last_selection: &'a mut Option, - ) -> Self { - Menu { - state, - options, - hovered_option, - last_selection, - width: 0.0, - padding: Padding::ZERO, - text_size: None, - font: None, - style: Default::default(), - } - } - - /// Sets the width of the [`Menu`]. - pub fn width(mut self, width: f32) -> Self { - self.width = width; - self - } - - /// Sets the [`Padding`] of the [`Menu`]. - pub fn padding>(mut self, padding: P) -> Self { - self.padding = padding.into(); - self - } - - /// Sets the text size of the [`Menu`]. - pub fn text_size(mut self, text_size: impl Into) -> Self { - self.text_size = Some(text_size.into().0); - self - } - - /// Sets the font of the [`Menu`]. - pub fn font(mut self, font: impl Into) -> Self { - self.font = Some(font.into()); - self - } - - /// Sets the style of the [`Menu`]. - pub fn style( - mut self, - style: impl Into<::Style>, - ) -> Self { - self.style = style.into(); - self - } - - /// Turns the [`Menu`] into an overlay [`Element`] at the given target - /// position. - /// - /// The `target_height` will be used to display the menu either on top - /// of the target or under it, depending on the screen position and the - /// dimensions of the [`Menu`]. - pub fn overlay( - self, - position: Point, - target_height: f32, - ) -> overlay::Element<'a, Message, Renderer> { - overlay::Element::new( - position, - Box::new(Overlay::new(self, target_height)), - ) - } -} - -/// The local state of a [`Menu`]. -#[derive(Debug)] -pub struct State { - tree: Tree, -} - -impl State { - /// Creates a new [`State`] for a [`Menu`]. - pub fn new() -> Self { - Self { - tree: Tree::empty(), - } - } -} - -impl Default for State { - fn default() -> Self { - Self::new() - } -} - -struct Overlay<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet + container::StyleSheet, -{ - state: &'a mut Tree, - container: Container<'a, Message, Renderer>, - width: f32, - target_height: f32, - style: ::Style, -} - -impl<'a, Message, Renderer> Overlay<'a, Message, Renderer> -where - Message: 'a, - Renderer: 'a, - Renderer: text::Renderer, - Renderer::Theme: - StyleSheet + container::StyleSheet + scrollable::StyleSheet, -{ - pub fn new(menu: Menu<'a, T, Renderer>, target_height: f32) -> Self - where - T: Clone + ToString, - { - let Menu { - state, - options, - hovered_option, - last_selection, - width, - padding, - font, - text_size, - style, - } = menu; - - let container = Container::new(Scrollable::new(List { - options, - hovered_option, - last_selection, - font, - text_size, - padding, - style: style.clone(), - })); - - state.tree.diff(&container as &dyn Widget<_, _>); - - Self { - state: &mut state.tree, - container, - width, - target_height, - style, - } - } -} - -impl<'a, Message, Renderer> crate::Overlay - for Overlay<'a, Message, Renderer> -where - Renderer: text::Renderer, - Renderer::Theme: StyleSheet + container::StyleSheet, -{ - fn layout( - &self, - renderer: &Renderer, - bounds: Size, - position: Point, - ) -> layout::Node { - let space_below = bounds.height - (position.y + self.target_height); - let space_above = position.y; - - let limits = layout::Limits::new( - Size::ZERO, - Size::new( - bounds.width - position.x, - if space_below > space_above { - space_below - } else { - space_above - }, - ), - ) - .width(self.width); - - let mut node = self.container.layout(renderer, &limits); - - node.move_to(if space_below > space_above { - position + Vector::new(0.0, self.target_height) - } else { - position - Vector::new(0.0, node.size().height) - }); - - node - } - - fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.container.on_event( - self.state, - event, - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.container.mouse_interaction( - self.state, - layout, - cursor_position, - viewport, - renderer, - ) - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - ) { - let appearance = theme.appearance(&self.style); - let bounds = layout.bounds(); - - renderer.fill_quad( - renderer::Quad { - bounds, - border_color: appearance.border_color, - border_width: appearance.border_width, - border_radius: appearance.border_radius.into(), - }, - appearance.background, - ); - - self.container.draw( - self.state, - renderer, - theme, - style, - layout, - cursor_position, - &bounds, - ); - } -} - -struct List<'a, T, Renderer> -where - Renderer: text::Renderer, - Renderer::Theme: StyleSheet, -{ - options: &'a [T], - hovered_option: &'a mut Option, - last_selection: &'a mut Option, - padding: Padding, - text_size: Option, - font: Option, - style: ::Style, -} - -impl<'a, T, Message, Renderer> Widget - for List<'a, T, Renderer> -where - T: Clone + ToString, - Renderer: text::Renderer, - Renderer::Theme: StyleSheet, -{ - fn width(&self) -> Length { - Length::Fill - } - - fn height(&self) -> Length { - Length::Shrink - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - use std::f32; - - let limits = limits.width(Length::Fill).height(Length::Shrink); - let text_size = - self.text_size.unwrap_or_else(|| renderer.default_size()); - - let size = { - let intrinsic = Size::new( - 0.0, - (text_size * 1.2 + self.padding.vertical()) - * self.options.len() as f32, - ); - - limits.resolve(intrinsic) - }; - - layout::Node::new(size) - } - - fn on_event( - &mut self, - _state: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - _shell: &mut Shell<'_, Message>, - ) -> event::Status { - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { - let bounds = layout.bounds(); - - if bounds.contains(cursor_position) { - if let Some(index) = *self.hovered_option { - if let Some(option) = self.options.get(index) { - *self.last_selection = Some(option.clone()); - } - } - } - } - Event::Mouse(mouse::Event::CursorMoved { .. }) => { - let bounds = layout.bounds(); - - if bounds.contains(cursor_position) { - let text_size = self - .text_size - .unwrap_or_else(|| renderer.default_size()); - - *self.hovered_option = Some( - ((cursor_position.y - bounds.y) - / (text_size * 1.2 + self.padding.vertical())) - as usize, - ); - } - } - Event::Touch(touch::Event::FingerPressed { .. }) => { - let bounds = layout.bounds(); - - if bounds.contains(cursor_position) { - let text_size = self - .text_size - .unwrap_or_else(|| renderer.default_size()); - - *self.hovered_option = Some( - ((cursor_position.y - bounds.y) - / (text_size * 1.2 + self.padding.vertical())) - as usize, - ); - - if let Some(index) = *self.hovered_option { - if let Some(option) = self.options.get(index) { - *self.last_selection = Some(option.clone()); - } - } - } - } - _ => {} - } - - event::Status::Ignored - } - - fn mouse_interaction( - &self, - _state: &Tree, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - let is_mouse_over = layout.bounds().contains(cursor_position); - - if is_mouse_over { - mouse::Interaction::Pointer - } else { - mouse::Interaction::default() - } - } - - fn draw( - &self, - _state: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - _cursor_position: Point, - viewport: &Rectangle, - ) { - let appearance = theme.appearance(&self.style); - let bounds = layout.bounds(); - - let text_size = - self.text_size.unwrap_or_else(|| renderer.default_size()); - let option_height = - (text_size * 1.2 + self.padding.vertical()) as usize; - - let offset = viewport.y - bounds.y; - let start = (offset / option_height as f32) as usize; - let end = - ((offset + viewport.height) / option_height as f32).ceil() as usize; - - let visible_options = &self.options[start..end.min(self.options.len())]; - - for (i, option) in visible_options.iter().enumerate() { - let i = start + i; - let is_selected = *self.hovered_option == Some(i); - - let bounds = Rectangle { - x: bounds.x, - y: bounds.y + (option_height * i) as f32, - width: bounds.width, - height: text_size * 1.2 + self.padding.vertical(), - }; - - if is_selected { - renderer.fill_quad( - renderer::Quad { - bounds, - border_color: Color::TRANSPARENT, - border_width: 0.0, - border_radius: appearance.border_radius.into(), - }, - appearance.selected_background, - ); - } - - renderer.fill_text(Text { - content: &option.to_string(), - bounds: Rectangle { - x: bounds.x + self.padding.left, - y: bounds.center_y(), - width: f32::INFINITY, - ..bounds - }, - size: text_size, - font: self.font.unwrap_or_else(|| renderer.default_font()), - color: if is_selected { - appearance.selected_text_color - } else { - appearance.text_color - }, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, - }); - } - } -} - -impl<'a, T, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - T: ToString + Clone, - Message: 'a, - Renderer: 'a + text::Renderer, - Renderer::Theme: StyleSheet, -{ - fn from(list: List<'a, T, Renderer>) -> Self { - Element::new(list) - } -} diff --git a/native/src/program.rs b/native/src/program.rs index 25cab332..44585cc5 100644 --- a/native/src/program.rs +++ b/native/src/program.rs @@ -1,6 +1,8 @@ //! Build interactive programs using The Elm Architecture. -use crate::text; -use crate::{Command, Element, Renderer}; +use crate::Command; + +use iced_core::text; +use iced_core::{Element, Renderer}; mod state; diff --git a/native/src/program/state.rs b/native/src/program/state.rs index 8ae1cacb..2fa9934d 100644 --- a/native/src/program/state.rs +++ b/native/src/program/state.rs @@ -1,9 +1,9 @@ -use crate::application; -use crate::event::{self, Event}; -use crate::mouse; -use crate::renderer; +use crate::core::event::{self, Event}; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::{Clipboard, Point, Size}; use crate::user_interface::{self, UserInterface}; -use crate::{Clipboard, Command, Debug, Point, Program, Size}; +use crate::{Command, Debug, Program}; /// The execution state of a [`Program`]. It leverages caching, event /// processing, and rendering primitive storage. @@ -22,7 +22,6 @@ where impl

State

where P: Program + 'static, - ::Theme: application::StyleSheet, { /// Creates a new [`State`] with the provided [`Program`], initializing its /// primitive with the given logical bounds and renderer. @@ -91,7 +90,7 @@ where bounds: Size, cursor_position: Point, renderer: &mut P::Renderer, - theme: &::Theme, + theme: &::Theme, style: &renderer::Style, clipboard: &mut dyn Clipboard, debug: &mut Debug, @@ -182,10 +181,7 @@ fn build_user_interface<'a, P: Program>( renderer: &mut P::Renderer, size: Size, debug: &mut Debug, -) -> UserInterface<'a, P::Message, P::Renderer> -where - ::Theme: application::StyleSheet, -{ +) -> UserInterface<'a, P::Message, P::Renderer> { debug.view_started(); let view = program.view(); debug.view_finished(); diff --git a/native/src/renderer.rs b/native/src/renderer.rs deleted file mode 100644 index 2ac78982..00000000 --- a/native/src/renderer.rs +++ /dev/null @@ -1,98 +0,0 @@ -//! Write your own renderer. -#[cfg(debug_assertions)] -mod null; -#[cfg(debug_assertions)] -pub use null::Null; - -use crate::layout; -use crate::{Background, Color, Element, Rectangle, Vector}; - -/// A component that can be used by widgets to draw themselves on a screen. -pub trait Renderer: Sized { - /// The supported theme of the [`Renderer`]. - type Theme; - - /// Lays out the elements of a user interface. - /// - /// You should override this if you need to perform any operations before or - /// after layouting. For instance, trimming the measurements cache. - fn layout( - &mut self, - element: &Element<'_, Message, Self>, - limits: &layout::Limits, - ) -> layout::Node { - element.as_widget().layout(self, limits) - } - - /// Draws the primitives recorded in the given closure in a new layer. - /// - /// The layer will clip its contents to the provided `bounds`. - fn with_layer(&mut self, bounds: Rectangle, f: impl FnOnce(&mut Self)); - - /// Applies a `translation` to the primitives recorded in the given closure. - fn with_translation( - &mut self, - translation: Vector, - f: impl FnOnce(&mut Self), - ); - - /// Fills a [`Quad`] with the provided [`Background`]. - fn fill_quad(&mut self, quad: Quad, background: impl Into); - - /// Clears all of the recorded primitives in the [`Renderer`]. - fn clear(&mut self); -} - -/// A polygon with four sides. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Quad { - /// The bounds of the [`Quad`]. - pub bounds: Rectangle, - - /// The border radius of the [`Quad`]. - pub border_radius: BorderRadius, - - /// The border width of the [`Quad`]. - pub border_width: f32, - - /// The border color of the [`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 { - /// The text color - pub text_color: Color, -} - -impl Default for Style { - fn default() -> Self { - Style { - text_color: Color::BLACK, - } - } -} diff --git a/native/src/renderer/null.rs b/native/src/renderer/null.rs deleted file mode 100644 index 150ee786..00000000 --- a/native/src/renderer/null.rs +++ /dev/null @@ -1,82 +0,0 @@ -use crate::renderer::{self, Renderer}; -use crate::text::{self, Text}; -use crate::{Background, Font, Point, Rectangle, Size, Theme, Vector}; - -use std::borrow::Cow; - -/// A renderer that does nothing. -/// -/// It can be useful if you are writing tests! -#[derive(Debug, Clone, Copy, Default)] -pub struct Null; - -impl Null { - /// Creates a new [`Null`] renderer. - pub fn new() -> Null { - Null - } -} - -impl Renderer for Null { - type Theme = Theme; - - fn with_layer(&mut self, _bounds: Rectangle, _f: impl FnOnce(&mut Self)) {} - - fn with_translation( - &mut self, - _translation: Vector, - _f: impl FnOnce(&mut Self), - ) { - } - - fn clear(&mut self) {} - - fn fill_quad( - &mut self, - _quad: renderer::Quad, - _background: impl Into, - ) { - } -} - -impl text::Renderer for Null { - type Font = Font; - - const ICON_FONT: Font = Font::SansSerif; - const CHECKMARK_ICON: char = '0'; - const ARROW_DOWN_ICON: char = '0'; - - fn default_font(&self) -> Self::Font { - Font::SansSerif - } - - fn default_size(&self) -> f32 { - 16.0 - } - - fn load_font(&mut self, _font: Cow<'static, [u8]>) {} - - fn measure( - &self, - _content: &str, - _size: f32, - _font: Font, - _bounds: Size, - ) -> (f32, f32) { - (0.0, 20.0) - } - - fn hit_test( - &self, - _contents: &str, - _size: f32, - _font: Self::Font, - _bounds: Size, - _point: Point, - _nearest_only: bool, - ) -> Option { - None - } - - fn fill_text(&mut self, _text: Text<'_, Self::Font>) {} -} diff --git a/native/src/runtime.rs b/native/src/runtime.rs index 5b0a6925..1b81314f 100644 --- a/native/src/runtime.rs +++ b/native/src/runtime.rs @@ -1,6 +1,6 @@ //! Run commands and subscriptions. -use crate::event::{self, Event}; -use crate::Hasher; +use iced_core::event::{self, Event}; +use iced_core::Hasher; /// A native runtime with a generic executor and receiver of results. /// diff --git a/native/src/shell.rs b/native/src/shell.rs deleted file mode 100644 index 74a5c616..00000000 --- a/native/src/shell.rs +++ /dev/null @@ -1,108 +0,0 @@ -use crate::window; - -/// A connection to the state of a shell. -/// -/// A [`Widget`] can leverage a [`Shell`] to trigger changes in an application, -/// like publishing messages or invalidating the current layout. -/// -/// [`Widget`]: crate::Widget -#[derive(Debug)] -pub struct Shell<'a, Message> { - messages: &'a mut Vec, - redraw_request: Option, - is_layout_invalid: bool, - are_widgets_invalid: bool, -} - -impl<'a, Message> Shell<'a, Message> { - /// Creates a new [`Shell`] with the provided buffer of messages. - pub fn new(messages: &'a mut Vec) -> Self { - Self { - messages, - redraw_request: None, - is_layout_invalid: false, - are_widgets_invalid: false, - } - } - - /// Returns true if the [`Shell`] contains no published messages - pub fn is_empty(&self) -> bool { - self.messages.is_empty() - } - - /// Publish the given `Message` for an application to process it. - pub fn publish(&mut self, message: Message) { - self.messages.push(message); - } - - /// Requests a new frame to be drawn at the given [`Instant`]. - pub fn request_redraw(&mut self, request: window::RedrawRequest) { - match self.redraw_request { - None => { - self.redraw_request = Some(request); - } - Some(current) if request < current => { - self.redraw_request = Some(request); - } - _ => {} - } - } - - /// Returns the requested [`Instant`] a redraw should happen, if any. - pub fn redraw_request(&self) -> Option { - self.redraw_request - } - - /// Returns whether the current layout is invalid or not. - pub fn is_layout_invalid(&self) -> bool { - self.is_layout_invalid - } - - /// Invalidates the current application layout. - /// - /// The shell will relayout the application widgets. - pub fn invalidate_layout(&mut self) { - self.is_layout_invalid = true; - } - - /// Triggers the given function if the layout is invalid, cleaning it in the - /// process. - pub fn revalidate_layout(&mut self, f: impl FnOnce()) { - if self.is_layout_invalid { - self.is_layout_invalid = false; - - f() - } - } - - /// Returns whether the widgets of the current application have been - /// invalidated. - pub fn are_widgets_invalid(&self) -> bool { - self.are_widgets_invalid - } - - /// Invalidates the current application widgets. - /// - /// The shell will rebuild and relayout the widget tree. - pub fn invalidate_widgets(&mut self) { - self.are_widgets_invalid = true; - } - - /// Merges the current [`Shell`] with another one by applying the given - /// function to the messages of the latter. - /// - /// This method is useful for composition. - pub fn merge(&mut self, other: Shell<'_, B>, f: impl Fn(B) -> Message) { - self.messages.extend(other.messages.drain(..).map(f)); - - if let Some(at) = other.redraw_request { - self.request_redraw(at); - } - - self.is_layout_invalid = - self.is_layout_invalid || other.is_layout_invalid; - - self.are_widgets_invalid = - self.are_widgets_invalid || other.are_widgets_invalid; - } -} diff --git a/native/src/subscription.rs b/native/src/subscription.rs index 16e78e82..b16bcb03 100644 --- a/native/src/subscription.rs +++ b/native/src/subscription.rs @@ -1,10 +1,9 @@ //! Listen to external events in your application. -use crate::event::{self, Event}; -use crate::window; -use crate::Hasher; - -use iced_futures::futures::{self, Future, Stream}; -use iced_futures::{BoxStream, MaybeSend}; +use crate::core::event::{self, Event}; +use crate::core::window; +use crate::core::Hasher; +use crate::futures::futures::{self, Future, Stream}; +use crate::futures::{BoxStream, MaybeSend}; use std::hash::Hash; @@ -144,7 +143,9 @@ where /// /// ``` /// use iced_native::subscription::{self, Subscription}; -/// use iced_native::futures::channel::mpsc; +/// use iced_native::futures::futures; +/// +/// use futures::channel::mpsc; /// /// pub enum Event { /// Ready(mpsc::Sender), @@ -174,7 +175,7 @@ where /// (Some(Event::Ready(sender)), State::Ready(receiver)) /// } /// State::Ready(mut receiver) => { -/// use iced_native::futures::StreamExt; +/// use futures::StreamExt; /// /// // Read next input sent from `Application` /// let input = receiver.select_next_some().await; diff --git a/native/src/svg.rs b/native/src/svg.rs deleted file mode 100644 index 9b98877a..00000000 --- a/native/src/svg.rs +++ /dev/null @@ -1,89 +0,0 @@ -//! Load and draw vector graphics. -use crate::{Color, Hasher, Rectangle, Size}; - -use std::borrow::Cow; -use std::hash::{Hash, Hasher as _}; -use std::path::PathBuf; -use std::sync::Arc; - -/// A handle of Svg data. -#[derive(Debug, Clone)] -pub struct Handle { - id: u64, - data: Arc, -} - -impl Handle { - /// Creates an SVG [`Handle`] pointing to the vector image of the given - /// path. - pub fn from_path(path: impl Into) -> Handle { - Self::from_data(Data::Path(path.into())) - } - - /// Creates an SVG [`Handle`] from raw bytes containing either an SVG string - /// or gzip compressed data. - /// - /// This is useful if you already have your SVG data in-memory, maybe - /// because you downloaded or generated it procedurally. - pub fn from_memory(bytes: impl Into>) -> Handle { - Self::from_data(Data::Bytes(bytes.into())) - } - - fn from_data(data: Data) -> Handle { - let mut hasher = Hasher::default(); - data.hash(&mut hasher); - - Handle { - id: hasher.finish(), - data: Arc::new(data), - } - } - - /// Returns the unique identifier of the [`Handle`]. - pub fn id(&self) -> u64 { - self.id - } - - /// Returns a reference to the SVG [`Data`]. - pub fn data(&self) -> &Data { - &self.data - } -} - -impl Hash for Handle { - fn hash(&self, state: &mut H) { - self.id.hash(state); - } -} - -/// The data of a vectorial image. -#[derive(Clone, Hash)] -pub enum Data { - /// File data - Path(PathBuf), - - /// In-memory data - /// - /// Can contain an SVG string or a gzip compressed data. - Bytes(Cow<'static, [u8]>), -} - -impl std::fmt::Debug for Data { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Data::Path(path) => write!(f, "Path({path:?})"), - Data::Bytes(_) => write!(f, "Bytes(...)"), - } - } -} - -/// A [`Renderer`] that can render vector graphics. -/// -/// [renderer]: crate::renderer -pub trait Renderer: crate::Renderer { - /// Returns the default dimensions of an SVG for the given [`Handle`]. - fn dimensions(&self, handle: &Handle) -> Size; - - /// Draws an SVG with the given [`Handle`], an optional [`Color`] filter, and inside the provided `bounds`. - fn draw(&mut self, handle: Handle, color: Option, bounds: Rectangle); -} diff --git a/native/src/text.rs b/native/src/text.rs deleted file mode 100644 index 4c72abc3..00000000 --- a/native/src/text.rs +++ /dev/null @@ -1,111 +0,0 @@ -//! Draw and interact with text. -use crate::alignment; -use crate::{Color, Point, Rectangle, Size}; - -use std::borrow::Cow; - -/// A paragraph. -#[derive(Debug, Clone, Copy)] -pub struct Text<'a, Font> { - /// The content of the paragraph. - pub content: &'a str, - - /// The bounds of the paragraph. - pub bounds: Rectangle, - - /// The size of the [`Text`]. - pub size: f32, - - /// The color of the [`Text`]. - pub color: Color, - - /// The font of the [`Text`]. - pub font: Font, - - /// The horizontal alignment of the [`Text`]. - pub horizontal_alignment: alignment::Horizontal, - - /// The vertical alignment of the [`Text`]. - pub vertical_alignment: alignment::Vertical, -} - -/// The result of hit testing on text. -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Hit { - /// The point was within the bounds of the returned character index. - CharOffset(usize), -} - -impl Hit { - /// Computes the cursor position of the [`Hit`] . - pub fn cursor(self) -> usize { - match self { - Self::CharOffset(i) => i, - } - } -} - -/// A renderer capable of measuring and drawing [`Text`]. -pub trait Renderer: crate::Renderer { - /// The font type used. - type Font: Copy; - - /// The icon font of the backend. - const ICON_FONT: Self::Font; - - /// The `char` representing a ✔ icon in the [`ICON_FONT`]. - /// - /// [`ICON_FONT`]: Self::ICON_FONT - const CHECKMARK_ICON: char; - - /// The `char` representing a ▼ icon in the built-in [`ICON_FONT`]. - /// - /// [`ICON_FONT`]: Self::ICON_FONT - const ARROW_DOWN_ICON: char; - - /// Returns the default [`Self::Font`]. - fn default_font(&self) -> Self::Font; - - /// Returns the default size of [`Text`]. - fn default_size(&self) -> f32; - - /// Measures the text in the given bounds and returns the minimum boundaries - /// that can fit the contents. - fn measure( - &self, - content: &str, - size: f32, - font: Self::Font, - bounds: Size, - ) -> (f32, f32); - - /// Measures the width of the text as if it were laid out in a single line. - fn measure_width(&self, content: &str, size: f32, font: Self::Font) -> f32 { - let (width, _) = self.measure(content, size, font, Size::INFINITY); - - width - } - - /// Tests whether the provided point is within the boundaries of text - /// laid out with the given parameters, returning information about - /// the nearest character. - /// - /// If `nearest_only` is true, the hit test does not consider whether the - /// the point is interior to any glyph bounds, returning only the character - /// with the nearest centeroid. - fn hit_test( - &self, - contents: &str, - size: f32, - font: Self::Font, - bounds: Size, - point: Point, - nearest_only: bool, - ) -> Option; - - /// Loads a [`Self::Font`] from its bytes. - fn load_font(&mut self, font: Cow<'static, [u8]>); - - /// Draws the given [`Text`]. - fn fill_text(&mut self, text: Text<'_, Self::Font>); -} diff --git a/native/src/touch.rs b/native/src/touch.rs deleted file mode 100644 index 18120644..00000000 --- a/native/src/touch.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Build touch events. -use crate::Point; - -/// A touch interaction. -#[derive(Debug, Clone, Copy, PartialEq)] -#[allow(missing_docs)] -pub enum Event { - /// A touch interaction was started. - FingerPressed { id: Finger, position: Point }, - - /// An on-going touch interaction was moved. - FingerMoved { id: Finger, position: Point }, - - /// A touch interaction was ended. - FingerLifted { id: Finger, position: Point }, - - /// A touch interaction was canceled. - FingerLost { id: Finger, position: Point }, -} - -/// A unique identifier representing a finger on a touch interaction. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct Finger(pub u64); diff --git a/native/src/user_interface.rs b/native/src/user_interface.rs index 68ccda55..315027fa 100644 --- a/native/src/user_interface.rs +++ b/native/src/user_interface.rs @@ -1,14 +1,12 @@ //! Implement your own event loop to drive a user interface. -use crate::application; -use crate::event::{self, Event}; -use crate::layout; -use crate::mouse; -use crate::renderer; -use crate::widget; -use crate::window; -use crate::{ - Clipboard, Element, Layout, Point, Rectangle, Shell, Size, Vector, -}; +use crate::core::event::{self, Event}; +use crate::core::layout; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::widget; +use crate::core::window; +use crate::core::{Clipboard, Point, Rectangle, Size, Vector}; +use crate::core::{Element, Layout, Shell}; /// A set of interactive graphical elements with a specific [`Layout`]. /// @@ -34,8 +32,7 @@ pub struct UserInterface<'a, Message, Renderer> { impl<'a, Message, Renderer> UserInterface<'a, Message, Renderer> where - Renderer: crate::Renderer, - Renderer::Theme: application::StyleSheet, + Renderer: iced_core::Renderer, { /// Builds a user interface for an [`Element`]. /// @@ -48,24 +45,21 @@ where /// is naive way to set up our application loop: /// /// ```no_run - /// use iced_native::Size; - /// use iced_native::user_interface::{self, UserInterface}; - /// use iced_wgpu::Renderer; - /// /// # mod iced_wgpu { - /// # pub use iced_native::renderer::Null as Renderer; + /// # pub use iced_native::core::renderer::Null as Renderer; /// # } /// # - /// # use iced_native::widget::Column; - /// # /// # pub struct Counter; /// # /// # impl Counter { /// # pub fn new() -> Self { Counter } - /// # pub fn view(&self) -> Column<(), Renderer> { - /// # Column::new() - /// # } + /// # pub fn view(&self) -> iced_core::Element<(), Renderer> { unimplemented!() } + /// # pub fn update(&mut self, _: ()) {} /// # } + /// use iced_native::core::Size; + /// use iced_native::user_interface::{self, UserInterface}; + /// use iced_wgpu::Renderer; + /// /// // Initialization /// let mut counter = Counter::new(); /// let mut cache = user_interface::Cache::new(); @@ -124,25 +118,21 @@ where /// completing [the previous example](#example): /// /// ```no_run - /// use iced_native::{clipboard, Size, Point}; - /// use iced_native::user_interface::{self, UserInterface}; - /// use iced_wgpu::Renderer; - /// /// # mod iced_wgpu { - /// # pub use iced_native::renderer::Null as Renderer; + /// # pub use iced_native::core::renderer::Null as Renderer; /// # } /// # - /// # use iced_native::widget::Column; - /// # /// # pub struct Counter; /// # /// # impl Counter { /// # pub fn new() -> Self { Counter } - /// # pub fn view(&self) -> Column<(), Renderer> { - /// # Column::new() - /// # } - /// # pub fn update(&mut self, message: ()) {} + /// # pub fn view(&self) -> iced_core::Element<(), Renderer> { unimplemented!() } + /// # pub fn update(&mut self, _: ()) {} /// # } + /// use iced_native::core::{clipboard, Size, Point}; + /// use iced_native::user_interface::{self, UserInterface}; + /// use iced_wgpu::Renderer; + /// /// let mut counter = Counter::new(); /// let mut cache = user_interface::Cache::new(); /// let mut renderer = Renderer::new(); @@ -357,27 +347,24 @@ where /// [completing the last example](#example-1): /// /// ```no_run - /// use iced_native::clipboard; - /// use iced_native::renderer; - /// use iced_native::user_interface::{self, UserInterface}; - /// use iced_native::{Size, Point, Theme}; - /// use iced_wgpu::Renderer; - /// /// # mod iced_wgpu { - /// # pub use iced_native::renderer::Null as Renderer; + /// # pub use iced_native::core::renderer::Null as Renderer; + /// # pub type Theme = (); /// # } /// # - /// # use iced_native::widget::Column; - /// # /// # pub struct Counter; /// # /// # impl Counter { /// # pub fn new() -> Self { Counter } - /// # pub fn view(&self) -> Column<(), Renderer> { - /// # Column::new() - /// # } - /// # pub fn update(&mut self, message: ()) {} + /// # pub fn view(&self) -> Element<(), Renderer> { unimplemented!() } + /// # pub fn update(&mut self, _: ()) {} /// # } + /// use iced_native::core::clipboard; + /// use iced_native::core::renderer; + /// use iced_native::core::{Element, Size, Point}; + /// use iced_native::user_interface::{self, UserInterface}; + /// use iced_wgpu::{Renderer, Theme}; + /// /// let mut counter = Counter::new(); /// let mut cache = user_interface::Cache::new(); /// let mut renderer = Renderer::new(); @@ -386,6 +373,7 @@ where /// let mut clipboard = clipboard::Null; /// let mut events = Vec::new(); /// let mut messages = Vec::new(); + /// let mut theme = Theme::default(); /// /// loop { /// // Obtain system events... @@ -407,7 +395,7 @@ where /// ); /// /// // Draw the user interface - /// let mouse_cursor = user_interface.draw(&mut renderer, &Theme::default(), &renderer::Style::default(), cursor_position); + /// let mouse_cursor = user_interface.draw(&mut renderer, &theme, &renderer::Style::default(), cursor_position); /// /// cache = user_interface.into_cache(); /// diff --git a/native/src/widget.rs b/native/src/widget.rs index 2b3ca7be..0fdade54 100644 --- a/native/src/widget.rs +++ b/native/src/widget.rs @@ -11,212 +11,6 @@ //! source of inspiration. //! //! [renderer]: crate::renderer -pub mod button; -pub mod checkbox; -pub mod column; -pub mod container; -pub mod helpers; -pub mod image; -pub mod operation; -pub mod pane_grid; -pub mod pick_list; -pub mod progress_bar; -pub mod radio; -pub mod row; -pub mod rule; -pub mod scrollable; -pub mod slider; -pub mod space; -pub mod svg; -pub mod text; -pub mod text_input; -pub mod toggler; -pub mod tooltip; -pub mod tree; -pub mod vertical_slider; - mod action; -mod id; - -#[doc(no_inline)] -pub use button::Button; -#[doc(no_inline)] -pub use checkbox::Checkbox; -#[doc(no_inline)] -pub use column::Column; -#[doc(no_inline)] -pub use container::Container; -#[doc(no_inline)] -pub use helpers::*; -#[doc(no_inline)] -pub use image::Image; -#[doc(no_inline)] -pub use pane_grid::PaneGrid; -#[doc(no_inline)] -pub use pick_list::PickList; -#[doc(no_inline)] -pub use progress_bar::ProgressBar; -#[doc(no_inline)] -pub use radio::Radio; -#[doc(no_inline)] -pub use row::Row; -#[doc(no_inline)] -pub use rule::Rule; -#[doc(no_inline)] -pub use scrollable::Scrollable; -#[doc(no_inline)] -pub use slider::Slider; -#[doc(no_inline)] -pub use space::Space; -#[doc(no_inline)] -pub use svg::Svg; -#[doc(no_inline)] -pub use text::Text; -#[doc(no_inline)] -pub use text_input::TextInput; -#[doc(no_inline)] -pub use toggler::Toggler; -#[doc(no_inline)] -pub use tooltip::Tooltip; -#[doc(no_inline)] -pub use tree::Tree; -#[doc(no_inline)] -pub use vertical_slider::VerticalSlider; pub use action::Action; -pub use id::Id; -pub use operation::Operation; - -use crate::event::{self, Event}; -use crate::layout; -use crate::mouse; -use crate::overlay; -use crate::renderer; -use crate::{Clipboard, Layout, Length, Point, Rectangle, Shell}; - -/// A component that displays information and allows interaction. -/// -/// If you want to build your own widgets, you will need to implement this -/// trait. -/// -/// # Examples -/// The repository has some [examples] showcasing how to implement a custom -/// widget: -/// -/// - [`bezier_tool`], a Paint-like tool for drawing Bézier curves using -/// [`lyon`]. -/// - [`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`]. -/// -/// [examples]: https://github.com/iced-rs/iced/tree/0.8/examples -/// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.8/examples/bezier_tool -/// [`custom_widget`]: https://github.com/iced-rs/iced/tree/0.8/examples/custom_widget -/// [`geometry`]: https://github.com/iced-rs/iced/tree/0.8/examples/geometry -/// [`lyon`]: https://github.com/nical/lyon -/// [`iced_wgpu`]: https://github.com/iced-rs/iced/tree/0.8/wgpu -pub trait Widget -where - Renderer: crate::Renderer, -{ - /// Returns the width of the [`Widget`]. - fn width(&self) -> Length; - - /// Returns the height of the [`Widget`]. - fn height(&self) -> Length; - - /// Returns the [`layout::Node`] of the [`Widget`]. - /// - /// This [`layout::Node`] is used by the runtime to compute the [`Layout`] of the - /// user interface. - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node; - - /// Draws the [`Widget`] using the associated `Renderer`. - fn draw( - &self, - state: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ); - - /// Returns the [`Tag`] of the [`Widget`]. - /// - /// [`Tag`]: tree::Tag - fn tag(&self) -> tree::Tag { - tree::Tag::stateless() - } - - /// Returns the [`State`] of the [`Widget`]. - /// - /// [`State`]: tree::State - fn state(&self) -> tree::State { - tree::State::None - } - - /// Returns the state [`Tree`] of the children of the [`Widget`]. - fn children(&self) -> Vec { - Vec::new() - } - - /// Reconciliates the [`Widget`] with the provided [`Tree`]. - fn diff(&self, _tree: &mut Tree) {} - - /// Applies an [`Operation`] to the [`Widget`]. - fn operate( - &self, - _state: &mut Tree, - _layout: Layout<'_>, - _renderer: &Renderer, - _operation: &mut dyn Operation, - ) { - } - - /// Processes a runtime [`Event`]. - /// - /// By default, it does nothing. - fn on_event( - &mut self, - _state: &mut Tree, - _event: Event, - _layout: Layout<'_>, - _cursor_position: Point, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - _shell: &mut Shell<'_, Message>, - ) -> event::Status { - event::Status::Ignored - } - - /// Returns the current [`mouse::Interaction`] of the [`Widget`]. - /// - /// By default, it returns [`mouse::Interaction::Idle`]. - fn mouse_interaction( - &self, - _state: &Tree, - _layout: Layout<'_>, - _cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - mouse::Interaction::Idle - } - - /// Returns the overlay of the [`Widget`], if there is any. - fn overlay<'a>( - &'a mut self, - _state: &'a mut Tree, - _layout: Layout<'_>, - _renderer: &Renderer, - ) -> Option> { - None - } -} diff --git a/native/src/widget/action.rs b/native/src/widget/action.rs index 3f1b6b6c..f50d7aec 100644 --- a/native/src/widget/action.rs +++ b/native/src/widget/action.rs @@ -1,8 +1,7 @@ -use crate::widget::operation::{ +use iced_core::widget::operation::{ self, Focusable, Operation, Scrollable, TextInput, }; -use crate::widget::Id; - +use iced_core::widget::Id; use iced_futures::MaybeSend; use std::any::Any; diff --git a/native/src/widget/button.rs b/native/src/widget/button.rs deleted file mode 100644 index 39387173..00000000 --- a/native/src/widget/button.rs +++ /dev/null @@ -1,455 +0,0 @@ -//! Allow your users to perform actions by pressing a button. -//! -//! A [`Button`] has some local [`State`]. -use crate::event::{self, Event}; -use crate::layout; -use crate::mouse; -use crate::overlay; -use crate::renderer; -use crate::touch; -use crate::widget::tree::{self, Tree}; -use crate::widget::Operation; -use crate::{ - Background, Clipboard, Color, Element, Layout, Length, Padding, Point, - Rectangle, Shell, Vector, Widget, -}; - -pub use iced_style::button::{Appearance, StyleSheet}; - -/// A generic widget that produces a message when pressed. -/// -/// ``` -/// # type Button<'a, Message> = -/// # iced_native::widget::Button<'a, Message, iced_native::renderer::Null>; -/// # -/// #[derive(Clone)] -/// enum Message { -/// ButtonPressed, -/// } -/// -/// let button = Button::new("Press me!").on_press(Message::ButtonPressed); -/// ``` -/// -/// If a [`Button::on_press`] handler is not set, the resulting [`Button`] will -/// be disabled: -/// -/// ``` -/// # type Button<'a, Message> = -/// # iced_native::widget::Button<'a, Message, iced_native::renderer::Null>; -/// # -/// #[derive(Clone)] -/// enum Message { -/// ButtonPressed, -/// } -/// -/// fn disabled_button<'a>() -> Button<'a, Message> { -/// Button::new("I'm disabled!") -/// } -/// -/// fn enabled_button<'a>() -> Button<'a, Message> { -/// disabled_button().on_press(Message::ButtonPressed) -/// } -/// ``` -#[allow(missing_debug_implementations)] -pub struct Button<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - content: Element<'a, Message, Renderer>, - on_press: Option, - width: Length, - height: Length, - padding: Padding, - style: ::Style, -} - -impl<'a, Message, Renderer> Button<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - /// Creates a new [`Button`] with the given content. - pub fn new(content: impl Into>) -> Self { - Button { - content: content.into(), - on_press: None, - width: Length::Shrink, - height: Length::Shrink, - padding: Padding::new(5.0), - style: ::Style::default(), - } - } - - /// Sets the width of the [`Button`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the height of the [`Button`]. - pub fn height(mut self, height: impl Into) -> Self { - self.height = height.into(); - self - } - - /// Sets the [`Padding`] of the [`Button`]. - pub fn padding>(mut self, padding: P) -> Self { - self.padding = padding.into(); - self - } - - /// Sets the message that will be produced when the [`Button`] is pressed. - /// - /// Unless `on_press` is called, the [`Button`] will be disabled. - pub fn on_press(mut self, msg: Message) -> Self { - self.on_press = Some(msg); - self - } - - /// Sets the style variant of this [`Button`]. - pub fn style( - mut self, - style: ::Style, - ) -> Self { - self.style = style; - self - } -} - -impl<'a, Message, Renderer> Widget - for Button<'a, Message, Renderer> -where - Message: 'a + Clone, - Renderer: 'a + crate::Renderer, - Renderer::Theme: StyleSheet, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(State::new()) - } - - 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.width - } - - fn height(&self) -> Length { - self.height - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - layout( - renderer, - limits, - self.width, - self.height, - self.padding, - |renderer, limits| { - self.content.as_widget().layout(renderer, limits) - }, - ) - } - - fn operate( - &self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn Operation, - ) { - operation.container(None, &mut |operation| { - self.content.as_widget().operate( - &mut tree.children[0], - layout.children().next().unwrap(), - renderer, - operation, - ); - }); - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: 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( - event, - layout, - cursor_position, - shell, - &self.on_press, - || tree.state.downcast_mut::(), - ) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - let bounds = layout.bounds(); - let content_layout = layout.children().next().unwrap(); - - let styling = draw( - renderer, - bounds, - cursor_position, - self.on_press.is_some(), - theme, - &self.style, - || tree.state.downcast_ref::(), - ); - - self.content.as_widget().draw( - &tree.children[0], - renderer, - theme, - &renderer::Style { - text_color: styling.text_color, - }, - content_layout, - cursor_position, - &bounds, - ); - } - - fn mouse_interaction( - &self, - _tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - mouse_interaction(layout, cursor_position, self.on_press.is_some()) - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - self.content.as_widget_mut().overlay( - &mut tree.children[0], - layout.children().next().unwrap(), - renderer, - ) - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Message: Clone + 'a, - Renderer: crate::Renderer + 'a, - Renderer::Theme: StyleSheet, -{ - fn from(button: Button<'a, Message, Renderer>) -> Self { - Self::new(button) - } -} - -/// The local state of a [`Button`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub struct State { - is_pressed: bool, -} - -impl State { - /// Creates a new [`State`]. - pub fn new() -> State { - State::default() - } -} - -/// Processes the given [`Event`] and updates the [`State`] of a [`Button`] -/// accordingly. -pub fn update<'a, Message: Clone>( - event: Event, - layout: Layout<'_>, - cursor_position: Point, - shell: &mut Shell<'_, Message>, - on_press: &Option, - state: impl FnOnce() -> &'a mut State, -) -> event::Status { - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - if on_press.is_some() { - let bounds = layout.bounds(); - - if bounds.contains(cursor_position) { - let state = state(); - - state.is_pressed = true; - - return event::Status::Captured; - } - } - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) => { - if let Some(on_press) = on_press.clone() { - let state = state(); - - if state.is_pressed { - state.is_pressed = false; - - let bounds = layout.bounds(); - - if bounds.contains(cursor_position) { - shell.publish(on_press); - } - - return event::Status::Captured; - } - } - } - Event::Touch(touch::Event::FingerLost { .. }) => { - let state = state(); - - state.is_pressed = false; - } - _ => {} - } - - event::Status::Ignored -} - -/// Draws a [`Button`]. -pub fn draw<'a, Renderer: crate::Renderer>( - renderer: &mut Renderer, - bounds: Rectangle, - cursor_position: Point, - is_enabled: bool, - style_sheet: &dyn StyleSheet< - Style = ::Style, - >, - style: &::Style, - state: impl FnOnce() -> &'a State, -) -> Appearance -where - Renderer::Theme: StyleSheet, -{ - let is_mouse_over = bounds.contains(cursor_position); - - 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 { - style_sheet.active(style) - }; - - if styling.background.is_some() || styling.border_width > 0.0 { - if styling.shadow_offset != Vector::default() { - // TODO: Implement proper shadow support - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x + styling.shadow_offset.x, - y: bounds.y + styling.shadow_offset.y, - ..bounds - }, - border_radius: styling.border_radius.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - Background::Color([0.0, 0.0, 0.0, 0.5].into()), - ); - } - - renderer.fill_quad( - renderer::Quad { - bounds, - border_radius: styling.border_radius.into(), - border_width: styling.border_width, - border_color: styling.border_color, - }, - styling - .background - .unwrap_or(Background::Color(Color::TRANSPARENT)), - ); - } - - styling -} - -/// Computes the layout of a [`Button`]. -pub fn layout( - renderer: &Renderer, - limits: &layout::Limits, - width: Length, - height: Length, - padding: Padding, - layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, -) -> layout::Node { - let limits = limits.width(width).height(height); - - let mut content = layout_content(renderer, &limits.pad(padding)); - let padding = padding.fit(content.size(), limits.max()); - let size = limits.pad(padding).resolve(content.size()).pad(padding); - - content.move_to(Point::new(padding.left, padding.top)); - - layout::Node::with_children(size, vec![content]) -} - -/// Returns the [`mouse::Interaction`] of a [`Button`]. -pub fn mouse_interaction( - layout: Layout<'_>, - cursor_position: Point, - is_enabled: bool, -) -> mouse::Interaction { - let is_mouse_over = layout.bounds().contains(cursor_position); - - if is_mouse_over && is_enabled { - mouse::Interaction::Pointer - } else { - mouse::Interaction::default() - } -} diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs deleted file mode 100644 index cd8b9c6b..00000000 --- a/native/src/widget/checkbox.rs +++ /dev/null @@ -1,321 +0,0 @@ -//! Show toggle controls using checkboxes. -use crate::alignment; -use crate::event::{self, Event}; -use crate::layout; -use crate::mouse; -use crate::renderer; -use crate::text; -use crate::touch; -use crate::widget::{self, Row, Text, Tree}; -use crate::{ - Alignment, Clipboard, Element, Layout, Length, Pixels, Point, Rectangle, - Shell, Widget, -}; - -pub use iced_style::checkbox::{Appearance, StyleSheet}; - -/// The icon in a [`Checkbox`]. -#[derive(Debug, Clone, PartialEq)] -pub struct Icon { - /// Font that will be used to display the `code_point`, - pub font: Font, - /// The unicode code point that will be used as the icon. - pub code_point: char, - /// Font size of the content. - pub size: Option, -} - -/// A box that can be checked. -/// -/// # Example -/// -/// ``` -/// # type Checkbox<'a, Message> = iced_native::widget::Checkbox<'a, Message, iced_native::renderer::Null>; -/// # -/// pub enum Message { -/// CheckboxToggled(bool), -/// } -/// -/// let is_checked = true; -/// -/// Checkbox::new("Toggle me!", is_checked, Message::CheckboxToggled); -/// ``` -/// -/// ![Checkbox drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/checkbox.png?raw=true) -#[allow(missing_debug_implementations)] -pub struct Checkbox<'a, Message, Renderer> -where - Renderer: text::Renderer, - Renderer::Theme: StyleSheet + widget::text::StyleSheet, -{ - is_checked: bool, - on_toggle: Box Message + 'a>, - label: String, - width: Length, - size: f32, - spacing: f32, - text_size: Option, - font: Option, - icon: Icon, - style: ::Style, -} - -impl<'a, Message, Renderer> Checkbox<'a, Message, Renderer> -where - Renderer: text::Renderer, - Renderer::Theme: StyleSheet + widget::text::StyleSheet, -{ - /// The default size of a [`Checkbox`]. - const DEFAULT_SIZE: f32 = 20.0; - - /// The default spacing of a [`Checkbox`]. - const DEFAULT_SPACING: f32 = 15.0; - - /// Creates a new [`Checkbox`]. - /// - /// It expects: - /// * a boolean describing whether the [`Checkbox`] is checked or not - /// * the label of the [`Checkbox`] - /// * a function that will be called when the [`Checkbox`] is toggled. It - /// will receive the new state of the [`Checkbox`] and must produce a - /// `Message`. - pub fn new(label: impl Into, is_checked: bool, f: F) -> Self - where - F: 'a + Fn(bool) -> Message, - { - Checkbox { - is_checked, - on_toggle: Box::new(f), - label: label.into(), - width: Length::Shrink, - size: Self::DEFAULT_SIZE, - spacing: Self::DEFAULT_SPACING, - text_size: None, - font: None, - icon: Icon { - font: Renderer::ICON_FONT, - code_point: Renderer::CHECKMARK_ICON, - size: None, - }, - style: Default::default(), - } - } - - /// Sets the size of the [`Checkbox`]. - pub fn size(mut self, size: impl Into) -> Self { - self.size = size.into().0; - self - } - - /// Sets the width of the [`Checkbox`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the spacing between the [`Checkbox`] and the text. - pub fn spacing(mut self, spacing: impl Into) -> Self { - self.spacing = spacing.into().0; - self - } - - /// Sets the text size of the [`Checkbox`]. - pub fn text_size(mut self, text_size: impl Into) -> Self { - self.text_size = Some(text_size.into().0); - self - } - - /// Sets the [`Font`] of the text of the [`Checkbox`]. - /// - /// [`Font`]: crate::text::Renderer::Font - pub fn font(mut self, font: impl Into) -> Self { - self.font = Some(font.into()); - self - } - - /// Sets the [`Icon`] of the [`Checkbox`]. - pub fn icon(mut self, icon: Icon) -> Self { - self.icon = icon; - self - } - - /// Sets the style of the [`Checkbox`]. - pub fn style( - mut self, - style: impl Into<::Style>, - ) -> Self { - self.style = style.into(); - self - } -} - -impl<'a, Message, Renderer> Widget - for Checkbox<'a, Message, Renderer> -where - Renderer: text::Renderer, - Renderer::Theme: StyleSheet + widget::text::StyleSheet, -{ - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - Length::Shrink - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - Row::<(), Renderer>::new() - .width(self.width) - .spacing(self.spacing) - .align_items(Alignment::Center) - .push(Row::new().width(self.size).height(self.size)) - .push( - Text::new(&self.label) - .font(self.font.unwrap_or_else(|| renderer.default_font())) - .width(self.width) - .size( - self.text_size - .unwrap_or_else(|| renderer.default_size()), - ), - ) - .layout(renderer, limits) - } - - 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 { - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let mouse_over = layout.bounds().contains(cursor_position); - - if mouse_over { - shell.publish((self.on_toggle)(!self.is_checked)); - - return event::Status::Captured; - } - } - _ => {} - } - - event::Status::Ignored - } - - fn mouse_interaction( - &self, - _tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - if layout.bounds().contains(cursor_position) { - mouse::Interaction::Pointer - } else { - mouse::Interaction::default() - } - } - - fn draw( - &self, - _tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); - - let mut children = layout.children(); - - let custom_style = if is_mouse_over { - theme.hovered(&self.style, self.is_checked) - } else { - theme.active(&self.style, self.is_checked) - }; - - { - let layout = children.next().unwrap(); - let bounds = layout.bounds(); - - renderer.fill_quad( - renderer::Quad { - bounds, - border_radius: custom_style.border_radius.into(), - border_width: custom_style.border_width, - border_color: custom_style.border_color, - }, - custom_style.background, - ); - - let Icon { - font, - code_point, - size, - } = &self.icon; - let size = size.unwrap_or(bounds.height * 0.7); - - if self.is_checked { - renderer.fill_text(text::Text { - content: &code_point.to_string(), - font: *font, - size, - bounds: Rectangle { - x: bounds.center_x(), - y: bounds.center_y(), - ..bounds - }, - color: custom_style.icon_color, - horizontal_alignment: alignment::Horizontal::Center, - vertical_alignment: alignment::Vertical::Center, - }); - } - } - - { - let label_layout = children.next().unwrap(); - - widget::text::draw( - renderer, - style, - label_layout, - &self.label, - self.text_size, - self.font, - widget::text::Appearance { - color: custom_style.text_color, - }, - alignment::Horizontal::Left, - alignment::Vertical::Center, - ); - } - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Message: 'a, - Renderer: 'a + text::Renderer, - Renderer::Theme: StyleSheet + widget::text::StyleSheet, -{ - fn from( - checkbox: Checkbox<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(checkbox) - } -} diff --git a/native/src/widget/column.rs b/native/src/widget/column.rs deleted file mode 100644 index ebe579d5..00000000 --- a/native/src/widget/column.rs +++ /dev/null @@ -1,264 +0,0 @@ -//! Distribute content vertically. -use crate::event::{self, Event}; -use crate::layout; -use crate::mouse; -use crate::overlay; -use crate::renderer; -use crate::widget::{Operation, Tree}; -use crate::{ - Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Point, - Rectangle, Shell, Widget, -}; - -/// A container that distributes its contents vertically. -#[allow(missing_debug_implementations)] -pub struct Column<'a, Message, Renderer> { - spacing: f32, - padding: Padding, - width: Length, - height: Length, - max_width: f32, - align_items: Alignment, - children: Vec>, -} - -impl<'a, Message, Renderer> Column<'a, Message, Renderer> { - /// Creates an empty [`Column`]. - pub fn new() -> Self { - Self::with_children(Vec::new()) - } - - /// Creates a [`Column`] with the given elements. - pub fn with_children( - children: Vec>, - ) -> Self { - Column { - spacing: 0.0, - padding: Padding::ZERO, - width: Length::Shrink, - height: Length::Shrink, - max_width: f32::INFINITY, - align_items: Alignment::Start, - children, - } - } - - /// Sets the vertical spacing _between_ elements. - /// - /// Custom margins per element do not exist in iced. You should use this - /// method instead! While less flexible, it helps you keep spacing between - /// elements consistent. - pub fn spacing(mut self, amount: impl Into) -> Self { - self.spacing = amount.into().0; - self - } - - /// Sets the [`Padding`] of the [`Column`]. - pub fn padding>(mut self, padding: P) -> Self { - self.padding = padding.into(); - self - } - - /// Sets the width of the [`Column`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the height of the [`Column`]. - pub fn height(mut self, height: impl Into) -> Self { - self.height = height.into(); - self - } - - /// Sets the maximum width of the [`Column`]. - pub fn max_width(mut self, max_width: impl Into) -> Self { - self.max_width = max_width.into().0; - self - } - - /// Sets the horizontal alignment of the contents of the [`Column`] . - pub fn align_items(mut self, align: Alignment) -> Self { - self.align_items = align; - self - } - - /// Adds an element to the [`Column`]. - pub fn push( - mut self, - child: impl Into>, - ) -> Self { - self.children.push(child.into()); - self - } -} - -impl<'a, Message, Renderer> Default for Column<'a, Message, Renderer> { - fn default() -> Self { - Self::new() - } -} - -impl<'a, Message, Renderer> Widget - for Column<'a, Message, Renderer> -where - Renderer: crate::Renderer, -{ - fn children(&self) -> Vec { - self.children.iter().map(Tree::new).collect() - } - - fn diff(&self, tree: &mut Tree) { - tree.diff_children(&self.children); - } - - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let limits = limits - .max_width(self.max_width) - .width(self.width) - .height(self.height); - - layout::flex::resolve( - layout::flex::Axis::Vertical, - renderer, - &limits, - self.padding, - self.spacing, - self.align_items, - &self.children, - ) - } - - fn operate( - &self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn Operation, - ) { - operation.container(None, &mut |operation| { - self.children - .iter() - .zip(&mut tree.children) - .zip(layout.children()) - .for_each(|((child, state), layout)| { - child - .as_widget() - .operate(state, layout, renderer, 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 { - self.children - .iter_mut() - .zip(&mut tree.children) - .zip(layout.children()) - .map(|((child, state), layout)| { - child.as_widget_mut().on_event( - state, - event.clone(), - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - }) - .fold(event::Status::Ignored, event::Status::merge) - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.children - .iter() - .zip(&tree.children) - .zip(layout.children()) - .map(|((child, state), layout)| { - child.as_widget().mouse_interaction( - state, - layout, - cursor_position, - viewport, - renderer, - ) - }) - .max() - .unwrap_or_default() - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - for ((child, state), layout) in self - .children - .iter() - .zip(&tree.children) - .zip(layout.children()) - { - child.as_widget().draw( - state, - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ); - } - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - overlay::from_children(&mut self.children, tree, layout, renderer) - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Message: 'a, - Renderer: crate::Renderer + 'a, -{ - fn from(column: Column<'a, Message, Renderer>) -> Self { - Self::new(column) - } -} diff --git a/native/src/widget/container.rs b/native/src/widget/container.rs deleted file mode 100644 index b77bf50d..00000000 --- a/native/src/widget/container.rs +++ /dev/null @@ -1,368 +0,0 @@ -//! Decorate content and apply alignment. -use crate::alignment::{self, Alignment}; -use crate::event::{self, Event}; -use crate::layout; -use crate::mouse; -use crate::overlay; -use crate::renderer; -use crate::widget::{self, Operation, Tree}; -use crate::{ - Background, Clipboard, Color, Element, Layout, Length, Padding, Pixels, - Point, Rectangle, Shell, Widget, -}; - -pub use iced_style::container::{Appearance, StyleSheet}; - -/// An element decorating some content. -/// -/// It is normally used for alignment purposes. -#[allow(missing_debug_implementations)] -pub struct Container<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - id: Option, - padding: Padding, - width: Length, - height: Length, - max_width: f32, - max_height: f32, - horizontal_alignment: alignment::Horizontal, - vertical_alignment: alignment::Vertical, - style: ::Style, - content: Element<'a, Message, Renderer>, -} - -impl<'a, Message, Renderer> Container<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - /// Creates an empty [`Container`]. - pub fn new(content: T) -> Self - where - T: Into>, - { - Container { - id: None, - padding: Padding::ZERO, - width: Length::Shrink, - height: Length::Shrink, - max_width: f32::INFINITY, - max_height: f32::INFINITY, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, - style: Default::default(), - content: content.into(), - } - } - - /// Sets the [`Id`] of the [`Container`]. - pub fn id(mut self, id: Id) -> Self { - self.id = Some(id); - self - } - - /// Sets the [`Padding`] of the [`Container`]. - pub fn padding>(mut self, padding: P) -> Self { - self.padding = padding.into(); - self - } - - /// Sets the width of the [`Container`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the height of the [`Container`]. - pub fn height(mut self, height: impl Into) -> Self { - self.height = height.into(); - self - } - - /// Sets the maximum width of the [`Container`]. - pub fn max_width(mut self, max_width: impl Into) -> Self { - self.max_width = max_width.into().0; - self - } - - /// Sets the maximum height of the [`Container`]. - pub fn max_height(mut self, max_height: impl Into) -> Self { - self.max_height = max_height.into().0; - self - } - - /// Sets the content alignment for the horizontal axis of the [`Container`]. - pub fn align_x(mut self, alignment: alignment::Horizontal) -> Self { - self.horizontal_alignment = alignment; - self - } - - /// Sets the content alignment for the vertical axis of the [`Container`]. - pub fn align_y(mut self, alignment: alignment::Vertical) -> Self { - self.vertical_alignment = alignment; - self - } - - /// Centers the contents in the horizontal axis of the [`Container`]. - pub fn center_x(mut self) -> Self { - self.horizontal_alignment = alignment::Horizontal::Center; - self - } - - /// Centers the contents in the vertical axis of the [`Container`]. - pub fn center_y(mut self) -> Self { - self.vertical_alignment = alignment::Vertical::Center; - self - } - - /// Sets the style of the [`Container`]. - pub fn style( - mut self, - style: impl Into<::Style>, - ) -> Self { - self.style = style.into(); - self - } -} - -impl<'a, Message, Renderer> Widget - for Container<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - 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.width - } - - fn height(&self) -> Length { - self.height - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - layout( - renderer, - limits, - self.width, - self.height, - self.max_width, - self.max_height, - self.padding, - self.horizontal_alignment, - self.vertical_alignment, - |renderer, limits| { - self.content.as_widget().layout(renderer, limits) - }, - ) - } - - fn operate( - &self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn Operation, - ) { - operation.container( - self.id.as_ref().map(|id| &id.0), - &mut |operation| { - self.content.as_widget().operate( - &mut tree.children[0], - layout.children().next().unwrap(), - renderer, - operation, - ); - }, - ); - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.content.as_widget_mut().on_event( - &mut tree.children[0], - event, - layout.children().next().unwrap(), - cursor_position, - renderer, - clipboard, - shell, - ) - } - - 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, - ) { - let style = theme.appearance(&self.style); - - draw_background(renderer, &style, layout.bounds()); - - self.content.as_widget().draw( - &tree.children[0], - renderer, - theme, - &renderer::Style { - text_color: style - .text_color - .unwrap_or(renderer_style.text_color), - }, - layout.children().next().unwrap(), - cursor_position, - viewport, - ); - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - self.content.as_widget_mut().overlay( - &mut tree.children[0], - layout.children().next().unwrap(), - renderer, - ) - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Message: 'a, - Renderer: 'a + crate::Renderer, - Renderer::Theme: StyleSheet, -{ - fn from( - column: Container<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(column) - } -} - -/// Computes the layout of a [`Container`]. -pub fn layout( - renderer: &Renderer, - limits: &layout::Limits, - width: Length, - height: Length, - max_width: f32, - max_height: f32, - padding: Padding, - horizontal_alignment: alignment::Horizontal, - vertical_alignment: alignment::Vertical, - layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, -) -> layout::Node { - let limits = limits - .loose() - .max_width(max_width) - .max_height(max_height) - .width(width) - .height(height); - - let mut content = layout_content(renderer, &limits.pad(padding).loose()); - let padding = padding.fit(content.size(), limits.max()); - let size = limits.pad(padding).resolve(content.size()); - - content.move_to(Point::new(padding.left, padding.top)); - content.align( - Alignment::from(horizontal_alignment), - Alignment::from(vertical_alignment), - size, - ); - - layout::Node::with_children(size.pad(padding), vec![content]) -} - -/// Draws the background of a [`Container`] given its [`Appearance`] and its `bounds`. -pub fn draw_background( - renderer: &mut Renderer, - appearance: &Appearance, - bounds: Rectangle, -) where - Renderer: crate::Renderer, -{ - if appearance.background.is_some() || appearance.border_width > 0.0 { - renderer.fill_quad( - renderer::Quad { - bounds, - border_radius: appearance.border_radius.into(), - border_width: appearance.border_width, - border_color: appearance.border_color, - }, - appearance - .background - .unwrap_or(Background::Color(Color::TRANSPARENT)), - ); - } -} - -/// The identifier of a [`Container`]. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Id(widget::Id); - -impl Id { - /// Creates a custom [`Id`]. - pub fn new(id: impl Into>) -> Self { - Self(widget::Id::new(id)) - } - - /// Creates a unique [`Id`]. - /// - /// This function produces a different [`Id`] every time it is called. - pub fn unique() -> Self { - Self(widget::Id::unique()) - } -} - -impl From for widget::Id { - fn from(id: Id) -> Self { - id.0 - } -} diff --git a/native/src/widget/helpers.rs b/native/src/widget/helpers.rs deleted file mode 100644 index d13eca75..00000000 --- a/native/src/widget/helpers.rs +++ /dev/null @@ -1,317 +0,0 @@ -//! Helper functions to create pure widgets. -use crate::overlay; -use crate::widget; -use crate::{Element, Length, Pixels}; - -use std::borrow::Cow; -use std::ops::RangeInclusive; - -/// Creates a [`Column`] with the given children. -/// -/// [`Column`]: widget::Column -#[macro_export] -macro_rules! column { - () => ( - $crate::widget::Column::new() - ); - ($($x:expr),+ $(,)?) => ( - $crate::widget::Column::with_children(vec![$($crate::Element::from($x)),+]) - ); -} - -/// Creates a [`Row`] with the given children. -/// -/// [`Row`]: widget::Row -#[macro_export] -macro_rules! row { - () => ( - $crate::widget::Row::new() - ); - ($($x:expr),+ $(,)?) => ( - $crate::widget::Row::with_children(vec![$($crate::Element::from($x)),+]) - ); -} - -/// Creates a new [`Container`] with the provided content. -/// -/// [`Container`]: widget::Container -pub fn container<'a, Message, Renderer>( - content: impl Into>, -) -> widget::Container<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: widget::container::StyleSheet, -{ - widget::Container::new(content) -} - -/// Creates a new [`Column`] with the given children. -/// -/// [`Column`]: widget::Column -pub fn column( - children: Vec>, -) -> widget::Column<'_, Message, Renderer> { - widget::Column::with_children(children) -} - -/// Creates a new [`Row`] with the given children. -/// -/// [`Row`]: widget::Row -pub fn row( - children: Vec>, -) -> widget::Row<'_, Message, Renderer> { - widget::Row::with_children(children) -} - -/// Creates a new [`Scrollable`] with the provided content. -/// -/// [`Scrollable`]: widget::Scrollable -pub fn scrollable<'a, Message, Renderer>( - content: impl Into>, -) -> widget::Scrollable<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: widget::scrollable::StyleSheet, -{ - widget::Scrollable::new(content) -} - -/// Creates a new [`Button`] with the provided content. -/// -/// [`Button`]: widget::Button -pub fn button<'a, Message, Renderer>( - content: impl Into>, -) -> widget::Button<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: widget::button::StyleSheet, - ::Style: Default, -{ - widget::Button::new(content) -} - -/// Creates a new [`Tooltip`] with the provided content, tooltip text, and [`tooltip::Position`]. -/// -/// [`Tooltip`]: widget::Tooltip -/// [`tooltip::Position`]: widget::tooltip::Position -pub fn tooltip<'a, Message, Renderer>( - content: impl Into>, - tooltip: impl ToString, - position: widget::tooltip::Position, -) -> widget::Tooltip<'a, Message, Renderer> -where - Renderer: crate::text::Renderer, - Renderer::Theme: widget::container::StyleSheet + widget::text::StyleSheet, -{ - widget::Tooltip::new(content, tooltip.to_string(), position) -} - -/// Creates a new [`Text`] widget with the provided content. -/// -/// [`Text`]: widget::Text -pub fn text<'a, Renderer>(text: impl ToString) -> widget::Text<'a, Renderer> -where - Renderer: crate::text::Renderer, - Renderer::Theme: widget::text::StyleSheet, -{ - widget::Text::new(text.to_string()) -} - -/// Creates a new [`Checkbox`]. -/// -/// [`Checkbox`]: widget::Checkbox -pub fn checkbox<'a, Message, Renderer>( - label: impl Into, - is_checked: bool, - f: impl Fn(bool) -> Message + 'a, -) -> widget::Checkbox<'a, Message, Renderer> -where - Renderer: crate::text::Renderer, - Renderer::Theme: widget::checkbox::StyleSheet + widget::text::StyleSheet, -{ - widget::Checkbox::new(label, is_checked, f) -} - -/// Creates a new [`Radio`]. -/// -/// [`Radio`]: widget::Radio -pub fn radio( - label: impl Into, - value: V, - selected: Option, - on_click: impl FnOnce(V) -> Message, -) -> widget::Radio -where - Message: Clone, - Renderer: crate::text::Renderer, - Renderer::Theme: widget::radio::StyleSheet, - V: Copy + Eq, -{ - widget::Radio::new(value, label, selected, on_click) -} - -/// Creates a new [`Toggler`]. -/// -/// [`Toggler`]: widget::Toggler -pub fn toggler<'a, Message, Renderer>( - label: impl Into>, - is_checked: bool, - f: impl Fn(bool) -> Message + 'a, -) -> widget::Toggler<'a, Message, Renderer> -where - Renderer: crate::text::Renderer, - Renderer::Theme: widget::toggler::StyleSheet, -{ - widget::Toggler::new(label, is_checked, f) -} - -/// Creates a new [`TextInput`]. -/// -/// [`TextInput`]: widget::TextInput -pub fn text_input<'a, Message, Renderer>( - placeholder: &str, - value: &str, - on_change: impl Fn(String) -> Message + 'a, -) -> widget::TextInput<'a, Message, Renderer> -where - Message: Clone, - Renderer: crate::text::Renderer, - Renderer::Theme: widget::text_input::StyleSheet, -{ - widget::TextInput::new(placeholder, value, on_change) -} - -/// Creates a new [`Slider`]. -/// -/// [`Slider`]: widget::Slider -pub fn slider<'a, T, Message, Renderer>( - range: std::ops::RangeInclusive, - value: T, - on_change: impl Fn(T) -> Message + 'a, -) -> widget::Slider<'a, T, Message, Renderer> -where - T: Copy + From + std::cmp::PartialOrd, - Message: Clone, - Renderer: crate::Renderer, - Renderer::Theme: widget::slider::StyleSheet, -{ - widget::Slider::new(range, value, on_change) -} - -/// Creates a new [`VerticalSlider`]. -/// -/// [`VerticalSlider`]: widget::VerticalSlider -pub fn vertical_slider<'a, T, Message, Renderer>( - range: std::ops::RangeInclusive, - value: T, - on_change: impl Fn(T) -> Message + 'a, -) -> widget::VerticalSlider<'a, T, Message, Renderer> -where - T: Copy + From + std::cmp::PartialOrd, - Message: Clone, - Renderer: crate::Renderer, - Renderer::Theme: widget::slider::StyleSheet, -{ - widget::VerticalSlider::new(range, value, on_change) -} - -/// Creates a new [`PickList`]. -/// -/// [`PickList`]: widget::PickList -pub fn pick_list<'a, Message, Renderer, T>( - options: impl Into>, - selected: Option, - on_selected: impl Fn(T) -> Message + 'a, -) -> widget::PickList<'a, T, Message, Renderer> -where - T: ToString + Eq + 'static, - [T]: ToOwned>, - Renderer: crate::text::Renderer, - Renderer::Theme: widget::pick_list::StyleSheet - + widget::scrollable::StyleSheet - + overlay::menu::StyleSheet - + widget::container::StyleSheet, - ::Style: - From<::Style>, -{ - widget::PickList::new(options, selected, on_selected) -} - -/// Creates a new [`Image`]. -/// -/// [`Image`]: widget::Image -pub fn image(handle: impl Into) -> widget::Image { - widget::Image::new(handle.into()) -} - -/// Creates a new horizontal [`Space`] with the given [`Length`]. -/// -/// [`Space`]: widget::Space -pub fn horizontal_space(width: impl Into) -> widget::Space { - widget::Space::with_width(width) -} - -/// Creates a new vertical [`Space`] with the given [`Length`]. -/// -/// [`Space`]: widget::Space -pub fn vertical_space(height: impl Into) -> widget::Space { - widget::Space::with_height(height) -} - -/// Creates a horizontal [`Rule`] with the given height. -/// -/// [`Rule`]: widget::Rule -pub fn horizontal_rule( - height: impl Into, -) -> widget::Rule -where - Renderer: crate::Renderer, - Renderer::Theme: widget::rule::StyleSheet, -{ - widget::Rule::horizontal(height) -} - -/// Creates a vertical [`Rule`] with the given width. -/// -/// [`Rule`]: widget::Rule -pub fn vertical_rule( - width: impl Into, -) -> widget::Rule -where - Renderer: crate::Renderer, - Renderer::Theme: widget::rule::StyleSheet, -{ - widget::Rule::vertical(width) -} - -/// Creates a new [`ProgressBar`]. -/// -/// It expects: -/// * an inclusive range of possible values, and -/// * the current value of the [`ProgressBar`]. -/// -/// [`ProgressBar`]: widget::ProgressBar -pub fn progress_bar( - range: RangeInclusive, - value: f32, -) -> widget::ProgressBar -where - Renderer: crate::Renderer, - Renderer::Theme: widget::progress_bar::StyleSheet, -{ - widget::ProgressBar::new(range, value) -} - -/// Creates a new [`Svg`] widget from the given [`Handle`]. -/// -/// [`Svg`]: widget::Svg -/// [`Handle`]: widget::svg::Handle -pub fn svg( - handle: impl Into, -) -> widget::Svg -where - Renderer: crate::svg::Renderer, - Renderer::Theme: widget::svg::StyleSheet, -{ - widget::Svg::new(handle) -} diff --git a/native/src/widget/id.rs b/native/src/widget/id.rs deleted file mode 100644 index 4b8fedf1..00000000 --- a/native/src/widget/id.rs +++ /dev/null @@ -1,43 +0,0 @@ -use std::borrow; -use std::sync::atomic::{self, AtomicUsize}; - -static NEXT_ID: AtomicUsize = AtomicUsize::new(0); - -/// The identifier of a generic widget. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Id(Internal); - -impl Id { - /// Creates a custom [`Id`]. - pub fn new(id: impl Into>) -> Self { - Self(Internal::Custom(id.into())) - } - - /// Creates a unique [`Id`]. - /// - /// This function produces a different [`Id`] every time it is called. - pub fn unique() -> Self { - let id = NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed); - - Self(Internal::Unique(id)) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum Internal { - Unique(usize), - Custom(borrow::Cow<'static, str>), -} - -#[cfg(test)] -mod tests { - use super::Id; - - #[test] - fn unique_generates_different_ids() { - let a = Id::unique(); - let b = Id::unique(); - - assert_ne!(a, b); - } -} diff --git a/native/src/widget/image.rs b/native/src/widget/image.rs deleted file mode 100644 index 73257a74..00000000 --- a/native/src/widget/image.rs +++ /dev/null @@ -1,204 +0,0 @@ -//! Display images in your user interface. -pub mod viewer; -pub use viewer::Viewer; - -use crate::image; -use crate::layout; -use crate::renderer; -use crate::widget::Tree; -use crate::{ - ContentFit, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget, -}; - -use std::hash::Hash; - -/// Creates a new [`Viewer`] with the given image `Handle`. -pub fn viewer(handle: Handle) -> Viewer { - Viewer::new(handle) -} - -/// A frame that displays an image while keeping aspect ratio. -/// -/// # Example -/// -/// ``` -/// # use iced_native::widget::Image; -/// # use iced_native::image; -/// # -/// let image = Image::::new("resources/ferris.png"); -/// ``` -/// -/// -#[derive(Debug)] -pub struct Image { - handle: Handle, - width: Length, - height: Length, - content_fit: ContentFit, -} - -impl Image { - /// Creates a new [`Image`] with the given path. - pub fn new>(handle: T) -> Self { - Image { - handle: handle.into(), - width: Length::Shrink, - height: Length::Shrink, - content_fit: ContentFit::Contain, - } - } - - /// Sets the width of the [`Image`] boundaries. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the height of the [`Image`] boundaries. - pub fn height(mut self, height: impl Into) -> Self { - self.height = height.into(); - self - } - - /// Sets the [`ContentFit`] of the [`Image`]. - /// - /// Defaults to [`ContentFit::Contain`] - pub fn content_fit(self, content_fit: ContentFit) -> Self { - Self { - content_fit, - ..self - } - } -} - -/// Computes the layout of an [`Image`]. -pub fn layout( - renderer: &Renderer, - limits: &layout::Limits, - handle: &Handle, - width: Length, - height: Length, - content_fit: ContentFit, -) -> layout::Node -where - Renderer: image::Renderer, -{ - // The raw w/h of the underlying image - let image_size = { - let Size { width, height } = renderer.dimensions(handle); - - Size::new(width as f32, height as f32) - }; - - // The size to be available to the widget prior to `Shrink`ing - let raw_size = limits.width(width).height(height).resolve(image_size); - - // The uncropped size of the image when fit to the bounds above - let full_size = content_fit.fit(image_size, raw_size); - - // Shrink the widget to fit the resized image, if requested - let final_size = Size { - width: match width { - Length::Shrink => f32::min(raw_size.width, full_size.width), - _ => raw_size.width, - }, - height: match height { - Length::Shrink => f32::min(raw_size.height, full_size.height), - _ => raw_size.height, - }, - }; - - layout::Node::new(final_size) -} - -/// Draws an [`Image`] -pub fn draw( - renderer: &mut Renderer, - layout: Layout<'_>, - handle: &Handle, - content_fit: ContentFit, -) where - Renderer: image::Renderer, - Handle: Clone + Hash, -{ - let Size { width, height } = renderer.dimensions(handle); - let image_size = Size::new(width as f32, height as f32); - - let bounds = layout.bounds(); - let adjusted_fit = content_fit.fit(image_size, bounds.size()); - - let render = |renderer: &mut Renderer| { - let offset = Vector::new( - (bounds.width - adjusted_fit.width).max(0.0) / 2.0, - (bounds.height - adjusted_fit.height).max(0.0) / 2.0, - ); - - let drawing_bounds = Rectangle { - width: adjusted_fit.width, - height: adjusted_fit.height, - ..bounds - }; - - renderer.draw(handle.clone(), drawing_bounds + offset) - }; - - if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height - { - renderer.with_layer(bounds, render); - } else { - render(renderer) - } -} - -impl Widget for Image -where - Renderer: image::Renderer, - Handle: Clone + Hash, -{ - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - layout( - renderer, - limits, - &self.handle, - self.width, - self.height, - self.content_fit, - ) - } - - fn draw( - &self, - _state: &Tree, - renderer: &mut Renderer, - _theme: &Renderer::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - _cursor_position: Point, - _viewport: &Rectangle, - ) { - draw(renderer, layout, &self.handle, self.content_fit) - } -} - -impl<'a, Message, Renderer, Handle> From> - for Element<'a, Message, Renderer> -where - Renderer: image::Renderer, - Handle: Clone + Hash + 'a, -{ - fn from(image: Image) -> Element<'a, Message, Renderer> { - Element::new(image) - } -} diff --git a/native/src/widget/image/viewer.rs b/native/src/widget/image/viewer.rs deleted file mode 100644 index 1f8d5d7a..00000000 --- a/native/src/widget/image/viewer.rs +++ /dev/null @@ -1,428 +0,0 @@ -//! Zoom and pan on an image. -use crate::event::{self, Event}; -use crate::image; -use crate::layout; -use crate::mouse; -use crate::renderer; -use crate::widget::tree::{self, Tree}; -use crate::{ - Clipboard, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size, - Vector, Widget, -}; - -use std::hash::Hash; - -/// A frame that displays an image with the ability to zoom in/out and pan. -#[allow(missing_debug_implementations)] -pub struct Viewer { - padding: f32, - width: Length, - height: Length, - min_scale: f32, - max_scale: f32, - scale_step: f32, - handle: Handle, -} - -impl Viewer { - /// Creates a new [`Viewer`] with the given [`State`]. - pub fn new(handle: Handle) -> Self { - Viewer { - padding: 0.0, - width: Length::Shrink, - height: Length::Shrink, - min_scale: 0.25, - max_scale: 10.0, - scale_step: 0.10, - handle, - } - } - - /// Sets the padding of the [`Viewer`]. - pub fn padding(mut self, padding: impl Into) -> Self { - self.padding = padding.into().0; - self - } - - /// Sets the width of the [`Viewer`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the height of the [`Viewer`]. - pub fn height(mut self, height: impl Into) -> Self { - self.height = height.into(); - self - } - - /// Sets the max scale applied to the image of the [`Viewer`]. - /// - /// Default is `10.0` - pub fn max_scale(mut self, max_scale: f32) -> Self { - self.max_scale = max_scale; - self - } - - /// Sets the min scale applied to the image of the [`Viewer`]. - /// - /// Default is `0.25` - pub fn min_scale(mut self, min_scale: f32) -> Self { - self.min_scale = min_scale; - self - } - - /// Sets the percentage the image of the [`Viewer`] will be scaled by - /// when zoomed in / out. - /// - /// Default is `0.10` - pub fn scale_step(mut self, scale_step: f32) -> Self { - self.scale_step = scale_step; - self - } -} - -impl Widget for Viewer -where - Renderer: image::Renderer, - Handle: Clone + Hash, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(State::new()) - } - - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let Size { width, height } = renderer.dimensions(&self.handle); - - let mut size = limits - .width(self.width) - .height(self.height) - .resolve(Size::new(width as f32, height as f32)); - - let expansion_size = if height > width { - self.width - } else { - self.height - }; - - // Only calculate viewport sizes if the images are constrained to a limited space. - // If they are Fill|Portion let them expand within their alotted space. - match expansion_size { - Length::Shrink | Length::Fixed(_) => { - let aspect_ratio = width as f32 / height as f32; - let viewport_aspect_ratio = size.width / size.height; - if viewport_aspect_ratio > aspect_ratio { - size.width = width as f32 * size.height / height as f32; - } else { - size.height = height as f32 * size.width / width as f32; - } - } - Length::Fill | Length::FillPortion(_) => {} - } - - layout::Node::new(size) - } - - 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 { - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); - - match event { - Event::Mouse(mouse::Event::WheelScrolled { delta }) - if is_mouse_over => - { - match delta { - mouse::ScrollDelta::Lines { y, .. } - | mouse::ScrollDelta::Pixels { y, .. } => { - let state = tree.state.downcast_mut::(); - let previous_scale = state.scale; - - if y < 0.0 && previous_scale > self.min_scale - || y > 0.0 && previous_scale < self.max_scale - { - state.scale = (if y > 0.0 { - state.scale * (1.0 + self.scale_step) - } else { - state.scale / (1.0 + self.scale_step) - }) - .clamp(self.min_scale, self.max_scale); - - let image_size = image_size( - renderer, - &self.handle, - state, - bounds.size(), - ); - - let factor = state.scale / previous_scale - 1.0; - - let cursor_to_center = - cursor_position - bounds.center(); - - let adjustment = cursor_to_center * factor - + state.current_offset * factor; - - state.current_offset = Vector::new( - if image_size.width > bounds.width { - state.current_offset.x + adjustment.x - } else { - 0.0 - }, - if image_size.height > bounds.height { - state.current_offset.y + adjustment.y - } else { - 0.0 - }, - ); - } - } - } - - event::Status::Captured - } - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - if is_mouse_over => - { - let state = tree.state.downcast_mut::(); - - state.cursor_grabbed_at = Some(cursor_position); - state.starting_offset = state.current_offset; - - event::Status::Captured - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { - let state = tree.state.downcast_mut::(); - - if state.cursor_grabbed_at.is_some() { - state.cursor_grabbed_at = None; - - event::Status::Captured - } else { - event::Status::Ignored - } - } - Event::Mouse(mouse::Event::CursorMoved { position }) => { - let state = tree.state.downcast_mut::(); - - if let Some(origin) = state.cursor_grabbed_at { - let image_size = image_size( - renderer, - &self.handle, - state, - bounds.size(), - ); - - let hidden_width = (image_size.width - bounds.width / 2.0) - .max(0.0) - .round(); - - let hidden_height = (image_size.height - - bounds.height / 2.0) - .max(0.0) - .round(); - - let delta = position - origin; - - let x = if bounds.width < image_size.width { - (state.starting_offset.x - delta.x) - .clamp(-hidden_width, hidden_width) - } else { - 0.0 - }; - - let y = if bounds.height < image_size.height { - (state.starting_offset.y - delta.y) - .clamp(-hidden_height, hidden_height) - } else { - 0.0 - }; - - state.current_offset = Vector::new(x, y); - - event::Status::Captured - } else { - event::Status::Ignored - } - } - _ => event::Status::Ignored, - } - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - let state = tree.state.downcast_ref::(); - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); - - if state.is_cursor_grabbed() { - mouse::Interaction::Grabbing - } else if is_mouse_over { - mouse::Interaction::Grab - } else { - mouse::Interaction::Idle - } - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - _theme: &Renderer::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - _cursor_position: Point, - _viewport: &Rectangle, - ) { - let state = tree.state.downcast_ref::(); - let bounds = layout.bounds(); - - let image_size = - image_size(renderer, &self.handle, state, bounds.size()); - - let translation = { - let image_top_left = Vector::new( - bounds.width / 2.0 - image_size.width / 2.0, - bounds.height / 2.0 - image_size.height / 2.0, - ); - - image_top_left - state.offset(bounds, image_size) - }; - - renderer.with_layer(bounds, |renderer| { - renderer.with_translation(translation, |renderer| { - image::Renderer::draw( - renderer, - self.handle.clone(), - Rectangle { - x: bounds.x, - y: bounds.y, - ..Rectangle::with_size(image_size) - }, - ) - }); - }); - } -} - -/// The local state of a [`Viewer`]. -#[derive(Debug, Clone, Copy)] -pub struct State { - scale: f32, - starting_offset: Vector, - current_offset: Vector, - cursor_grabbed_at: Option, -} - -impl Default for State { - fn default() -> Self { - Self { - scale: 1.0, - starting_offset: Vector::default(), - current_offset: Vector::default(), - cursor_grabbed_at: None, - } - } -} - -impl State { - /// Creates a new [`State`]. - pub fn new() -> Self { - State::default() - } - - /// Returns the current offset of the [`State`], given the bounds - /// of the [`Viewer`] and its image. - fn offset(&self, bounds: Rectangle, image_size: Size) -> Vector { - let hidden_width = - (image_size.width - bounds.width / 2.0).max(0.0).round(); - - let hidden_height = - (image_size.height - bounds.height / 2.0).max(0.0).round(); - - Vector::new( - self.current_offset.x.clamp(-hidden_width, hidden_width), - self.current_offset.y.clamp(-hidden_height, hidden_height), - ) - } - - /// Returns if the cursor is currently grabbed by the [`Viewer`]. - pub fn is_cursor_grabbed(&self) -> bool { - self.cursor_grabbed_at.is_some() - } -} - -impl<'a, Message, Renderer, Handle> From> - for Element<'a, Message, Renderer> -where - Renderer: 'a + image::Renderer, - Message: 'a, - Handle: Clone + Hash + 'a, -{ - fn from(viewer: Viewer) -> Element<'a, Message, Renderer> { - Element::new(viewer) - } -} - -/// Returns the bounds of the underlying image, given the bounds of -/// the [`Viewer`]. Scaling will be applied and original aspect ratio -/// will be respected. -pub fn image_size( - renderer: &Renderer, - handle: &::Handle, - state: &State, - bounds: Size, -) -> Size -where - Renderer: image::Renderer, -{ - let Size { width, height } = renderer.dimensions(handle); - - let (width, height) = { - let dimensions = (width as f32, height as f32); - - let width_ratio = bounds.width / dimensions.0; - let height_ratio = bounds.height / dimensions.1; - - let ratio = width_ratio.min(height_ratio); - let scale = state.scale; - - if ratio < 1.0 { - (dimensions.0 * ratio * scale, dimensions.1 * ratio * scale) - } else { - (dimensions.0 * scale, dimensions.1 * scale) - } - }; - - Size::new(width, height) -} diff --git a/native/src/widget/operation.rs b/native/src/widget/operation.rs deleted file mode 100644 index 53688a21..00000000 --- a/native/src/widget/operation.rs +++ /dev/null @@ -1,112 +0,0 @@ -//! Query or update internal widget state. -pub mod focusable; -pub mod scrollable; -pub mod text_input; - -pub use focusable::Focusable; -pub use scrollable::Scrollable; -pub use text_input::TextInput; - -use crate::widget::Id; - -use std::any::Any; -use std::fmt; - -/// A piece of logic that can traverse the widget tree of an application in -/// order to query or update some widget state. -pub trait Operation { - /// Operates on a widget that contains other widgets. - /// - /// The `operate_on_children` function can be called to return control to - /// the widget tree and keep traversing it. - fn container( - &mut self, - id: Option<&Id>, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ); - - /// Operates on a widget that can be focused. - fn focusable(&mut self, _state: &mut dyn Focusable, _id: Option<&Id>) {} - - /// Operates on a widget that can be scrolled. - fn scrollable(&mut self, _state: &mut dyn Scrollable, _id: Option<&Id>) {} - - /// Operates on a widget that has text input. - fn text_input(&mut self, _state: &mut dyn TextInput, _id: Option<&Id>) {} - - /// Operates on a custom widget with some state. - fn custom(&mut self, _state: &mut dyn Any, _id: Option<&Id>) {} - - /// Finishes the [`Operation`] and returns its [`Outcome`]. - fn finish(&self) -> Outcome { - Outcome::None - } -} - -/// The result of an [`Operation`]. -pub enum Outcome { - /// The [`Operation`] produced no result. - None, - - /// The [`Operation`] produced some result. - Some(T), - - /// The [`Operation`] needs to be followed by another [`Operation`]. - Chain(Box>), -} - -impl fmt::Debug for Outcome -where - T: fmt::Debug, -{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::None => write!(f, "Outcome::None"), - Self::Some(output) => write!(f, "Outcome::Some({output:?})"), - Self::Chain(_) => write!(f, "Outcome::Chain(...)"), - } - } -} - -/// Produces an [`Operation`] that applies the given [`Operation`] to the -/// children of a container with the given [`Id`]. -pub fn scoped( - target: Id, - operation: impl Operation + 'static, -) -> impl Operation { - struct ScopedOperation { - target: Id, - operation: Box>, - } - - impl Operation for ScopedOperation { - fn container( - &mut self, - id: Option<&Id>, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - if id == Some(&self.target) { - operate_on_children(self.operation.as_mut()); - } else { - operate_on_children(self); - } - } - - fn finish(&self) -> Outcome { - match self.operation.finish() { - Outcome::Chain(next) => { - Outcome::Chain(Box::new(ScopedOperation { - target: self.target.clone(), - operation: next, - })) - } - outcome => outcome, - } - } - } - - ScopedOperation { - target, - operation: Box::new(operation), - } -} diff --git a/native/src/widget/operation/focusable.rs b/native/src/widget/operation/focusable.rs deleted file mode 100644 index 312e4894..00000000 --- a/native/src/widget/operation/focusable.rs +++ /dev/null @@ -1,203 +0,0 @@ -//! Operate on widgets that can be focused. -use crate::widget::operation::{Operation, Outcome}; -use crate::widget::Id; - -/// The internal state of a widget that can be focused. -pub trait Focusable { - /// Returns whether the widget is focused or not. - fn is_focused(&self) -> bool; - - /// Focuses the widget. - fn focus(&mut self); - - /// Unfocuses the widget. - fn unfocus(&mut self); -} - -/// A summary of the focusable widgets present on a widget tree. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub struct Count { - /// The index of the current focused widget, if any. - pub focused: Option, - - /// The total amount of focusable widgets. - pub total: usize, -} - -/// Produces an [`Operation`] that focuses the widget with the given [`Id`]. -pub fn focus(target: Id) -> impl Operation { - struct Focus { - target: Id, - } - - impl Operation for Focus { - fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) { - match id { - Some(id) if id == &self.target => { - state.focus(); - } - _ => { - state.unfocus(); - } - } - } - - fn container( - &mut self, - _id: Option<&Id>, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self) - } - } - - Focus { target } -} - -/// Produces an [`Operation`] that generates a [`Count`] and chains it with the -/// provided function to build a new [`Operation`]. -pub fn count(f: fn(Count) -> O) -> impl Operation -where - O: Operation + 'static, -{ - struct CountFocusable { - count: Count, - next: fn(Count) -> O, - } - - impl Operation for CountFocusable - where - O: Operation + 'static, - { - fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) { - if state.is_focused() { - self.count.focused = Some(self.count.total); - } - - self.count.total += 1; - } - - fn container( - &mut self, - _id: Option<&Id>, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self) - } - - fn finish(&self) -> Outcome { - Outcome::Chain(Box::new((self.next)(self.count))) - } - } - - CountFocusable { - count: Count::default(), - next: f, - } -} - -/// Produces an [`Operation`] that searches for the current focused widget, and -/// - if found, focuses the previous focusable widget. -/// - if not found, focuses the last focusable widget. -pub fn focus_previous() -> impl Operation { - struct FocusPrevious { - count: Count, - current: usize, - } - - impl Operation for FocusPrevious { - fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) { - if self.count.total == 0 { - return; - } - - match self.count.focused { - None if self.current == self.count.total - 1 => state.focus(), - Some(0) if self.current == 0 => state.unfocus(), - Some(0) => {} - Some(focused) if focused == self.current => state.unfocus(), - Some(focused) if focused - 1 == self.current => state.focus(), - _ => {} - } - - self.current += 1; - } - - fn container( - &mut self, - _id: Option<&Id>, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self) - } - } - - count(|count| FocusPrevious { count, current: 0 }) -} - -/// Produces an [`Operation`] that searches for the current focused widget, and -/// - if found, focuses the next focusable widget. -/// - if not found, focuses the first focusable widget. -pub fn focus_next() -> impl Operation { - struct FocusNext { - count: Count, - current: usize, - } - - impl Operation for FocusNext { - fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) { - match self.count.focused { - None if self.current == 0 => state.focus(), - Some(focused) if focused == self.current => state.unfocus(), - Some(focused) if focused + 1 == self.current => state.focus(), - _ => {} - } - - self.current += 1; - } - - fn container( - &mut self, - _id: Option<&Id>, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self) - } - } - - count(|count| FocusNext { count, current: 0 }) -} - -/// Produces an [`Operation`] that searches for the current focused widget -/// and stores its ID. This ignores widgets that do not have an ID. -pub fn find_focused() -> impl Operation { - struct FindFocused { - focused: Option, - } - - impl Operation for FindFocused { - fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) { - if state.is_focused() && id.is_some() { - self.focused = id.cloned(); - } - } - - fn container( - &mut self, - _id: Option<&Id>, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self) - } - - fn finish(&self) -> Outcome { - if let Some(id) = &self.focused { - Outcome::Some(id.clone()) - } else { - Outcome::None - } - } - } - - FindFocused { focused: None } -} diff --git a/native/src/widget/operation/scrollable.rs b/native/src/widget/operation/scrollable.rs deleted file mode 100644 index 3b20631f..00000000 --- a/native/src/widget/operation/scrollable.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! Operate on widgets that can be scrolled. -use crate::widget::{Id, Operation}; - -/// The internal state of a widget that can be scrolled. -pub trait Scrollable { - /// Snaps the scroll of the widget to the given `percentage` along the horizontal & vertical axis. - fn snap_to(&mut self, offset: RelativeOffset); -} - -/// Produces an [`Operation`] that snaps the widget with the given [`Id`] to -/// the provided `percentage`. -pub fn snap_to(target: Id, offset: RelativeOffset) -> impl Operation { - struct SnapTo { - target: Id, - offset: RelativeOffset, - } - - impl Operation for SnapTo { - fn container( - &mut self, - _id: Option<&Id>, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self) - } - - fn scrollable(&mut self, state: &mut dyn Scrollable, id: Option<&Id>) { - if Some(&self.target) == id { - state.snap_to(self.offset); - } - } - } - - SnapTo { target, offset } -} - -/// The amount of offset in each direction of a [`Scrollable`]. -/// -/// A value of `0.0` means start, while `1.0` means end. -#[derive(Debug, Clone, Copy, PartialEq, Default)] -pub struct RelativeOffset { - /// The amount of horizontal offset - pub x: f32, - /// The amount of vertical offset - pub y: f32, -} - -impl RelativeOffset { - /// A relative offset that points to the top-left of a [`Scrollable`]. - pub const START: Self = Self { x: 0.0, y: 0.0 }; - - /// A relative offset that points to the bottom-right of a [`Scrollable`]. - pub const END: Self = Self { x: 1.0, y: 1.0 }; -} diff --git a/native/src/widget/operation/text_input.rs b/native/src/widget/operation/text_input.rs deleted file mode 100644 index 4c773e99..00000000 --- a/native/src/widget/operation/text_input.rs +++ /dev/null @@ -1,131 +0,0 @@ -//! Operate on widgets that have text input. -use crate::widget::operation::Operation; -use crate::widget::Id; - -/// The internal state of a widget that has text input. -pub trait TextInput { - /// Moves the cursor of the text input to the front of the input text. - fn move_cursor_to_front(&mut self); - /// Moves the cursor of the text input to the end of the input text. - fn move_cursor_to_end(&mut self); - /// Moves the cursor of the text input to an arbitrary location. - fn move_cursor_to(&mut self, position: usize); - /// Selects all the content of the text input. - fn select_all(&mut self); -} - -/// Produces an [`Operation`] that moves the cursor of the widget with the given [`Id`] to the -/// front. -pub fn move_cursor_to_front(target: Id) -> impl Operation { - struct MoveCursor { - target: Id, - } - - impl Operation for MoveCursor { - fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { - match id { - Some(id) if id == &self.target => { - state.move_cursor_to_front(); - } - _ => {} - } - } - - fn container( - &mut self, - _id: Option<&Id>, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self) - } - } - - MoveCursor { target } -} - -/// Produces an [`Operation`] that moves the cursor of the widget with the given [`Id`] to the -/// end. -pub fn move_cursor_to_end(target: Id) -> impl Operation { - struct MoveCursor { - target: Id, - } - - impl Operation for MoveCursor { - fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { - match id { - Some(id) if id == &self.target => { - state.move_cursor_to_end(); - } - _ => {} - } - } - - fn container( - &mut self, - _id: Option<&Id>, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self) - } - } - - MoveCursor { target } -} - -/// Produces an [`Operation`] that moves the cursor of the widget with the given [`Id`] to the -/// provided position. -pub fn move_cursor_to(target: Id, position: usize) -> impl Operation { - struct MoveCursor { - target: Id, - position: usize, - } - - impl Operation for MoveCursor { - fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { - match id { - Some(id) if id == &self.target => { - state.move_cursor_to(self.position); - } - _ => {} - } - } - - fn container( - &mut self, - _id: Option<&Id>, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self) - } - } - - MoveCursor { target, position } -} - -/// Produces an [`Operation`] that selects all the content of the widget with the given [`Id`]. -pub fn select_all(target: Id) -> impl Operation { - struct MoveCursor { - target: Id, - } - - impl Operation for MoveCursor { - fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { - match id { - Some(id) if id == &self.target => { - state.select_all(); - } - _ => {} - } - } - - fn container( - &mut self, - _id: Option<&Id>, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self) - } - } - - MoveCursor { target } -} diff --git a/native/src/widget/pane_grid.rs b/native/src/widget/pane_grid.rs deleted file mode 100644 index bcb17ebd..00000000 --- a/native/src/widget/pane_grid.rs +++ /dev/null @@ -1,991 +0,0 @@ -//! 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.8/examples/pane_grid -mod axis; -mod configuration; -mod content; -mod direction; -mod draggable; -mod node; -mod pane; -mod split; -mod title_bar; - -pub mod state; - -pub use axis::Axis; -pub use configuration::Configuration; -pub use content::Content; -pub use direction::Direction; -pub use draggable::Draggable; -pub use node::Node; -pub use pane::Pane; -pub use split::Split; -pub use state::State; -pub use title_bar::TitleBar; - -pub use iced_style::pane_grid::{Line, StyleSheet}; - -use crate::event::{self, Event}; -use crate::layout; -use crate::mouse; -use crate::overlay::{self, Group}; -use crate::renderer; -use crate::touch; -use crate::widget; -use crate::widget::container; -use crate::widget::tree::{self, Tree}; -use crate::{ - Clipboard, Color, Element, Layout, Length, Pixels, Point, Rectangle, Shell, - Size, Vector, Widget, -}; - -/// A collection of panes distributed using either vertical or horizontal splits -/// to completely fill the space available. -/// -/// [![Pane grid - Iced](https://thumbs.gfycat.com/FrailFreshAiredaleterrier-small.gif)](https://gfycat.com/frailfreshairedaleterrier) -/// -/// This distribution of space is common in tiling window managers (like -/// [`awesome`](https://awesomewm.org/), [`i3`](https://i3wm.org/), or even -/// [`tmux`](https://github.com/tmux/tmux)). -/// -/// A [`PaneGrid`] supports: -/// -/// * Vertical and horizontal splits -/// * Tracking of the last active pane -/// * Mouse-based resizing -/// * Drag and drop to reorganize panes -/// * Hotkey support -/// * Configurable modifier keys -/// * [`State`] API to perform actions programmatically (`split`, `swap`, `resize`, etc.) -/// -/// ## Example -/// -/// ``` -/// # use iced_native::widget::{pane_grid, text}; -/// # -/// # type PaneGrid<'a, Message> = -/// # iced_native::widget::PaneGrid<'a, Message, iced_native::renderer::Null>; -/// # -/// enum PaneState { -/// SomePane, -/// AnotherKindOfPane, -/// } -/// -/// enum Message { -/// PaneDragged(pane_grid::DragEvent), -/// PaneResized(pane_grid::ResizeEvent), -/// } -/// -/// let (mut state, _) = pane_grid::State::new(PaneState::SomePane); -/// -/// let pane_grid = -/// PaneGrid::new(&state, |pane, state, is_maximized| { -/// pane_grid::Content::new(match state { -/// PaneState::SomePane => text("This is some pane"), -/// PaneState::AnotherKindOfPane => text("This is another kind of pane"), -/// }) -/// }) -/// .on_drag(Message::PaneDragged) -/// .on_resize(10, Message::PaneResized); -/// ``` -#[allow(missing_debug_implementations)] -pub struct PaneGrid<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet + container::StyleSheet, -{ - contents: Contents<'a, Content<'a, Message, Renderer>>, - width: Length, - height: Length, - spacing: f32, - on_click: Option Message + 'a>>, - on_drag: Option Message + 'a>>, - on_resize: Option<(f32, Box Message + 'a>)>, - style: ::Style, -} - -impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet + container::StyleSheet, -{ - /// Creates a [`PaneGrid`] with the given [`State`] and view function. - /// - /// The view function will be called to display each [`Pane`] present in the - /// [`State`]. [`bool`] is set if the pane is maximized. - pub fn new( - state: &'a State, - view: impl Fn(Pane, &'a T, bool) -> Content<'a, Message, Renderer>, - ) -> Self { - let contents = if let Some((pane, pane_state)) = - state.maximized.and_then(|pane| { - state.panes.get(&pane).map(|pane_state| (pane, pane_state)) - }) { - Contents::Maximized( - pane, - view(pane, pane_state, true), - Node::Pane(pane), - ) - } else { - Contents::All( - state - .panes - .iter() - .map(|(pane, pane_state)| { - (*pane, view(*pane, pane_state, false)) - }) - .collect(), - &state.internal, - ) - }; - - Self { - contents, - width: Length::Fill, - height: Length::Fill, - spacing: 0.0, - on_click: None, - on_drag: None, - on_resize: None, - style: Default::default(), - } - } - - /// Sets the width of the [`PaneGrid`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the height of the [`PaneGrid`]. - pub fn height(mut self, height: impl Into) -> Self { - self.height = height.into(); - self - } - - /// Sets the spacing _between_ the panes of the [`PaneGrid`]. - pub fn spacing(mut self, amount: impl Into) -> Self { - self.spacing = amount.into().0; - self - } - - /// Sets the message that will be produced when a [`Pane`] of the - /// [`PaneGrid`] is clicked. - pub fn on_click(mut self, f: F) -> Self - where - F: 'a + Fn(Pane) -> Message, - { - self.on_click = Some(Box::new(f)); - self - } - - /// Enables the drag and drop interactions of the [`PaneGrid`], which will - /// use the provided function to produce messages. - pub fn on_drag(mut self, f: F) -> Self - where - F: 'a + Fn(DragEvent) -> Message, - { - self.on_drag = Some(Box::new(f)); - self - } - - /// Enables the resize interactions of the [`PaneGrid`], which will - /// use the provided function to produce messages. - /// - /// The `leeway` describes the amount of space around a split that can be - /// used to grab it. - /// - /// The grabbable area of a split will have a length of `spacing + leeway`, - /// properly centered. In other words, a length of - /// `(spacing + leeway) / 2.0` on either side of the split line. - pub fn on_resize(mut self, leeway: impl Into, f: F) -> Self - where - F: 'a + Fn(ResizeEvent) -> Message, - { - self.on_resize = Some((leeway.into().0, Box::new(f))); - self - } - - /// Sets the style of the [`PaneGrid`]. - pub fn style( - mut self, - style: impl Into<::Style>, - ) -> Self { - self.style = style.into(); - self - } - - fn drag_enabled(&self) -> bool { - (!self.contents.is_maximized()) - .then(|| self.on_drag.is_some()) - .unwrap_or_default() - } -} - -impl<'a, Message, Renderer> Widget - for PaneGrid<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet + container::StyleSheet, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(state::Action::Idle) - } - - fn children(&self) -> Vec { - self.contents - .iter() - .map(|(_, content)| content.state()) - .collect() - } - - fn diff(&self, tree: &mut Tree) { - match &self.contents { - Contents::All(contents, _) => tree.diff_children_custom( - contents, - |state, (_, content)| content.diff(state), - |(_, content)| content.state(), - ), - Contents::Maximized(_, content, _) => tree.diff_children_custom( - &[content], - |state, content| content.diff(state), - |content| content.state(), - ), - } - } - - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - layout( - renderer, - limits, - self.contents.layout(), - self.width, - self.height, - self.spacing, - self.contents.iter(), - |content, renderer, limits| content.layout(renderer, limits), - ) - } - - fn operate( - &self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn widget::Operation, - ) { - operation.container(None, &mut |operation| { - self.contents - .iter() - .zip(&mut tree.children) - .zip(layout.children()) - .for_each(|(((_pane, content), state), layout)| { - content.operate(state, layout, renderer, 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 { - let action = tree.state.downcast_mut::(); - - let on_drag = if self.drag_enabled() { - &self.on_drag - } else { - &None - }; - - let event_status = update( - action, - self.contents.layout(), - &event, - layout, - cursor_position, - shell, - self.spacing, - self.contents.iter(), - &self.on_click, - on_drag, - &self.on_resize, - ); - - let picked_pane = action.picked_pane().map(|(pane, _)| pane); - - self.contents - .iter_mut() - .zip(&mut tree.children) - .zip(layout.children()) - .map(|(((pane, content), tree), layout)| { - let is_picked = picked_pane == Some(pane); - - content.on_event( - tree, - event.clone(), - layout, - cursor_position, - renderer, - clipboard, - shell, - is_picked, - ) - }) - .fold(event_status, event::Status::merge) - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - mouse_interaction( - tree.state.downcast_ref(), - self.contents.layout(), - layout, - cursor_position, - self.spacing, - self.on_resize.as_ref().map(|(leeway, _)| *leeway), - ) - .unwrap_or_else(|| { - self.contents - .iter() - .zip(&tree.children) - .zip(layout.children()) - .map(|(((_pane, content), tree), layout)| { - content.mouse_interaction( - tree, - layout, - cursor_position, - viewport, - renderer, - self.drag_enabled(), - ) - }) - .max() - .unwrap_or_default() - }) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - draw( - tree.state.downcast_ref(), - self.contents.layout(), - layout, - cursor_position, - renderer, - theme, - style, - viewport, - self.spacing, - self.on_resize.as_ref().map(|(leeway, _)| *leeway), - &self.style, - self.contents - .iter() - .zip(&tree.children) - .map(|((pane, content), tree)| (pane, (content, tree))), - |(content, tree), - renderer, - style, - layout, - cursor_position, - rectangle| { - content.draw( - tree, - renderer, - theme, - style, - layout, - cursor_position, - rectangle, - ); - }, - ) - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - let children = self - .contents - .iter_mut() - .zip(&mut tree.children) - .zip(layout.children()) - .filter_map(|(((_, content), state), layout)| { - content.overlay(state, layout, renderer) - }) - .collect::>(); - - (!children.is_empty()).then(|| Group::with_children(children).overlay()) - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Message: 'a, - Renderer: 'a + crate::Renderer, - Renderer::Theme: StyleSheet + container::StyleSheet, -{ - fn from( - pane_grid: PaneGrid<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(pane_grid) - } -} - -/// Calculates the [`Layout`] of a [`PaneGrid`]. -pub fn layout( - renderer: &Renderer, - limits: &layout::Limits, - node: &Node, - width: Length, - height: Length, - spacing: f32, - contents: impl Iterator, - layout_content: impl Fn(T, &Renderer, &layout::Limits) -> layout::Node, -) -> layout::Node { - let limits = limits.width(width).height(height); - let size = limits.resolve(Size::ZERO); - - let regions = node.pane_regions(spacing, size); - let children = contents - .filter_map(|(pane, content)| { - let region = regions.get(&pane)?; - let size = Size::new(region.width, region.height); - - let mut node = layout_content( - content, - renderer, - &layout::Limits::new(size, size), - ); - - node.move_to(Point::new(region.x, region.y)); - - Some(node) - }) - .collect(); - - layout::Node::with_children(size, children) -} - -/// Processes an [`Event`] and updates the [`state`] of a [`PaneGrid`] -/// accordingly. -pub fn update<'a, Message, T: Draggable>( - action: &mut state::Action, - node: &Node, - event: &Event, - layout: Layout<'_>, - cursor_position: Point, - shell: &mut Shell<'_, Message>, - spacing: f32, - contents: impl Iterator, - on_click: &Option Message + 'a>>, - on_drag: &Option Message + 'a>>, - on_resize: &Option<(f32, Box Message + 'a>)>, -) -> event::Status { - let mut event_status = event::Status::Ignored; - - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let bounds = layout.bounds(); - - if bounds.contains(cursor_position) { - event_status = event::Status::Captured; - - match on_resize { - Some((leeway, _)) => { - let relative_cursor = Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ); - - let splits = node.split_regions( - spacing, - Size::new(bounds.width, bounds.height), - ); - - let clicked_split = hovered_split( - splits.iter(), - spacing + leeway, - relative_cursor, - ); - - if let Some((split, axis, _)) = clicked_split { - if action.picked_pane().is_none() { - *action = - state::Action::Resizing { split, axis }; - } - } else { - click_pane( - action, - layout, - cursor_position, - shell, - contents, - on_click, - on_drag, - ); - } - } - None => { - click_pane( - action, - layout, - cursor_position, - shell, - contents, - on_click, - on_drag, - ); - } - } - } - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - if let Some((pane, _)) = action.picked_pane() { - if let Some(on_drag) = on_drag { - let mut dropped_region = contents - .zip(layout.children()) - .filter(|(_, layout)| { - layout.bounds().contains(cursor_position) - }); - - let event = match dropped_region.next() { - Some(((target, _), _)) if pane != target => { - DragEvent::Dropped { pane, target } - } - _ => DragEvent::Canceled { pane }, - }; - - shell.publish(on_drag(event)); - } - - *action = state::Action::Idle; - - event_status = event::Status::Captured; - } else if action.picked_split().is_some() { - *action = state::Action::Idle; - - event_status = event::Status::Captured; - } - } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - if let Some((_, on_resize)) = on_resize { - if let Some((split, _)) = action.picked_split() { - let bounds = layout.bounds(); - - let splits = node.split_regions( - spacing, - Size::new(bounds.width, bounds.height), - ); - - if let Some((axis, rectangle, _)) = splits.get(&split) { - let ratio = match axis { - Axis::Horizontal => { - let position = - cursor_position.y - bounds.y - rectangle.y; - - (position / rectangle.height).clamp(0.1, 0.9) - } - Axis::Vertical => { - let position = - cursor_position.x - bounds.x - rectangle.x; - - (position / rectangle.width).clamp(0.1, 0.9) - } - }; - - shell.publish(on_resize(ResizeEvent { split, ratio })); - - event_status = event::Status::Captured; - } - } - } - } - _ => {} - } - - event_status -} - -fn click_pane<'a, Message, T>( - action: &mut state::Action, - layout: Layout<'_>, - cursor_position: Point, - shell: &mut Shell<'_, Message>, - contents: impl Iterator, - on_click: &Option Message + 'a>>, - on_drag: &Option Message + 'a>>, -) where - T: Draggable, -{ - let mut clicked_region = contents - .zip(layout.children()) - .filter(|(_, layout)| layout.bounds().contains(cursor_position)); - - if let Some(((pane, content), layout)) = clicked_region.next() { - if let Some(on_click) = &on_click { - shell.publish(on_click(pane)); - } - - if let Some(on_drag) = &on_drag { - if content.can_be_dragged_at(layout, cursor_position) { - let pane_position = layout.position(); - - let origin = cursor_position - - Vector::new(pane_position.x, pane_position.y); - - *action = state::Action::Dragging { pane, origin }; - - shell.publish(on_drag(DragEvent::Picked { pane })); - } - } - } -} - -/// Returns the current [`mouse::Interaction`] of a [`PaneGrid`]. -pub fn mouse_interaction( - action: &state::Action, - node: &Node, - layout: Layout<'_>, - cursor_position: Point, - spacing: f32, - resize_leeway: Option, -) -> Option { - if action.picked_pane().is_some() { - return Some(mouse::Interaction::Grabbing); - } - - let resize_axis = - action.picked_split().map(|(_, axis)| axis).or_else(|| { - resize_leeway.and_then(|leeway| { - let bounds = layout.bounds(); - - let splits = node.split_regions(spacing, bounds.size()); - - let relative_cursor = Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ); - - hovered_split(splits.iter(), spacing + leeway, relative_cursor) - .map(|(_, axis, _)| axis) - }) - }); - - if let Some(resize_axis) = resize_axis { - return Some(match resize_axis { - Axis::Horizontal => mouse::Interaction::ResizingVertically, - Axis::Vertical => mouse::Interaction::ResizingHorizontally, - }); - } - - None -} - -/// Draws a [`PaneGrid`]. -pub fn draw( - action: &state::Action, - node: &Node, - layout: Layout<'_>, - cursor_position: Point, - renderer: &mut Renderer, - theme: &Renderer::Theme, - default_style: &renderer::Style, - viewport: &Rectangle, - spacing: f32, - resize_leeway: Option, - style: &::Style, - contents: impl Iterator, - draw_pane: impl Fn( - T, - &mut Renderer, - &renderer::Style, - Layout<'_>, - Point, - &Rectangle, - ), -) where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - let picked_pane = action.picked_pane(); - - let picked_split = action - .picked_split() - .and_then(|(split, axis)| { - let bounds = layout.bounds(); - - let splits = node.split_regions(spacing, bounds.size()); - - let (_axis, region, ratio) = splits.get(&split)?; - - let region = axis.split_line_bounds(*region, *ratio, spacing); - - Some((axis, region + Vector::new(bounds.x, bounds.y), true)) - }) - .or_else(|| match resize_leeway { - Some(leeway) => { - let bounds = layout.bounds(); - - let relative_cursor = Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ); - - let splits = node.split_regions(spacing, bounds.size()); - - let (_split, axis, region) = hovered_split( - splits.iter(), - spacing + leeway, - relative_cursor, - )?; - - Some((axis, region + Vector::new(bounds.x, bounds.y), false)) - } - None => None, - }); - - let pane_cursor_position = if picked_pane.is_some() { - // TODO: Remove once cursor availability is encoded in the type - // system - Point::new(-1.0, -1.0) - } else { - cursor_position - }; - - let mut render_picked_pane = None; - - for ((id, pane), layout) in contents.zip(layout.children()) { - match picked_pane { - Some((dragging, origin)) if id == dragging => { - render_picked_pane = Some((pane, origin, layout)); - } - _ => { - draw_pane( - pane, - renderer, - default_style, - layout, - pane_cursor_position, - viewport, - ); - } - } - } - - // Render picked pane last - if let Some((pane, origin, layout)) = render_picked_pane { - let bounds = layout.bounds(); - - renderer.with_translation( - cursor_position - - Point::new(bounds.x + origin.x, bounds.y + origin.y), - |renderer| { - renderer.with_layer(bounds, |renderer| { - draw_pane( - pane, - renderer, - default_style, - layout, - pane_cursor_position, - viewport, - ); - }); - }, - ); - }; - - if let Some((axis, split_region, is_picked)) = picked_split { - let highlight = if is_picked { - theme.picked_split(style) - } else { - theme.hovered_split(style) - }; - - if let Some(highlight) = highlight { - renderer.fill_quad( - renderer::Quad { - bounds: match axis { - Axis::Horizontal => Rectangle { - x: split_region.x, - y: (split_region.y - + (split_region.height - highlight.width) - / 2.0) - .round(), - width: split_region.width, - height: highlight.width, - }, - Axis::Vertical => Rectangle { - x: (split_region.x - + (split_region.width - highlight.width) / 2.0) - .round(), - y: split_region.y, - width: highlight.width, - height: split_region.height, - }, - }, - border_radius: 0.0.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - highlight.color, - ); - } - } -} - -/// An event produced during a drag and drop interaction of a [`PaneGrid`]. -#[derive(Debug, Clone, Copy)] -pub enum DragEvent { - /// A [`Pane`] was picked for dragging. - Picked { - /// The picked [`Pane`]. - pane: Pane, - }, - - /// A [`Pane`] was dropped on top of another [`Pane`]. - Dropped { - /// The picked [`Pane`]. - pane: Pane, - - /// The [`Pane`] where the picked one was dropped on. - target: Pane, - }, - - /// A [`Pane`] was picked and then dropped outside of other [`Pane`] - /// boundaries. - Canceled { - /// The picked [`Pane`]. - pane: Pane, - }, -} - -/// An event produced during a resize interaction of a [`PaneGrid`]. -#[derive(Debug, Clone, Copy)] -pub struct ResizeEvent { - /// The [`Split`] that is being dragged for resizing. - pub split: Split, - - /// The new ratio of the [`Split`]. - /// - /// The ratio is a value in [0, 1], representing the exact position of a - /// [`Split`] between two panes. - pub ratio: f32, -} - -/* - * Helpers - */ -fn hovered_split<'a>( - splits: impl Iterator, - spacing: f32, - cursor_position: Point, -) -> Option<(Split, Axis, Rectangle)> { - splits - .filter_map(|(split, (axis, region, ratio))| { - let bounds = axis.split_line_bounds(*region, *ratio, spacing); - - if bounds.contains(cursor_position) { - Some((*split, *axis, bounds)) - } else { - None - } - }) - .next() -} - -/// The visible contents of the [`PaneGrid`] -#[derive(Debug)] -pub enum Contents<'a, T> { - /// All panes are visible - All(Vec<(Pane, T)>, &'a state::Internal), - /// A maximized pane is visible - Maximized(Pane, T, Node), -} - -impl<'a, T> Contents<'a, T> { - /// Returns the layout [`Node`] of the [`Contents`] - pub fn layout(&self) -> &Node { - match self { - Contents::All(_, state) => state.layout(), - Contents::Maximized(_, _, layout) => layout, - } - } - - /// Returns an iterator over the values of the [`Contents`] - pub fn iter(&self) -> Box + '_> { - match self { - Contents::All(contents, _) => Box::new( - contents.iter().map(|(pane, content)| (*pane, content)), - ), - Contents::Maximized(pane, content, _) => { - Box::new(std::iter::once((*pane, content))) - } - } - } - - fn iter_mut(&mut self) -> Box + '_> { - match self { - Contents::All(contents, _) => Box::new( - contents.iter_mut().map(|(pane, content)| (*pane, content)), - ), - Contents::Maximized(pane, content, _) => { - Box::new(std::iter::once((*pane, content))) - } - } - } - - fn is_maximized(&self) -> bool { - matches!(self, Self::Maximized(..)) - } -} diff --git a/native/src/widget/pane_grid/axis.rs b/native/src/widget/pane_grid/axis.rs deleted file mode 100644 index 02bde064..00000000 --- a/native/src/widget/pane_grid/axis.rs +++ /dev/null @@ -1,241 +0,0 @@ -use crate::Rectangle; - -/// A fixed reference line for the measurement of coordinates. -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] -pub enum Axis { - /// The horizontal axis: — - Horizontal, - /// The vertical axis: | - Vertical, -} - -impl Axis { - /// Splits the provided [`Rectangle`] on the current [`Axis`] with the - /// given `ratio` and `spacing`. - pub fn split( - &self, - rectangle: &Rectangle, - ratio: f32, - spacing: f32, - ) -> (Rectangle, Rectangle) { - match self { - Axis::Horizontal => { - let height_top = - (rectangle.height * ratio - spacing / 2.0).round(); - let height_bottom = rectangle.height - height_top - spacing; - - ( - Rectangle { - height: height_top, - ..*rectangle - }, - Rectangle { - y: rectangle.y + height_top + spacing, - height: height_bottom, - ..*rectangle - }, - ) - } - Axis::Vertical => { - let width_left = - (rectangle.width * ratio - spacing / 2.0).round(); - let width_right = rectangle.width - width_left - spacing; - - ( - Rectangle { - width: width_left, - ..*rectangle - }, - Rectangle { - x: rectangle.x + width_left + spacing, - width: width_right, - ..*rectangle - }, - ) - } - } - } - - /// Calculates the bounds of the split line in a [`Rectangle`] region. - pub fn split_line_bounds( - &self, - rectangle: Rectangle, - ratio: f32, - spacing: f32, - ) -> Rectangle { - match self { - Axis::Horizontal => Rectangle { - x: rectangle.x, - y: (rectangle.y + rectangle.height * ratio - spacing / 2.0) - .round(), - width: rectangle.width, - height: spacing, - }, - Axis::Vertical => Rectangle { - x: (rectangle.x + rectangle.width * ratio - spacing / 2.0) - .round(), - y: rectangle.y, - width: spacing, - height: rectangle.height, - }, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - enum Case { - Horizontal { - overall_height: f32, - spacing: f32, - top_height: f32, - bottom_y: f32, - bottom_height: f32, - }, - Vertical { - overall_width: f32, - spacing: f32, - left_width: f32, - right_x: f32, - right_width: f32, - }, - } - - #[test] - fn split() { - let cases = vec![ - // Even height, even spacing - Case::Horizontal { - overall_height: 10.0, - spacing: 2.0, - top_height: 4.0, - bottom_y: 6.0, - bottom_height: 4.0, - }, - // Odd height, even spacing - Case::Horizontal { - overall_height: 9.0, - spacing: 2.0, - top_height: 4.0, - bottom_y: 6.0, - bottom_height: 3.0, - }, - // Even height, odd spacing - Case::Horizontal { - overall_height: 10.0, - spacing: 1.0, - top_height: 5.0, - bottom_y: 6.0, - bottom_height: 4.0, - }, - // Odd height, odd spacing - Case::Horizontal { - overall_height: 9.0, - spacing: 1.0, - top_height: 4.0, - bottom_y: 5.0, - bottom_height: 4.0, - }, - // Even width, even spacing - Case::Vertical { - overall_width: 10.0, - spacing: 2.0, - left_width: 4.0, - right_x: 6.0, - right_width: 4.0, - }, - // Odd width, even spacing - Case::Vertical { - overall_width: 9.0, - spacing: 2.0, - left_width: 4.0, - right_x: 6.0, - right_width: 3.0, - }, - // Even width, odd spacing - Case::Vertical { - overall_width: 10.0, - spacing: 1.0, - left_width: 5.0, - right_x: 6.0, - right_width: 4.0, - }, - // Odd width, odd spacing - Case::Vertical { - overall_width: 9.0, - spacing: 1.0, - left_width: 4.0, - right_x: 5.0, - right_width: 4.0, - }, - ]; - for case in cases { - match case { - Case::Horizontal { - overall_height, - spacing, - top_height, - bottom_y, - bottom_height, - } => { - let a = Axis::Horizontal; - let r = Rectangle { - x: 0.0, - y: 0.0, - width: 10.0, - height: overall_height, - }; - let (top, bottom) = a.split(&r, 0.5, spacing); - assert_eq!( - top, - Rectangle { - height: top_height, - ..r - } - ); - assert_eq!( - bottom, - Rectangle { - y: bottom_y, - height: bottom_height, - ..r - } - ); - } - Case::Vertical { - overall_width, - spacing, - left_width, - right_x, - right_width, - } => { - let a = Axis::Vertical; - let r = Rectangle { - x: 0.0, - y: 0.0, - width: overall_width, - height: 10.0, - }; - let (left, right) = a.split(&r, 0.5, spacing); - assert_eq!( - left, - Rectangle { - width: left_width, - ..r - } - ); - assert_eq!( - right, - Rectangle { - x: right_x, - width: right_width, - ..r - } - ); - } - } - } - } -} diff --git a/native/src/widget/pane_grid/configuration.rs b/native/src/widget/pane_grid/configuration.rs deleted file mode 100644 index 7d68fb46..00000000 --- a/native/src/widget/pane_grid/configuration.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::widget::pane_grid::Axis; - -/// The arrangement of a [`PaneGrid`]. -/// -/// [`PaneGrid`]: crate::widget::PaneGrid -#[derive(Debug, Clone)] -pub enum Configuration { - /// A split of the available space. - Split { - /// The direction of the split. - axis: Axis, - - /// The ratio of the split in [0.0, 1.0]. - ratio: f32, - - /// The left/top [`Configuration`] of the split. - a: Box>, - - /// The right/bottom [`Configuration`] of the split. - b: Box>, - }, - /// A [`Pane`]. - /// - /// [`Pane`]: crate::widget::pane_grid::Pane - Pane(T), -} diff --git a/native/src/widget/pane_grid/content.rs b/native/src/widget/pane_grid/content.rs deleted file mode 100644 index c9b0df07..00000000 --- a/native/src/widget/pane_grid/content.rs +++ /dev/null @@ -1,373 +0,0 @@ -use crate::event::{self, Event}; -use crate::layout; -use crate::mouse; -use crate::overlay; -use crate::renderer; -use crate::widget::container; -use crate::widget::pane_grid::{Draggable, TitleBar}; -use crate::widget::{self, Tree}; -use crate::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size}; - -/// The content of a [`Pane`]. -/// -/// [`Pane`]: crate::widget::pane_grid::Pane -#[allow(missing_debug_implementations)] -pub struct Content<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: container::StyleSheet, -{ - title_bar: Option>, - body: Element<'a, Message, Renderer>, - style: ::Style, -} - -impl<'a, Message, Renderer> Content<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: container::StyleSheet, -{ - /// Creates a new [`Content`] with the provided body. - pub fn new(body: impl Into>) -> Self { - Self { - title_bar: None, - body: body.into(), - style: Default::default(), - } - } - - /// Sets the [`TitleBar`] of this [`Content`]. - pub fn title_bar( - mut self, - title_bar: TitleBar<'a, Message, Renderer>, - ) -> Self { - self.title_bar = Some(title_bar); - self - } - - /// Sets the style of the [`Content`]. - pub fn style( - mut self, - style: impl Into<::Style>, - ) -> Self { - self.style = style.into(); - self - } -} - -impl<'a, Message, Renderer> Content<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: container::StyleSheet, -{ - pub(super) fn state(&self) -> Tree { - let children = if let Some(title_bar) = self.title_bar.as_ref() { - vec![Tree::new(&self.body), title_bar.state()] - } else { - vec![Tree::new(&self.body), Tree::empty()] - }; - - Tree { - children, - ..Tree::empty() - } - } - - pub(super) fn diff(&self, tree: &mut Tree) { - if tree.children.len() == 2 { - if let Some(title_bar) = self.title_bar.as_ref() { - title_bar.diff(&mut tree.children[1]); - } - - tree.children[0].diff(&self.body); - } else { - *tree = self.state(); - } - } - - /// Draws the [`Content`] with the provided [`Renderer`] and [`Layout`]. - /// - /// [`Renderer`]: crate::Renderer - pub fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - use container::StyleSheet; - - let bounds = layout.bounds(); - - { - let style = theme.appearance(&self.style); - - container::draw_background(renderer, &style, bounds); - } - - if let Some(title_bar) = &self.title_bar { - let mut children = layout.children(); - let title_bar_layout = children.next().unwrap(); - let body_layout = children.next().unwrap(); - - let show_controls = bounds.contains(cursor_position); - - self.body.as_widget().draw( - &tree.children[0], - renderer, - theme, - style, - body_layout, - cursor_position, - viewport, - ); - - title_bar.draw( - &tree.children[1], - renderer, - theme, - style, - title_bar_layout, - cursor_position, - viewport, - show_controls, - ); - } else { - self.body.as_widget().draw( - &tree.children[0], - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ); - } - } - - pub(crate) fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - if let Some(title_bar) = &self.title_bar { - let max_size = limits.max(); - - let title_bar_layout = title_bar - .layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); - - let title_bar_size = title_bar_layout.size(); - - let mut body_layout = self.body.as_widget().layout( - renderer, - &layout::Limits::new( - Size::ZERO, - Size::new( - max_size.width, - max_size.height - title_bar_size.height, - ), - ), - ); - - body_layout.move_to(Point::new(0.0, title_bar_size.height)); - - layout::Node::with_children( - max_size, - vec![title_bar_layout, body_layout], - ) - } else { - self.body.as_widget().layout(renderer, limits) - } - } - - pub(crate) fn operate( - &self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn widget::Operation, - ) { - let body_layout = if let Some(title_bar) = &self.title_bar { - let mut children = layout.children(); - - title_bar.operate( - &mut tree.children[1], - children.next().unwrap(), - renderer, - operation, - ); - - children.next().unwrap() - } else { - layout - }; - - self.body.as_widget().operate( - &mut tree.children[0], - body_layout, - renderer, - operation, - ); - } - - pub(crate) 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>, - is_picked: bool, - ) -> event::Status { - let mut event_status = event::Status::Ignored; - - let body_layout = if let Some(title_bar) = &mut self.title_bar { - let mut children = layout.children(); - - event_status = title_bar.on_event( - &mut tree.children[1], - event.clone(), - children.next().unwrap(), - cursor_position, - renderer, - clipboard, - shell, - ); - - children.next().unwrap() - } else { - layout - }; - - let body_status = if is_picked { - event::Status::Ignored - } else { - self.body.as_widget_mut().on_event( - &mut tree.children[0], - event, - body_layout, - cursor_position, - renderer, - clipboard, - shell, - ) - }; - - event_status.merge(body_status) - } - - pub(crate) fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - drag_enabled: bool, - ) -> mouse::Interaction { - let (body_layout, title_bar_interaction) = - if let Some(title_bar) = &self.title_bar { - let mut children = layout.children(); - let title_bar_layout = children.next().unwrap(); - - let is_over_pick_area = title_bar - .is_over_pick_area(title_bar_layout, cursor_position); - - if is_over_pick_area && drag_enabled { - return mouse::Interaction::Grab; - } - - let mouse_interaction = title_bar.mouse_interaction( - &tree.children[1], - title_bar_layout, - cursor_position, - viewport, - renderer, - ); - - (children.next().unwrap(), mouse_interaction) - } else { - (layout, mouse::Interaction::default()) - }; - - self.body - .as_widget() - .mouse_interaction( - &tree.children[0], - body_layout, - cursor_position, - viewport, - renderer, - ) - .max(title_bar_interaction) - } - - pub(crate) fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - if let Some(title_bar) = self.title_bar.as_mut() { - let mut children = layout.children(); - let title_bar_layout = children.next()?; - - let mut states = tree.children.iter_mut(); - let body_state = states.next().unwrap(); - let title_bar_state = states.next().unwrap(); - - match title_bar.overlay(title_bar_state, title_bar_layout, renderer) - { - Some(overlay) => Some(overlay), - None => self.body.as_widget_mut().overlay( - body_state, - children.next()?, - renderer, - ), - } - } else { - self.body.as_widget_mut().overlay( - &mut tree.children[0], - layout, - renderer, - ) - } - } -} - -impl<'a, Message, Renderer> Draggable for &Content<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: container::StyleSheet, -{ - fn can_be_dragged_at( - &self, - layout: Layout<'_>, - cursor_position: Point, - ) -> bool { - if let Some(title_bar) = &self.title_bar { - let mut children = layout.children(); - let title_bar_layout = children.next().unwrap(); - - title_bar.is_over_pick_area(title_bar_layout, cursor_position) - } else { - false - } - } -} - -impl<'a, T, Message, Renderer> From for Content<'a, Message, Renderer> -where - T: Into>, - Renderer: crate::Renderer, - Renderer::Theme: container::StyleSheet, -{ - fn from(element: T) -> Self { - Self::new(element) - } -} diff --git a/native/src/widget/pane_grid/direction.rs b/native/src/widget/pane_grid/direction.rs deleted file mode 100644 index b31a8737..00000000 --- a/native/src/widget/pane_grid/direction.rs +++ /dev/null @@ -1,12 +0,0 @@ -/// A four cardinal direction. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Direction { - /// ↑ - Up, - /// ↓ - Down, - /// ← - Left, - /// → - Right, -} diff --git a/native/src/widget/pane_grid/draggable.rs b/native/src/widget/pane_grid/draggable.rs deleted file mode 100644 index 6044871d..00000000 --- a/native/src/widget/pane_grid/draggable.rs +++ /dev/null @@ -1,12 +0,0 @@ -use crate::{Layout, Point}; - -/// A pane that can be dragged. -pub trait Draggable { - /// Returns whether the [`Draggable`] with the given [`Layout`] can be picked - /// at the provided cursor position. - fn can_be_dragged_at( - &self, - layout: Layout<'_>, - cursor_position: Point, - ) -> bool; -} diff --git a/native/src/widget/pane_grid/node.rs b/native/src/widget/pane_grid/node.rs deleted file mode 100644 index cc304b96..00000000 --- a/native/src/widget/pane_grid/node.rs +++ /dev/null @@ -1,250 +0,0 @@ -use crate::widget::pane_grid::{Axis, Pane, Split}; -use crate::{Rectangle, Size}; - -use std::collections::BTreeMap; - -/// A layout node of a [`PaneGrid`]. -/// -/// [`PaneGrid`]: crate::widget::PaneGrid -#[derive(Debug, Clone)] -pub enum Node { - /// The region of this [`Node`] is split into two. - Split { - /// The [`Split`] of this [`Node`]. - id: Split, - - /// The direction of the split. - axis: Axis, - - /// The ratio of the split in [0.0, 1.0]. - ratio: f32, - - /// The left/top [`Node`] of the split. - a: Box, - - /// The right/bottom [`Node`] of the split. - b: Box, - }, - /// The region of this [`Node`] is taken by a [`Pane`]. - Pane(Pane), -} - -impl Node { - /// Returns an iterator over each [`Split`] in this [`Node`]. - pub fn splits(&self) -> impl Iterator { - let mut unvisited_nodes = vec![self]; - - std::iter::from_fn(move || { - while let Some(node) = unvisited_nodes.pop() { - if let Node::Split { id, a, b, .. } = node { - unvisited_nodes.push(a); - unvisited_nodes.push(b); - - return Some(id); - } - } - - None - }) - } - - /// Returns the rectangular region for each [`Pane`] in the [`Node`] given - /// the spacing between panes and the total available space. - pub fn pane_regions( - &self, - spacing: f32, - size: Size, - ) -> BTreeMap { - let mut regions = BTreeMap::new(); - - self.compute_regions( - spacing, - &Rectangle { - x: 0.0, - y: 0.0, - width: size.width, - height: size.height, - }, - &mut regions, - ); - - regions - } - - /// Returns the axis, rectangular region, and ratio for each [`Split`] in - /// the [`Node`] given the spacing between panes and the total available - /// space. - pub fn split_regions( - &self, - spacing: f32, - size: Size, - ) -> BTreeMap { - let mut splits = BTreeMap::new(); - - self.compute_splits( - spacing, - &Rectangle { - x: 0.0, - y: 0.0, - width: size.width, - height: size.height, - }, - &mut splits, - ); - - splits - } - - pub(crate) fn find(&mut self, pane: &Pane) -> Option<&mut Node> { - match self { - Node::Split { a, b, .. } => { - a.find(pane).or_else(move || b.find(pane)) - } - Node::Pane(p) => { - if p == pane { - Some(self) - } else { - None - } - } - } - } - - pub(crate) fn split(&mut self, id: Split, axis: Axis, new_pane: Pane) { - *self = Node::Split { - id, - axis, - ratio: 0.5, - a: Box::new(self.clone()), - b: Box::new(Node::Pane(new_pane)), - }; - } - - pub(crate) fn update(&mut self, f: &impl Fn(&mut Node)) { - if let Node::Split { a, b, .. } = self { - a.update(f); - b.update(f); - } - - f(self); - } - - pub(crate) fn resize(&mut self, split: &Split, percentage: f32) -> bool { - match self { - Node::Split { - id, ratio, a, b, .. - } => { - if id == split { - *ratio = percentage; - - true - } else if a.resize(split, percentage) { - true - } else { - b.resize(split, percentage) - } - } - Node::Pane(_) => false, - } - } - - pub(crate) fn remove(&mut self, pane: &Pane) -> Option { - match self { - Node::Split { a, b, .. } => { - if a.pane() == Some(*pane) { - *self = *b.clone(); - Some(self.first_pane()) - } else if b.pane() == Some(*pane) { - *self = *a.clone(); - Some(self.first_pane()) - } else { - a.remove(pane).or_else(|| b.remove(pane)) - } - } - Node::Pane(_) => None, - } - } - - fn pane(&self) -> Option { - match self { - Node::Split { .. } => None, - Node::Pane(pane) => Some(*pane), - } - } - - fn first_pane(&self) -> Pane { - match self { - Node::Split { a, .. } => a.first_pane(), - Node::Pane(pane) => *pane, - } - } - - fn compute_regions( - &self, - spacing: f32, - current: &Rectangle, - regions: &mut BTreeMap, - ) { - match self { - Node::Split { - axis, ratio, a, b, .. - } => { - let (region_a, region_b) = axis.split(current, *ratio, spacing); - - a.compute_regions(spacing, ®ion_a, regions); - b.compute_regions(spacing, ®ion_b, regions); - } - Node::Pane(pane) => { - let _ = regions.insert(*pane, *current); - } - } - } - - fn compute_splits( - &self, - spacing: f32, - current: &Rectangle, - splits: &mut BTreeMap, - ) { - match self { - Node::Split { - axis, - ratio, - a, - b, - id, - } => { - let (region_a, region_b) = axis.split(current, *ratio, spacing); - - let _ = splits.insert(*id, (*axis, *current, *ratio)); - - a.compute_splits(spacing, ®ion_a, splits); - b.compute_splits(spacing, ®ion_b, splits); - } - Node::Pane(_) => {} - } - } -} - -impl std::hash::Hash for Node { - fn hash(&self, state: &mut H) { - match self { - Node::Split { - id, - axis, - ratio, - a, - b, - } => { - id.hash(state); - axis.hash(state); - ((ratio * 100_000.0) as u32).hash(state); - a.hash(state); - b.hash(state); - } - Node::Pane(pane) => { - pane.hash(state); - } - } - } -} diff --git a/native/src/widget/pane_grid/pane.rs b/native/src/widget/pane_grid/pane.rs deleted file mode 100644 index d6fbab83..00000000 --- a/native/src/widget/pane_grid/pane.rs +++ /dev/null @@ -1,5 +0,0 @@ -/// A rectangular region in a [`PaneGrid`] used to display widgets. -/// -/// [`PaneGrid`]: crate::widget::PaneGrid -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Pane(pub(super) usize); diff --git a/native/src/widget/pane_grid/split.rs b/native/src/widget/pane_grid/split.rs deleted file mode 100644 index 8132272a..00000000 --- a/native/src/widget/pane_grid/split.rs +++ /dev/null @@ -1,5 +0,0 @@ -/// A divider that splits a region in a [`PaneGrid`] into two different panes. -/// -/// [`PaneGrid`]: crate::widget::PaneGrid -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Split(pub(super) usize); diff --git a/native/src/widget/pane_grid/state.rs b/native/src/widget/pane_grid/state.rs deleted file mode 100644 index c4ae0a0e..00000000 --- a/native/src/widget/pane_grid/state.rs +++ /dev/null @@ -1,350 +0,0 @@ -//! The state of a [`PaneGrid`]. -//! -//! [`PaneGrid`]: crate::widget::PaneGrid -use crate::widget::pane_grid::{ - Axis, Configuration, Direction, Node, Pane, Split, -}; -use crate::{Point, Size}; - -use std::collections::HashMap; - -/// The state of a [`PaneGrid`]. -/// -/// It keeps track of the state of each [`Pane`] and the position of each -/// [`Split`]. -/// -/// The [`State`] needs to own any mutable contents a [`Pane`] may need. This is -/// why this struct is generic over the type `T`. Values of this type are -/// provided to the view function of [`PaneGrid::new`] for displaying each -/// [`Pane`]. -/// -/// [`PaneGrid`]: crate::widget::PaneGrid -/// [`PaneGrid::new`]: crate::widget::PaneGrid::new -#[derive(Debug, Clone)] -pub struct State { - /// The panes of the [`PaneGrid`]. - /// - /// [`PaneGrid`]: crate::widget::PaneGrid - pub panes: HashMap, - - /// The internal state of the [`PaneGrid`]. - /// - /// [`PaneGrid`]: crate::widget::PaneGrid - pub internal: Internal, - - /// The maximized [`Pane`] of the [`PaneGrid`]. - /// - /// [`PaneGrid`]: crate::widget::PaneGrid - pub(super) maximized: Option, -} - -impl State { - /// Creates a new [`State`], initializing the first pane with the provided - /// state. - /// - /// Alongside the [`State`], it returns the first [`Pane`] identifier. - pub fn new(first_pane_state: T) -> (Self, Pane) { - ( - Self::with_configuration(Configuration::Pane(first_pane_state)), - Pane(0), - ) - } - - /// Creates a new [`State`] with the given [`Configuration`]. - pub fn with_configuration(config: impl Into>) -> Self { - let mut panes = HashMap::new(); - - let internal = - Internal::from_configuration(&mut panes, config.into(), 0); - - State { - panes, - internal, - maximized: None, - } - } - - /// Returns the total amount of panes in the [`State`]. - pub fn len(&self) -> usize { - self.panes.len() - } - - /// Returns `true` if the amount of panes in the [`State`] is 0. - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - /// Returns the internal state of the given [`Pane`], if it exists. - pub fn get(&self, pane: &Pane) -> Option<&T> { - self.panes.get(pane) - } - - /// Returns the internal state of the given [`Pane`] with mutability, if it - /// exists. - pub fn get_mut(&mut self, pane: &Pane) -> Option<&mut T> { - self.panes.get_mut(pane) - } - - /// Returns an iterator over all the panes of the [`State`], alongside its - /// internal state. - pub fn iter(&self) -> impl Iterator { - self.panes.iter() - } - - /// Returns a mutable iterator over all the panes of the [`State`], - /// alongside its internal state. - pub fn iter_mut(&mut self) -> impl Iterator { - self.panes.iter_mut() - } - - /// Returns the layout of the [`State`]. - pub fn layout(&self) -> &Node { - &self.internal.layout - } - - /// Returns the adjacent [`Pane`] of another [`Pane`] in the given - /// direction, if there is one. - pub fn adjacent(&self, pane: &Pane, direction: Direction) -> Option { - let regions = self - .internal - .layout - .pane_regions(0.0, Size::new(4096.0, 4096.0)); - - let current_region = regions.get(pane)?; - - let target = match direction { - Direction::Left => { - Point::new(current_region.x - 1.0, current_region.y + 1.0) - } - Direction::Right => Point::new( - current_region.x + current_region.width + 1.0, - current_region.y + 1.0, - ), - Direction::Up => { - Point::new(current_region.x + 1.0, current_region.y - 1.0) - } - Direction::Down => Point::new( - current_region.x + 1.0, - current_region.y + current_region.height + 1.0, - ), - }; - - let mut colliding_regions = - regions.iter().filter(|(_, region)| region.contains(target)); - - let (pane, _) = colliding_regions.next()?; - - Some(*pane) - } - - /// Splits the given [`Pane`] into two in the given [`Axis`] and - /// initializing the new [`Pane`] with the provided internal state. - pub fn split( - &mut self, - axis: Axis, - pane: &Pane, - state: T, - ) -> Option<(Pane, Split)> { - let node = self.internal.layout.find(pane)?; - - let new_pane = { - self.internal.last_id = self.internal.last_id.checked_add(1)?; - - Pane(self.internal.last_id) - }; - - let new_split = { - self.internal.last_id = self.internal.last_id.checked_add(1)?; - - Split(self.internal.last_id) - }; - - node.split(new_split, axis, new_pane); - - let _ = self.panes.insert(new_pane, state); - let _ = self.maximized.take(); - - Some((new_pane, new_split)) - } - - /// Swaps the position of the provided panes in the [`State`]. - /// - /// If you want to swap panes on drag and drop in your [`PaneGrid`], you - /// will need to call this method when handling a [`DragEvent`]. - /// - /// [`PaneGrid`]: crate::widget::PaneGrid - /// [`DragEvent`]: crate::widget::pane_grid::DragEvent - pub fn swap(&mut self, a: &Pane, b: &Pane) { - self.internal.layout.update(&|node| match node { - Node::Split { .. } => {} - Node::Pane(pane) => { - if pane == a { - *node = Node::Pane(*b); - } else if pane == b { - *node = Node::Pane(*a); - } - } - }); - } - - /// Resizes two panes by setting the position of the provided [`Split`]. - /// - /// The ratio is a value in [0, 1], representing the exact position of a - /// [`Split`] between two panes. - /// - /// If you want to enable resize interactions in your [`PaneGrid`], you will - /// need to call this method when handling a [`ResizeEvent`]. - /// - /// [`PaneGrid`]: crate::widget::PaneGrid - /// [`ResizeEvent`]: crate::widget::pane_grid::ResizeEvent - pub fn resize(&mut self, split: &Split, ratio: f32) { - let _ = self.internal.layout.resize(split, ratio); - } - - /// Closes the given [`Pane`] and returns its internal state and its closest - /// sibling, if it exists. - pub fn close(&mut self, pane: &Pane) -> Option<(T, Pane)> { - if self.maximized == Some(*pane) { - let _ = self.maximized.take(); - } - - if let Some(sibling) = self.internal.layout.remove(pane) { - self.panes.remove(pane).map(|state| (state, sibling)) - } else { - None - } - } - - /// Maximize the given [`Pane`]. Only this pane will be rendered by the - /// [`PaneGrid`] until [`Self::restore()`] is called. - /// - /// [`PaneGrid`]: crate::widget::PaneGrid - pub fn maximize(&mut self, pane: &Pane) { - self.maximized = Some(*pane); - } - - /// Restore the currently maximized [`Pane`] to it's normal size. All panes - /// will be rendered by the [`PaneGrid`]. - /// - /// [`PaneGrid`]: crate::widget::PaneGrid - pub fn restore(&mut self) { - let _ = self.maximized.take(); - } - - /// Returns the maximized [`Pane`] of the [`PaneGrid`]. - /// - /// [`PaneGrid`]: crate::widget::PaneGrid - pub fn maximized(&self) -> Option { - self.maximized - } -} - -/// The internal state of a [`PaneGrid`]. -/// -/// [`PaneGrid`]: crate::widget::PaneGrid -#[derive(Debug, Clone)] -pub struct Internal { - layout: Node, - last_id: usize, -} - -impl Internal { - /// Initializes the [`Internal`] state of a [`PaneGrid`] from a - /// [`Configuration`]. - /// - /// [`PaneGrid`]: crate::widget::PaneGrid - pub fn from_configuration( - panes: &mut HashMap, - content: Configuration, - next_id: usize, - ) -> Self { - let (layout, last_id) = match content { - Configuration::Split { axis, ratio, a, b } => { - let Internal { - layout: a, - last_id: next_id, - .. - } = Self::from_configuration(panes, *a, next_id); - - let Internal { - layout: b, - last_id: next_id, - .. - } = Self::from_configuration(panes, *b, next_id); - - ( - Node::Split { - id: Split(next_id), - axis, - ratio, - a: Box::new(a), - b: Box::new(b), - }, - next_id + 1, - ) - } - Configuration::Pane(state) => { - let id = Pane(next_id); - let _ = panes.insert(id, state); - - (Node::Pane(id), next_id + 1) - } - }; - - Self { layout, last_id } - } -} - -/// The current action of a [`PaneGrid`]. -/// -/// [`PaneGrid`]: crate::widget::PaneGrid -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Action { - /// The [`PaneGrid`] is idle. - /// - /// [`PaneGrid`]: crate::widget::PaneGrid - Idle, - /// A [`Pane`] in the [`PaneGrid`] is being dragged. - /// - /// [`PaneGrid`]: crate::widget::PaneGrid - Dragging { - /// The [`Pane`] being dragged. - pane: Pane, - /// The starting [`Point`] of the drag interaction. - origin: Point, - }, - /// A [`Split`] in the [`PaneGrid`] is being dragged. - /// - /// [`PaneGrid`]: crate::widget::PaneGrid - Resizing { - /// The [`Split`] being dragged. - split: Split, - /// The [`Axis`] of the [`Split`]. - axis: Axis, - }, -} - -impl Action { - /// Returns the current [`Pane`] that is being dragged, if any. - pub fn picked_pane(&self) -> Option<(Pane, Point)> { - match *self { - Action::Dragging { pane, origin, .. } => Some((pane, origin)), - _ => None, - } - } - - /// Returns the current [`Split`] that is being dragged, if any. - pub fn picked_split(&self) -> Option<(Split, Axis)> { - match *self { - Action::Resizing { split, axis, .. } => Some((split, axis)), - _ => None, - } - } -} - -impl Internal { - /// The layout [`Node`] of the [`Internal`] state - pub fn layout(&self) -> &Node { - &self.layout - } -} diff --git a/native/src/widget/pane_grid/title_bar.rs b/native/src/widget/pane_grid/title_bar.rs deleted file mode 100644 index 107078ef..00000000 --- a/native/src/widget/pane_grid/title_bar.rs +++ /dev/null @@ -1,432 +0,0 @@ -use crate::event::{self, Event}; -use crate::layout; -use crate::mouse; -use crate::overlay; -use crate::renderer; -use crate::widget::container; -use crate::widget::{self, Tree}; -use crate::{ - Clipboard, Element, Layout, Padding, Point, Rectangle, Shell, Size, -}; - -/// The title bar of a [`Pane`]. -/// -/// [`Pane`]: crate::widget::pane_grid::Pane -#[allow(missing_debug_implementations)] -pub struct TitleBar<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: container::StyleSheet, -{ - content: Element<'a, Message, Renderer>, - controls: Option>, - padding: Padding, - always_show_controls: bool, - style: ::Style, -} - -impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: container::StyleSheet, -{ - /// Creates a new [`TitleBar`] with the given content. - pub fn new(content: E) -> Self - where - E: Into>, - { - Self { - content: content.into(), - controls: None, - padding: Padding::ZERO, - always_show_controls: false, - style: Default::default(), - } - } - - /// Sets the controls of the [`TitleBar`]. - pub fn controls( - mut self, - controls: impl Into>, - ) -> Self { - self.controls = Some(controls.into()); - self - } - - /// Sets the [`Padding`] of the [`TitleBar`]. - pub fn padding>(mut self, padding: P) -> Self { - self.padding = padding.into(); - self - } - - /// Sets the style of the [`TitleBar`]. - pub fn style( - mut self, - style: impl Into<::Style>, - ) -> Self { - self.style = style.into(); - self - } - - /// Sets whether or not the [`controls`] attached to this [`TitleBar`] are - /// always visible. - /// - /// By default, the controls are only visible when the [`Pane`] of this - /// [`TitleBar`] is hovered. - /// - /// [`controls`]: Self::controls - /// [`Pane`]: crate::widget::pane_grid::Pane - pub fn always_show_controls(mut self) -> Self { - self.always_show_controls = true; - self - } -} - -impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: container::StyleSheet, -{ - pub(super) fn state(&self) -> Tree { - let children = if let Some(controls) = self.controls.as_ref() { - vec![Tree::new(&self.content), Tree::new(controls)] - } else { - vec![Tree::new(&self.content), Tree::empty()] - }; - - Tree { - children, - ..Tree::empty() - } - } - - pub(super) fn diff(&self, tree: &mut Tree) { - if tree.children.len() == 2 { - if let Some(controls) = self.controls.as_ref() { - tree.children[1].diff(controls); - } - - tree.children[0].diff(&self.content); - } else { - *tree = self.state(); - } - } - - /// Draws the [`TitleBar`] with the provided [`Renderer`] and [`Layout`]. - /// - /// [`Renderer`]: crate::Renderer - pub fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - inherited_style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - show_controls: bool, - ) { - use container::StyleSheet; - - let bounds = layout.bounds(); - let style = theme.appearance(&self.style); - let inherited_style = renderer::Style { - text_color: style.text_color.unwrap_or(inherited_style.text_color), - }; - - container::draw_background(renderer, &style, bounds); - - let mut children = layout.children(); - let padded = children.next().unwrap(); - - let mut children = padded.children(); - let title_layout = children.next().unwrap(); - let mut show_title = true; - - if let Some(controls) = &self.controls { - if show_controls || self.always_show_controls { - let controls_layout = children.next().unwrap(); - if title_layout.bounds().width + controls_layout.bounds().width - > padded.bounds().width - { - show_title = false; - } - - controls.as_widget().draw( - &tree.children[1], - renderer, - theme, - &inherited_style, - controls_layout, - cursor_position, - viewport, - ); - } - } - - if show_title { - self.content.as_widget().draw( - &tree.children[0], - renderer, - theme, - &inherited_style, - title_layout, - cursor_position, - viewport, - ); - } - } - - /// Returns whether the mouse cursor is over the pick area of the - /// [`TitleBar`] or not. - /// - /// The whole [`TitleBar`] is a pick area, except its controls. - pub fn is_over_pick_area( - &self, - layout: Layout<'_>, - cursor_position: Point, - ) -> bool { - if layout.bounds().contains(cursor_position) { - let mut children = layout.children(); - let padded = children.next().unwrap(); - let mut children = padded.children(); - let title_layout = children.next().unwrap(); - - if self.controls.is_some() { - let controls_layout = children.next().unwrap(); - - if title_layout.bounds().width + controls_layout.bounds().width - > padded.bounds().width - { - !controls_layout.bounds().contains(cursor_position) - } else { - !controls_layout.bounds().contains(cursor_position) - && !title_layout.bounds().contains(cursor_position) - } - } else { - !title_layout.bounds().contains(cursor_position) - } - } else { - false - } - } - - pub(crate) fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let limits = limits.pad(self.padding); - let max_size = limits.max(); - - let title_layout = self - .content - .as_widget() - .layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); - - let title_size = title_layout.size(); - - let mut node = if let Some(controls) = &self.controls { - let mut controls_layout = controls - .as_widget() - .layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); - - let controls_size = controls_layout.size(); - let space_before_controls = max_size.width - controls_size.width; - - let height = title_size.height.max(controls_size.height); - - controls_layout.move_to(Point::new(space_before_controls, 0.0)); - - layout::Node::with_children( - Size::new(max_size.width, height), - vec![title_layout, controls_layout], - ) - } else { - layout::Node::with_children( - Size::new(max_size.width, title_size.height), - vec![title_layout], - ) - }; - - node.move_to(Point::new(self.padding.left, self.padding.top)); - - layout::Node::with_children(node.size().pad(self.padding), vec![node]) - } - - pub(crate) fn operate( - &self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn widget::Operation, - ) { - let mut children = layout.children(); - let padded = children.next().unwrap(); - - let mut children = padded.children(); - let title_layout = children.next().unwrap(); - let mut show_title = true; - - if let Some(controls) = &self.controls { - let controls_layout = children.next().unwrap(); - - if title_layout.bounds().width + controls_layout.bounds().width - > padded.bounds().width - { - show_title = false; - } - - controls.as_widget().operate( - &mut tree.children[1], - controls_layout, - renderer, - operation, - ) - }; - - if show_title { - self.content.as_widget().operate( - &mut tree.children[0], - title_layout, - renderer, - operation, - ) - } - } - - pub(crate) 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 { - let mut children = layout.children(); - let padded = children.next().unwrap(); - - let mut children = padded.children(); - let title_layout = children.next().unwrap(); - let mut show_title = true; - - let control_status = if let Some(controls) = &mut self.controls { - let controls_layout = children.next().unwrap(); - if title_layout.bounds().width + controls_layout.bounds().width - > padded.bounds().width - { - show_title = false; - } - - controls.as_widget_mut().on_event( - &mut tree.children[1], - event.clone(), - controls_layout, - cursor_position, - renderer, - clipboard, - shell, - ) - } else { - event::Status::Ignored - }; - - let title_status = if show_title { - self.content.as_widget_mut().on_event( - &mut tree.children[0], - event, - title_layout, - cursor_position, - renderer, - clipboard, - shell, - ) - } else { - event::Status::Ignored - }; - - control_status.merge(title_status) - } - - pub(crate) fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - let mut children = layout.children(); - let padded = children.next().unwrap(); - - let mut children = padded.children(); - let title_layout = children.next().unwrap(); - - let title_interaction = self.content.as_widget().mouse_interaction( - &tree.children[0], - title_layout, - cursor_position, - viewport, - renderer, - ); - - if let Some(controls) = &self.controls { - let controls_layout = children.next().unwrap(); - let controls_interaction = controls.as_widget().mouse_interaction( - &tree.children[1], - controls_layout, - cursor_position, - viewport, - renderer, - ); - - if title_layout.bounds().width + controls_layout.bounds().width - > padded.bounds().width - { - controls_interaction - } else { - controls_interaction.max(title_interaction) - } - } else { - title_interaction - } - } - - pub(crate) fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - let mut children = layout.children(); - let padded = children.next()?; - - let mut children = padded.children(); - let title_layout = children.next()?; - - let Self { - content, controls, .. - } = self; - - let mut states = tree.children.iter_mut(); - let title_state = states.next().unwrap(); - let controls_state = states.next().unwrap(); - - content - .as_widget_mut() - .overlay(title_state, title_layout, renderer) - .or_else(move || { - controls.as_mut().and_then(|controls| { - let controls_layout = children.next()?; - - controls.as_widget_mut().overlay( - controls_state, - controls_layout, - renderer, - ) - }) - }) - } -} diff --git a/native/src/widget/pick_list.rs b/native/src/widget/pick_list.rs deleted file mode 100644 index 8ff82f3b..00000000 --- a/native/src/widget/pick_list.rs +++ /dev/null @@ -1,657 +0,0 @@ -//! Display a dropdown list of selectable values. -use crate::alignment; -use crate::event::{self, Event}; -use crate::keyboard; -use crate::layout; -use crate::mouse; -use crate::overlay; -use crate::overlay::menu::{self, Menu}; -use crate::renderer; -use crate::text::{self, Text}; -use crate::touch; -use crate::widget::container; -use crate::widget::scrollable; -use crate::widget::tree::{self, Tree}; -use crate::{ - Clipboard, Element, Layout, Length, Padding, Pixels, Point, Rectangle, - Shell, Size, Widget, -}; -use std::borrow::Cow; - -pub use iced_style::pick_list::{Appearance, StyleSheet}; - -/// A widget for selecting a single value from a list of options. -#[allow(missing_debug_implementations)] -pub struct PickList<'a, T, Message, Renderer> -where - [T]: ToOwned>, - Renderer: text::Renderer, - Renderer::Theme: StyleSheet, -{ - on_selected: Box Message + 'a>, - options: Cow<'a, [T]>, - placeholder: Option, - selected: Option, - width: Length, - padding: Padding, - text_size: Option, - font: Option, - handle: Handle, - style: ::Style, -} - -impl<'a, T: 'a, Message, Renderer> PickList<'a, T, Message, Renderer> -where - T: ToString + Eq, - [T]: ToOwned>, - Renderer: text::Renderer, - Renderer::Theme: StyleSheet - + scrollable::StyleSheet - + menu::StyleSheet - + container::StyleSheet, - ::Style: - From<::Style>, -{ - /// The default padding of a [`PickList`]. - pub const DEFAULT_PADDING: Padding = Padding::new(5.0); - - /// Creates a new [`PickList`] with the given list of options, the current - /// selected value, and the message to produce when an option is selected. - pub fn new( - options: impl Into>, - selected: Option, - on_selected: impl Fn(T) -> Message + 'a, - ) -> Self { - Self { - on_selected: Box::new(on_selected), - options: options.into(), - placeholder: None, - selected, - width: Length::Shrink, - padding: Self::DEFAULT_PADDING, - text_size: None, - font: None, - handle: Default::default(), - style: Default::default(), - } - } - - /// Sets the placeholder of the [`PickList`]. - pub fn placeholder(mut self, placeholder: impl Into) -> Self { - self.placeholder = Some(placeholder.into()); - self - } - - /// Sets the width of the [`PickList`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the [`Padding`] of the [`PickList`]. - pub fn padding>(mut self, padding: P) -> Self { - self.padding = padding.into(); - self - } - - /// Sets the text size of the [`PickList`]. - pub fn text_size(mut self, size: impl Into) -> Self { - self.text_size = Some(size.into().0); - self - } - - /// Sets the font of the [`PickList`]. - pub fn font(mut self, font: impl Into) -> Self { - self.font = Some(font.into()); - self - } - - /// Sets the [`Handle`] of the [`PickList`]. - pub fn handle(mut self, handle: Handle) -> Self { - self.handle = handle; - self - } - - /// Sets the style of the [`PickList`]. - pub fn style( - mut self, - style: impl Into<::Style>, - ) -> Self { - self.style = style.into(); - self - } -} - -impl<'a, T: 'a, Message, Renderer> Widget - for PickList<'a, T, Message, Renderer> -where - T: Clone + ToString + Eq + 'static, - [T]: ToOwned>, - Message: 'a, - Renderer: text::Renderer + 'a, - Renderer::Theme: StyleSheet - + scrollable::StyleSheet - + menu::StyleSheet - + container::StyleSheet, - ::Style: - From<::Style>, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::>() - } - - fn state(&self) -> tree::State { - tree::State::new(State::::new()) - } - - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - Length::Shrink - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - layout( - renderer, - limits, - self.width, - self.padding, - self.text_size, - self.font, - self.placeholder.as_deref(), - &self.options, - ) - } - - 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 { - update( - event, - layout, - cursor_position, - shell, - self.on_selected.as_ref(), - self.selected.as_ref(), - &self.options, - || tree.state.downcast_mut::>(), - ) - } - - fn mouse_interaction( - &self, - _tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - mouse_interaction(layout, cursor_position) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - let font = self.font.unwrap_or_else(|| renderer.default_font()); - draw( - renderer, - theme, - layout, - cursor_position, - self.padding, - self.text_size, - font, - self.placeholder.as_deref(), - self.selected.as_ref(), - &self.handle, - &self.style, - || tree.state.downcast_ref::>(), - ) - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - let state = tree.state.downcast_mut::>(); - - overlay( - layout, - state, - self.padding, - self.text_size, - self.font.unwrap_or_else(|| renderer.default_font()), - &self.options, - self.style.clone(), - ) - } -} - -impl<'a, T: 'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - T: Clone + ToString + Eq + 'static, - [T]: ToOwned>, - Message: 'a, - Renderer: text::Renderer + 'a, - Renderer::Theme: StyleSheet - + scrollable::StyleSheet - + menu::StyleSheet - + container::StyleSheet, - ::Style: - From<::Style>, -{ - fn from(pick_list: PickList<'a, T, Message, Renderer>) -> Self { - Self::new(pick_list) - } -} - -/// The local state of a [`PickList`]. -#[derive(Debug)] -pub struct State { - menu: menu::State, - keyboard_modifiers: keyboard::Modifiers, - is_open: bool, - hovered_option: Option, - last_selection: Option, -} - -impl State { - /// Creates a new [`State`] for a [`PickList`]. - pub fn new() -> Self { - Self { - menu: menu::State::default(), - keyboard_modifiers: keyboard::Modifiers::default(), - is_open: bool::default(), - hovered_option: Option::default(), - last_selection: Option::default(), - } - } -} - -impl Default for State { - fn default() -> Self { - Self::new() - } -} - -/// The handle to the right side of the [`PickList`]. -#[derive(Debug, Clone, PartialEq)] -pub enum Handle { - /// Displays an arrow icon (▼). - /// - /// This is the default. - Arrow { - /// Font size of the content. - size: Option, - }, - /// A custom static handle. - Static(Icon), - /// A custom dynamic handle. - Dynamic { - /// The [`Icon`] used when [`PickList`] is closed. - closed: Icon, - /// The [`Icon`] used when [`PickList`] is open. - open: Icon, - }, - /// No handle will be shown. - None, -} - -impl Default for Handle { - fn default() -> Self { - Self::Arrow { size: None } - } -} - -/// The icon of a [`Handle`]. -#[derive(Debug, Clone, PartialEq)] -pub struct Icon { - /// Font that will be used to display the `code_point`, - pub font: Font, - /// The unicode code point that will be used as the icon. - pub code_point: char, - /// Font size of the content. - pub size: Option, -} - -/// Computes the layout of a [`PickList`]. -pub fn layout( - renderer: &Renderer, - limits: &layout::Limits, - width: Length, - padding: Padding, - text_size: Option, - font: Option, - placeholder: Option<&str>, - options: &[T], -) -> layout::Node -where - Renderer: text::Renderer, - T: ToString, -{ - use std::f32; - - let limits = limits.width(width).height(Length::Shrink).pad(padding); - let text_size = text_size.unwrap_or_else(|| renderer.default_size()); - - let max_width = match width { - Length::Shrink => { - let measure = |label: &str| -> f32 { - let (width, _) = renderer.measure( - label, - text_size, - font.unwrap_or_else(|| renderer.default_font()), - Size::new(f32::INFINITY, f32::INFINITY), - ); - - width.round() - }; - - let labels = options.iter().map(ToString::to_string); - - let labels_width = labels - .map(|label| measure(&label)) - .fold(100.0, |candidate, current| current.max(candidate)); - - let placeholder_width = placeholder.map(measure).unwrap_or(100.0); - - labels_width.max(placeholder_width) - } - _ => 0.0, - }; - - let size = { - let intrinsic = - Size::new(max_width + text_size + padding.left, text_size * 1.2); - - limits.resolve(intrinsic).pad(padding) - }; - - layout::Node::new(size) -} - -/// Processes an [`Event`] and updates the [`State`] of a [`PickList`] -/// accordingly. -pub fn update<'a, T, Message>( - event: Event, - layout: Layout<'_>, - cursor_position: Point, - shell: &mut Shell<'_, Message>, - on_selected: &dyn Fn(T) -> Message, - selected: Option<&T>, - options: &[T], - state: impl FnOnce() -> &'a mut State, -) -> event::Status -where - T: PartialEq + Clone + 'a, -{ - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let state = state(); - - let event_status = if state.is_open { - // Event wasn't processed by overlay, so cursor was clicked either outside it's - // bounds or on the drop-down, either way we close the overlay. - state.is_open = false; - - event::Status::Captured - } else if layout.bounds().contains(cursor_position) { - state.is_open = true; - state.hovered_option = - options.iter().position(|option| Some(option) == selected); - - event::Status::Captured - } else { - event::Status::Ignored - }; - - if let Some(last_selection) = state.last_selection.take() { - shell.publish((on_selected)(last_selection)); - - state.is_open = false; - - event::Status::Captured - } else { - event_status - } - } - Event::Mouse(mouse::Event::WheelScrolled { - delta: mouse::ScrollDelta::Lines { y, .. }, - }) => { - let state = state(); - - if state.keyboard_modifiers.command() - && layout.bounds().contains(cursor_position) - && !state.is_open - { - fn find_next<'a, T: PartialEq>( - selected: &'a T, - mut options: impl Iterator, - ) -> Option<&'a T> { - let _ = options.find(|&option| option == selected); - - options.next() - } - - let next_option = if y < 0.0 { - if let Some(selected) = selected { - find_next(selected, options.iter()) - } else { - options.first() - } - } else if y > 0.0 { - if let Some(selected) = selected { - find_next(selected, options.iter().rev()) - } else { - options.last() - } - } else { - None - }; - - if let Some(next_option) = next_option { - shell.publish((on_selected)(next_option.clone())); - } - - event::Status::Captured - } else { - event::Status::Ignored - } - } - Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { - let state = state(); - - state.keyboard_modifiers = modifiers; - - event::Status::Ignored - } - _ => event::Status::Ignored, - } -} - -/// Returns the current [`mouse::Interaction`] of a [`PickList`]. -pub fn mouse_interaction( - layout: Layout<'_>, - cursor_position: Point, -) -> mouse::Interaction { - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); - - if is_mouse_over { - mouse::Interaction::Pointer - } else { - mouse::Interaction::default() - } -} - -/// Returns the current overlay of a [`PickList`]. -pub fn overlay<'a, T, Message, Renderer>( - layout: Layout<'_>, - state: &'a mut State, - padding: Padding, - text_size: Option, - font: Renderer::Font, - options: &'a [T], - style: ::Style, -) -> Option> -where - T: Clone + ToString, - Message: 'a, - Renderer: text::Renderer + 'a, - Renderer::Theme: StyleSheet - + scrollable::StyleSheet - + menu::StyleSheet - + container::StyleSheet, - ::Style: - From<::Style>, -{ - if state.is_open { - let bounds = layout.bounds(); - - let mut menu = Menu::new( - &mut state.menu, - options, - &mut state.hovered_option, - &mut state.last_selection, - ) - .width(bounds.width) - .padding(padding) - .font(font) - .style(style); - - if let Some(text_size) = text_size { - menu = menu.text_size(text_size); - } - - Some(menu.overlay(layout.position(), bounds.height)) - } else { - None - } -} - -/// Draws a [`PickList`]. -pub fn draw<'a, T, Renderer>( - renderer: &mut Renderer, - theme: &Renderer::Theme, - layout: Layout<'_>, - cursor_position: Point, - padding: Padding, - text_size: Option, - font: Renderer::Font, - placeholder: Option<&str>, - selected: Option<&T>, - handle: &Handle, - style: &::Style, - state: impl FnOnce() -> &'a State, -) where - Renderer: text::Renderer, - Renderer::Theme: StyleSheet, - T: ToString + 'a, -{ - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); - let is_selected = selected.is_some(); - - let style = if is_mouse_over { - theme.hovered(style) - } else { - theme.active(style) - }; - - renderer.fill_quad( - renderer::Quad { - bounds, - border_color: style.border_color, - border_width: style.border_width, - border_radius: style.border_radius.into(), - }, - style.background, - ); - - let handle = match handle { - Handle::Arrow { size } => { - Some((Renderer::ICON_FONT, Renderer::ARROW_DOWN_ICON, *size)) - } - Handle::Static(Icon { - font, - code_point, - size, - }) => Some((*font, *code_point, *size)), - Handle::Dynamic { open, closed } => { - if state().is_open { - Some((open.font, open.code_point, open.size)) - } else { - Some((closed.font, closed.code_point, closed.size)) - } - } - Handle::None => None, - }; - - if let Some((font, code_point, size)) = handle { - let size = size.unwrap_or_else(|| renderer.default_size()); - - renderer.fill_text(Text { - content: &code_point.to_string(), - size, - font, - color: style.handle_color, - bounds: Rectangle { - x: bounds.x + bounds.width - padding.horizontal(), - y: bounds.center_y(), - height: size * 1.2, - ..bounds - }, - horizontal_alignment: alignment::Horizontal::Right, - vertical_alignment: alignment::Vertical::Center, - }); - } - - let label = selected.map(ToString::to_string); - - if let Some(label) = label.as_deref().or(placeholder) { - let text_size = text_size.unwrap_or_else(|| renderer.default_size()); - - renderer.fill_text(Text { - content: label, - size: text_size, - font, - color: if is_selected { - style.text_color - } else { - style.placeholder_color - }, - bounds: Rectangle { - x: bounds.x + padding.left, - y: bounds.center_y(), - width: bounds.width - padding.horizontal(), - height: text_size * 1.2, - }, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, - }); - } -} diff --git a/native/src/widget/progress_bar.rs b/native/src/widget/progress_bar.rs deleted file mode 100644 index dd46fa76..00000000 --- a/native/src/widget/progress_bar.rs +++ /dev/null @@ -1,168 +0,0 @@ -//! Provide progress feedback to your users. -use crate::layout; -use crate::renderer; -use crate::widget::Tree; -use crate::{Color, Element, Layout, Length, Point, Rectangle, Size, Widget}; - -use std::ops::RangeInclusive; - -pub use iced_style::progress_bar::{Appearance, StyleSheet}; - -/// A bar that displays progress. -/// -/// # Example -/// ``` -/// # type ProgressBar = iced_native::widget::ProgressBar; -/// let value = 50.0; -/// -/// ProgressBar::new(0.0..=100.0, value); -/// ``` -/// -/// ![Progress bar drawn with `iced_wgpu`](https://user-images.githubusercontent.com/18618951/71662391-a316c200-2d51-11ea-9cef-52758cab85e3.png) -#[allow(missing_debug_implementations)] -pub struct ProgressBar -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - range: RangeInclusive, - value: f32, - width: Length, - height: Option, - style: ::Style, -} - -impl ProgressBar -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - /// The default height of a [`ProgressBar`]. - pub const DEFAULT_HEIGHT: f32 = 30.0; - - /// Creates a new [`ProgressBar`]. - /// - /// It expects: - /// * an inclusive range of possible values - /// * the current value of the [`ProgressBar`] - pub fn new(range: RangeInclusive, value: f32) -> Self { - ProgressBar { - value: value.clamp(*range.start(), *range.end()), - range, - width: Length::Fill, - height: None, - style: Default::default(), - } - } - - /// Sets the width of the [`ProgressBar`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the height of the [`ProgressBar`]. - pub fn height(mut self, height: impl Into) -> Self { - self.height = Some(height.into()); - self - } - - /// Sets the style of the [`ProgressBar`]. - pub fn style( - mut self, - style: impl Into<::Style>, - ) -> Self { - self.style = style.into(); - self - } -} - -impl Widget for ProgressBar -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT)) - } - - fn layout( - &self, - _renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let limits = limits - .width(self.width) - .height(self.height.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT))); - - let size = limits.resolve(Size::ZERO); - - layout::Node::new(size) - } - - fn draw( - &self, - _state: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - _cursor_position: Point, - _viewport: &Rectangle, - ) { - let bounds = layout.bounds(); - let (range_start, range_end) = self.range.clone().into_inner(); - - let active_progress_width = if range_start >= range_end { - 0.0 - } else { - bounds.width * (self.value - range_start) - / (range_end - range_start) - }; - - let style = theme.appearance(&self.style); - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { ..bounds }, - border_radius: style.border_radius.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - style.background, - ); - - if active_progress_width > 0.0 { - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - width: active_progress_width, - ..bounds - }, - border_radius: style.border_radius.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - style.bar, - ); - } - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Message: 'a, - Renderer: 'a + crate::Renderer, - Renderer::Theme: StyleSheet, -{ - fn from( - progress_bar: ProgressBar, - ) -> Element<'a, Message, Renderer> { - Element::new(progress_bar) - } -} diff --git a/native/src/widget/radio.rs b/native/src/widget/radio.rs deleted file mode 100644 index 5f60eaef..00000000 --- a/native/src/widget/radio.rs +++ /dev/null @@ -1,299 +0,0 @@ -//! Create choices using radio buttons. -use crate::alignment; -use crate::event::{self, Event}; -use crate::layout; -use crate::mouse; -use crate::renderer; -use crate::text; -use crate::touch; -use crate::widget::{self, Row, Text, Tree}; -use crate::{ - Alignment, Clipboard, Color, Element, Layout, Length, Pixels, Point, - Rectangle, Shell, Widget, -}; - -pub use iced_style::radio::{Appearance, StyleSheet}; - -/// A circular button representing a choice. -/// -/// # Example -/// ``` -/// # type Radio = -/// # iced_native::widget::Radio; -/// # -/// #[derive(Debug, Clone, Copy, PartialEq, Eq)] -/// pub enum Choice { -/// A, -/// B, -/// } -/// -/// #[derive(Debug, Clone, Copy)] -/// pub enum Message { -/// RadioSelected(Choice), -/// } -/// -/// let selected_choice = Some(Choice::A); -/// -/// Radio::new(Choice::A, "This is A", selected_choice, Message::RadioSelected); -/// -/// Radio::new(Choice::B, "This is B", selected_choice, Message::RadioSelected); -/// ``` -/// -/// ![Radio buttons drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/radio.png?raw=true) -#[allow(missing_debug_implementations)] -pub struct Radio -where - Renderer: text::Renderer, - Renderer::Theme: StyleSheet, -{ - is_selected: bool, - on_click: Message, - label: String, - width: Length, - size: f32, - spacing: f32, - text_size: Option, - font: Option, - style: ::Style, -} - -impl Radio -where - Message: Clone, - Renderer: text::Renderer, - Renderer::Theme: StyleSheet, -{ - /// The default size of a [`Radio`] button. - pub const DEFAULT_SIZE: f32 = 28.0; - - /// The default spacing of a [`Radio`] button. - pub const DEFAULT_SPACING: f32 = 15.0; - - /// Creates a new [`Radio`] button. - /// - /// It expects: - /// * the value related to the [`Radio`] button - /// * the label of the [`Radio`] button - /// * the current selected value - /// * a function that will be called when the [`Radio`] is selected. It - /// receives the value of the radio and must produce a `Message`. - pub fn new( - value: V, - label: impl Into, - selected: Option, - f: F, - ) -> Self - where - V: Eq + Copy, - F: FnOnce(V) -> Message, - { - Radio { - is_selected: Some(value) == selected, - on_click: f(value), - label: label.into(), - width: Length::Shrink, - size: Self::DEFAULT_SIZE, - spacing: Self::DEFAULT_SPACING, //15 - text_size: None, - font: None, - style: Default::default(), - } - } - - /// Sets the size of the [`Radio`] button. - pub fn size(mut self, size: impl Into) -> Self { - self.size = size.into().0; - self - } - - /// Sets the width of the [`Radio`] button. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the spacing between the [`Radio`] button and the text. - pub fn spacing(mut self, spacing: impl Into) -> Self { - self.spacing = spacing.into().0; - self - } - - /// Sets the text size of the [`Radio`] button. - pub fn text_size(mut self, text_size: impl Into) -> Self { - self.text_size = Some(text_size.into().0); - self - } - - /// Sets the text font of the [`Radio`] button. - pub fn font(mut self, font: impl Into) -> Self { - self.font = Some(font.into()); - self - } - - /// Sets the style of the [`Radio`] button. - pub fn style( - mut self, - style: impl Into<::Style>, - ) -> Self { - self.style = style.into(); - self - } -} - -impl Widget for Radio -where - Message: Clone, - Renderer: text::Renderer, - Renderer::Theme: StyleSheet + widget::text::StyleSheet, -{ - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - Length::Shrink - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - Row::<(), Renderer>::new() - .width(self.width) - .spacing(self.spacing) - .align_items(Alignment::Center) - .push(Row::new().width(self.size).height(self.size)) - .push(Text::new(&self.label).width(self.width).size( - self.text_size.unwrap_or_else(|| renderer.default_size()), - )) - .layout(renderer, limits) - } - - fn on_event( - &mut self, - _state: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - if layout.bounds().contains(cursor_position) { - shell.publish(self.on_click.clone()); - - return event::Status::Captured; - } - } - _ => {} - } - - event::Status::Ignored - } - - fn mouse_interaction( - &self, - _state: &Tree, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - if layout.bounds().contains(cursor_position) { - mouse::Interaction::Pointer - } else { - mouse::Interaction::default() - } - } - - fn draw( - &self, - _state: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); - - let mut children = layout.children(); - - let custom_style = if is_mouse_over { - theme.hovered(&self.style, self.is_selected) - } else { - theme.active(&self.style, self.is_selected) - }; - - { - let layout = children.next().unwrap(); - let bounds = layout.bounds(); - - let size = bounds.width; - let dot_size = size / 2.0; - - renderer.fill_quad( - renderer::Quad { - bounds, - border_radius: (size / 2.0).into(), - border_width: custom_style.border_width, - border_color: custom_style.border_color, - }, - custom_style.background, - ); - - if self.is_selected { - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x + dot_size / 2.0, - y: bounds.y + dot_size / 2.0, - width: bounds.width - dot_size, - height: bounds.height - dot_size, - }, - border_radius: (dot_size / 2.0).into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - custom_style.dot_color, - ); - } - } - - { - let label_layout = children.next().unwrap(); - - widget::text::draw( - renderer, - style, - label_layout, - &self.label, - self.text_size, - self.font, - widget::text::Appearance { - color: custom_style.text_color, - }, - alignment::Horizontal::Left, - alignment::Vertical::Center, - ); - } - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Message: 'a + Clone, - Renderer: 'a + text::Renderer, - Renderer::Theme: StyleSheet + widget::text::StyleSheet, -{ - fn from(radio: Radio) -> Element<'a, Message, Renderer> { - Element::new(radio) - } -} diff --git a/native/src/widget/row.rs b/native/src/widget/row.rs deleted file mode 100644 index 286c1c2d..00000000 --- a/native/src/widget/row.rs +++ /dev/null @@ -1,253 +0,0 @@ -//! Distribute content horizontally. -use crate::event::{self, Event}; -use crate::layout::{self, Layout}; -use crate::mouse; -use crate::overlay; -use crate::renderer; -use crate::widget::{Operation, Tree}; -use crate::{ - Alignment, Clipboard, Element, Length, Padding, Pixels, Point, Rectangle, - Shell, Widget, -}; - -/// A container that distributes its contents horizontally. -#[allow(missing_debug_implementations)] -pub struct Row<'a, Message, Renderer> { - spacing: f32, - padding: Padding, - width: Length, - height: Length, - align_items: Alignment, - children: Vec>, -} - -impl<'a, Message, Renderer> Row<'a, Message, Renderer> { - /// Creates an empty [`Row`]. - pub fn new() -> Self { - Self::with_children(Vec::new()) - } - - /// Creates a [`Row`] with the given elements. - pub fn with_children( - children: Vec>, - ) -> Self { - Row { - spacing: 0.0, - padding: Padding::ZERO, - width: Length::Shrink, - height: Length::Shrink, - align_items: Alignment::Start, - children, - } - } - - /// Sets the horizontal spacing _between_ elements. - /// - /// Custom margins per element do not exist in iced. You should use this - /// method instead! While less flexible, it helps you keep spacing between - /// elements consistent. - pub fn spacing(mut self, amount: impl Into) -> Self { - self.spacing = amount.into().0; - self - } - - /// Sets the [`Padding`] of the [`Row`]. - pub fn padding>(mut self, padding: P) -> Self { - self.padding = padding.into(); - self - } - - /// Sets the width of the [`Row`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the height of the [`Row`]. - pub fn height(mut self, height: impl Into) -> Self { - self.height = height.into(); - self - } - - /// Sets the vertical alignment of the contents of the [`Row`] . - pub fn align_items(mut self, align: Alignment) -> Self { - self.align_items = align; - self - } - - /// Adds an [`Element`] to the [`Row`]. - pub fn push( - mut self, - child: impl Into>, - ) -> Self { - self.children.push(child.into()); - self - } -} - -impl<'a, Message, Renderer> Default for Row<'a, Message, Renderer> { - fn default() -> Self { - Self::new() - } -} - -impl<'a, Message, Renderer> Widget - for Row<'a, Message, Renderer> -where - Renderer: crate::Renderer, -{ - fn children(&self) -> Vec { - self.children.iter().map(Tree::new).collect() - } - - fn diff(&self, tree: &mut Tree) { - tree.diff_children(&self.children) - } - - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - - layout::flex::resolve( - layout::flex::Axis::Horizontal, - renderer, - &limits, - self.padding, - self.spacing, - self.align_items, - &self.children, - ) - } - - fn operate( - &self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn Operation, - ) { - operation.container(None, &mut |operation| { - self.children - .iter() - .zip(&mut tree.children) - .zip(layout.children()) - .for_each(|((child, state), layout)| { - child - .as_widget() - .operate(state, layout, renderer, 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 { - self.children - .iter_mut() - .zip(&mut tree.children) - .zip(layout.children()) - .map(|((child, state), layout)| { - child.as_widget_mut().on_event( - state, - event.clone(), - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - }) - .fold(event::Status::Ignored, event::Status::merge) - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.children - .iter() - .zip(&tree.children) - .zip(layout.children()) - .map(|((child, state), layout)| { - child.as_widget().mouse_interaction( - state, - layout, - cursor_position, - viewport, - renderer, - ) - }) - .max() - .unwrap_or_default() - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - for ((child, state), layout) in self - .children - .iter() - .zip(&tree.children) - .zip(layout.children()) - { - child.as_widget().draw( - state, - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ); - } - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - overlay::from_children(&mut self.children, tree, layout, renderer) - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Message: 'a, - Renderer: crate::Renderer + 'a, -{ - fn from(row: Row<'a, Message, Renderer>) -> Self { - Self::new(row) - } -} diff --git a/native/src/widget/rule.rs b/native/src/widget/rule.rs deleted file mode 100644 index 1ab6a0d3..00000000 --- a/native/src/widget/rule.rs +++ /dev/null @@ -1,147 +0,0 @@ -//! Display a horizontal or vertical rule for dividing content. -use crate::layout; -use crate::renderer; -use crate::widget::Tree; -use crate::{ - Color, Element, Layout, Length, Pixels, Point, Rectangle, Size, Widget, -}; - -pub use iced_style::rule::{Appearance, FillMode, StyleSheet}; - -/// Display a horizontal or vertical rule for dividing content. -#[allow(missing_debug_implementations)] -pub struct Rule -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - width: Length, - height: Length, - is_horizontal: bool, - style: ::Style, -} - -impl Rule -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - /// Creates a horizontal [`Rule`] with the given height. - pub fn horizontal(height: impl Into) -> Self { - Rule { - width: Length::Fill, - height: Length::Fixed(height.into().0), - is_horizontal: true, - style: Default::default(), - } - } - - /// Creates a vertical [`Rule`] with the given width. - pub fn vertical(width: impl Into) -> Self { - Rule { - width: Length::Fixed(width.into().0), - height: Length::Fill, - is_horizontal: false, - style: Default::default(), - } - } - - /// Sets the style of the [`Rule`]. - pub fn style( - mut self, - style: impl Into<::Style>, - ) -> Self { - self.style = style.into(); - self - } -} - -impl Widget for Rule -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height - } - - fn layout( - &self, - _renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - - layout::Node::new(limits.resolve(Size::ZERO)) - } - - fn draw( - &self, - _state: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - _cursor_position: Point, - _viewport: &Rectangle, - ) { - let bounds = layout.bounds(); - let style = theme.appearance(&self.style); - - let bounds = if self.is_horizontal { - let line_y = (bounds.y + (bounds.height / 2.0) - - (style.width as f32 / 2.0)) - .round(); - - let (offset, line_width) = style.fill_mode.fill(bounds.width); - let line_x = bounds.x + offset; - - Rectangle { - x: line_x, - y: line_y, - width: line_width, - height: style.width as f32, - } - } else { - let line_x = (bounds.x + (bounds.width / 2.0) - - (style.width as f32 / 2.0)) - .round(); - - let (offset, line_height) = style.fill_mode.fill(bounds.height); - let line_y = bounds.y + offset; - - Rectangle { - x: line_x, - y: line_y, - width: style.width as f32, - height: line_height, - } - }; - - renderer.fill_quad( - renderer::Quad { - bounds, - border_radius: style.radius.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - style.color, - ); - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Message: 'a, - Renderer: 'a + crate::Renderer, - Renderer::Theme: StyleSheet, -{ - fn from(rule: Rule) -> Element<'a, Message, Renderer> { - Element::new(rule) - } -} diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs deleted file mode 100644 index c1df8c39..00000000 --- a/native/src/widget/scrollable.rs +++ /dev/null @@ -1,1327 +0,0 @@ -//! Navigate an endless amount of content with a scrollbar. -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::{ - Background, Clipboard, Color, Command, Element, Layout, Length, Pixels, - Point, Rectangle, Shell, Size, Vector, Widget, -}; - -pub use iced_style::scrollable::StyleSheet; -pub use operation::scrollable::RelativeOffset; - -pub mod style { - //! The styles of a [`Scrollable`]. - //! - //! [`Scrollable`]: crate::widget::Scrollable - pub use iced_style::scrollable::{Scrollbar, Scroller}; -} - -/// A widget that can vertically display an infinite amount of content with a -/// scrollbar. -#[allow(missing_debug_implementations)] -pub struct Scrollable<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - id: Option, - height: Length, - vertical: Properties, - horizontal: Option, - content: Element<'a, Message, Renderer>, - on_scroll: Option Message + 'a>>, - style: ::Style, -} - -impl<'a, Message, Renderer> Scrollable<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - /// Creates a new [`Scrollable`]. - pub fn new(content: impl Into>) -> Self { - Scrollable { - id: None, - height: Length::Shrink, - vertical: Properties::default(), - horizontal: None, - content: content.into(), - on_scroll: None, - style: Default::default(), - } - } - - /// Sets the [`Id`] of the [`Scrollable`]. - pub fn id(mut self, id: Id) -> Self { - self.id = Some(id); - self - } - - /// Sets the height of the [`Scrollable`]. - pub fn height(mut self, height: impl Into) -> Self { - self.height = height.into(); - self - } - - /// Configures the vertical scrollbar of the [`Scrollable`] . - pub fn vertical_scroll(mut self, properties: Properties) -> Self { - self.vertical = properties; - self - } - - /// Configures the horizontal scrollbar of the [`Scrollable`] . - pub fn horizontal_scroll(mut self, properties: Properties) -> Self { - self.horizontal = Some(properties); - self - } - - /// Sets a function to call when the [`Scrollable`] is scrolled. - /// - /// The function takes the new relative x & y offset of the [`Scrollable`] - /// (e.g. `0` means beginning, while `1` means end). - pub fn on_scroll( - mut self, - f: impl Fn(RelativeOffset) -> Message + 'a, - ) -> Self { - self.on_scroll = Some(Box::new(f)); - self - } - - /// Sets the style of the [`Scrollable`] . - pub fn style( - mut self, - style: impl Into<::Style>, - ) -> Self { - self.style = style.into(); - self - } -} - -/// Properties of a scrollbar within a [`Scrollable`]. -#[derive(Debug)] -pub struct Properties { - width: f32, - margin: f32, - scroller_width: f32, -} - -impl Default for Properties { - fn default() -> Self { - Self { - width: 10.0, - margin: 0.0, - scroller_width: 10.0, - } - } -} - -impl Properties { - /// Creates new [`Properties`] for use in a [`Scrollable`]. - pub fn new() -> Self { - Self::default() - } - - /// Sets the scrollbar width of the [`Scrollable`] . - /// Silently enforces a minimum width of 1. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into().0.max(1.0); - self - } - - /// Sets the scrollbar margin of the [`Scrollable`] . - pub fn margin(mut self, margin: impl Into) -> Self { - self.margin = margin.into().0; - self - } - - /// Sets the scroller width of the [`Scrollable`] . - /// Silently enforces a minimum width of 1. - pub fn scroller_width(mut self, scroller_width: impl Into) -> Self { - self.scroller_width = scroller_width.into().0.max(1.0); - self - } -} - -impl<'a, Message, Renderer> Widget - for Scrollable<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(State::new()) - } - - 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.height - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - layout( - renderer, - limits, - Widget::::width(self), - self.height, - self.horizontal.is_some(), - |renderer, limits| { - self.content.as_widget().layout(renderer, limits) - }, - ) - } - - fn operate( - &self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn Operation, - ) { - let state = tree.state.downcast_mut::(); - - operation.scrollable(state, self.id.as_ref().map(|id| &id.0)); - - operation.container( - self.id.as_ref().map(|id| &id.0), - &mut |operation| { - self.content.as_widget().operate( - &mut tree.children[0], - layout.children().next().unwrap(), - renderer, - operation, - ); - }, - ); - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - update( - tree.state.downcast_mut::(), - event, - layout, - cursor_position, - clipboard, - shell, - &self.vertical, - self.horizontal.as_ref(), - &self.on_scroll, - |event, layout, cursor_position, clipboard, shell| { - self.content.as_widget_mut().on_event( - &mut tree.children[0], - event, - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - }, - ) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - draw( - tree.state.downcast_ref::(), - renderer, - theme, - layout, - cursor_position, - &self.vertical, - self.horizontal.as_ref(), - &self.style, - |renderer, layout, cursor_position, viewport| { - self.content.as_widget().draw( - &tree.children[0], - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ) - }, - ) - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - mouse_interaction( - tree.state.downcast_ref::(), - layout, - cursor_position, - &self.vertical, - self.horizontal.as_ref(), - |layout, cursor_position, viewport| { - self.content.as_widget().mouse_interaction( - &tree.children[0], - layout, - cursor_position, - viewport, - renderer, - ) - }, - ) - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - self.content - .as_widget_mut() - .overlay( - &mut tree.children[0], - layout.children().next().unwrap(), - renderer, - ) - .map(|overlay| { - let bounds = layout.bounds(); - let content_layout = layout.children().next().unwrap(); - let content_bounds = content_layout.bounds(); - let offset = tree - .state - .downcast_ref::() - .offset(bounds, content_bounds); - - overlay.translate(Vector::new(-offset.x, -offset.y)) - }) - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Message: 'a, - Renderer: 'a + crate::Renderer, - Renderer::Theme: StyleSheet, -{ - fn from( - text_input: Scrollable<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(text_input) - } -} - -/// The identifier of a [`Scrollable`]. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Id(widget::Id); - -impl Id { - /// Creates a custom [`Id`]. - pub fn new(id: impl Into>) -> Self { - Self(widget::Id::new(id)) - } - - /// Creates a unique [`Id`]. - /// - /// This function produces a different [`Id`] every time it is called. - pub fn unique() -> Self { - Self(widget::Id::unique()) - } -} - -impl From for widget::Id { - fn from(id: Id) -> Self { - id.0 - } -} - -/// Produces a [`Command`] that snaps the [`Scrollable`] with the given [`Id`] -/// to the provided `percentage` along the x & y axis. -pub fn snap_to( - id: Id, - offset: RelativeOffset, -) -> Command { - Command::widget(operation::scrollable::snap_to(id.0, offset)) -} - -/// Computes the layout of a [`Scrollable`]. -pub fn layout( - renderer: &Renderer, - limits: &layout::Limits, - width: Length, - height: Length, - horizontal_enabled: bool, - layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, -) -> layout::Node { - let limits = limits - .max_height(f32::INFINITY) - .max_width(if horizontal_enabled { - f32::INFINITY - } else { - limits.max().width - }) - .width(width) - .height(height); - - let child_limits = layout::Limits::new( - Size::new(limits.min().width, 0.0), - Size::new( - if horizontal_enabled { - f32::INFINITY - } else { - limits.max().width - }, - f32::MAX, - ), - ); - - let content = layout_content(renderer, &child_limits); - let size = limits.resolve(content.size()); - - layout::Node::with_children(size, vec![content]) -} - -/// Processes an [`Event`] and updates the [`State`] of a [`Scrollable`] -/// accordingly. -pub fn update( - state: &mut State, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - vertical: &Properties, - horizontal: Option<&Properties>, - on_scroll: &Option Message + '_>>, - update_content: impl FnOnce( - Event, - Layout<'_>, - Point, - &mut dyn Clipboard, - &mut Shell<'_, Message>, - ) -> event::Status, -) -> event::Status { - let bounds = layout.bounds(); - let mouse_over_scrollable = bounds.contains(cursor_position); - - let content = layout.children().next().unwrap(); - let content_bounds = content.bounds(); - - let scrollbars = - Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); - - let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = - scrollbars.is_mouse_over(cursor_position); - - let event_status = { - let cursor_position = if mouse_over_scrollable - && !(mouse_over_y_scrollbar || mouse_over_x_scrollbar) - { - cursor_position + state.offset(bounds, content_bounds) - } else { - // TODO: Make `cursor_position` an `Option` so we can encode - // cursor availability. - // This will probably happen naturally once we add multi-window - // support. - Point::new(-1.0, -1.0) - }; - - update_content( - event.clone(), - content, - cursor_position, - clipboard, - shell, - ) - }; - - if let event::Status::Captured = event_status { - return event::Status::Captured; - } - - if let Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) = event - { - state.keyboard_modifiers = modifiers; - - return event::Status::Ignored; - } - - if mouse_over_scrollable { - match event { - Event::Mouse(mouse::Event::WheelScrolled { delta }) => { - let delta = match delta { - mouse::ScrollDelta::Lines { x, y } => { - // TODO: Configurable speed/friction (?) - let movement = if state.keyboard_modifiers.shift() { - Vector::new(y, x) - } else { - Vector::new(x, y) - }; - - movement * 60.0 - } - mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y), - }; - - state.scroll(delta, bounds, content_bounds); - - notify_on_scroll( - state, - on_scroll, - bounds, - content_bounds, - shell, - ); - - return event::Status::Captured; - } - Event::Touch(event) - if state.scroll_area_touched_at.is_some() - || !mouse_over_y_scrollbar && !mouse_over_x_scrollbar => - { - match event { - touch::Event::FingerPressed { .. } => { - state.scroll_area_touched_at = Some(cursor_position); - } - touch::Event::FingerMoved { .. } => { - if let Some(scroll_box_touched_at) = - state.scroll_area_touched_at - { - let delta = Vector::new( - cursor_position.x - scroll_box_touched_at.x, - cursor_position.y - scroll_box_touched_at.y, - ); - - state.scroll(delta, bounds, content_bounds); - - state.scroll_area_touched_at = - Some(cursor_position); - - notify_on_scroll( - state, - on_scroll, - bounds, - content_bounds, - shell, - ); - } - } - touch::Event::FingerLifted { .. } - | touch::Event::FingerLost { .. } => { - state.scroll_area_touched_at = None; - } - } - - return event::Status::Captured; - } - _ => {} - } - } - - if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at { - match event { - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - state.y_scroller_grabbed_at = None; - - return event::Status::Captured; - } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - if let Some(scrollbar) = scrollbars.y { - state.scroll_y_to( - scrollbar.scroll_percentage_y( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - notify_on_scroll( - state, - on_scroll, - bounds, - content_bounds, - shell, - ); - - return event::Status::Captured; - } - } - _ => {} - } - } else if mouse_over_y_scrollbar { - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - if let (Some(scroller_grabbed_at), Some(scrollbar)) = - (scrollbars.grab_y_scroller(cursor_position), scrollbars.y) - { - state.scroll_y_to( - scrollbar.scroll_percentage_y( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - state.y_scroller_grabbed_at = Some(scroller_grabbed_at); - - notify_on_scroll( - state, - on_scroll, - bounds, - content_bounds, - shell, - ); - } - - return event::Status::Captured; - } - _ => {} - } - } - - if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at { - match event { - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - state.x_scroller_grabbed_at = None; - - return event::Status::Captured; - } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - if let Some(scrollbar) = scrollbars.x { - state.scroll_x_to( - scrollbar.scroll_percentage_x( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - notify_on_scroll( - state, - on_scroll, - bounds, - content_bounds, - shell, - ); - } - - return event::Status::Captured; - } - _ => {} - } - } else if mouse_over_x_scrollbar { - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - if let (Some(scroller_grabbed_at), Some(scrollbar)) = - (scrollbars.grab_x_scroller(cursor_position), scrollbars.x) - { - state.scroll_x_to( - scrollbar.scroll_percentage_x( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - state.x_scroller_grabbed_at = Some(scroller_grabbed_at); - - notify_on_scroll( - state, - on_scroll, - bounds, - content_bounds, - shell, - ); - - return event::Status::Captured; - } - } - _ => {} - } - } - - event::Status::Ignored -} - -/// Computes the current [`mouse::Interaction`] of a [`Scrollable`]. -pub fn mouse_interaction( - state: &State, - layout: Layout<'_>, - cursor_position: Point, - vertical: &Properties, - horizontal: Option<&Properties>, - content_interaction: impl FnOnce( - Layout<'_>, - Point, - &Rectangle, - ) -> mouse::Interaction, -) -> mouse::Interaction { - let bounds = layout.bounds(); - let mouse_over_scrollable = bounds.contains(cursor_position); - - let content_layout = layout.children().next().unwrap(); - let content_bounds = content_layout.bounds(); - - let scrollbars = - Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); - - let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = - scrollbars.is_mouse_over(cursor_position); - - if (mouse_over_x_scrollbar || mouse_over_y_scrollbar) - || state.scrollers_grabbed() - { - mouse::Interaction::Idle - } else { - let offset = state.offset(bounds, content_bounds); - - let cursor_position = if mouse_over_scrollable - && !(mouse_over_y_scrollbar || mouse_over_x_scrollbar) - { - cursor_position + offset - } else { - Point::new(-1.0, -1.0) - }; - - content_interaction( - content_layout, - cursor_position, - &Rectangle { - y: bounds.y + offset.y, - x: bounds.x + offset.x, - ..bounds - }, - ) - } -} - -/// Draws a [`Scrollable`]. -pub fn draw( - state: &State, - renderer: &mut Renderer, - theme: &Renderer::Theme, - layout: Layout<'_>, - cursor_position: Point, - vertical: &Properties, - horizontal: Option<&Properties>, - style: &::Style, - draw_content: impl FnOnce(&mut Renderer, Layout<'_>, Point, &Rectangle), -) where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - let bounds = layout.bounds(); - let content_layout = layout.children().next().unwrap(); - let content_bounds = content_layout.bounds(); - - let scrollbars = - Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); - - let mouse_over_scrollable = bounds.contains(cursor_position); - let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = - scrollbars.is_mouse_over(cursor_position); - - let offset = state.offset(bounds, content_bounds); - - let cursor_position = if mouse_over_scrollable - && !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) - { - cursor_position + offset - } else { - Point::new(-1.0, -1.0) - }; - - // Draw inner content - if scrollbars.active() { - renderer.with_layer(bounds, |renderer| { - renderer.with_translation( - Vector::new(-offset.x, -offset.y), - |renderer| { - draw_content( - renderer, - content_layout, - cursor_position, - &Rectangle { - y: bounds.y + offset.y, - x: bounds.x + offset.x, - ..bounds - }, - ); - }, - ); - }); - - let draw_scrollbar = - |renderer: &mut Renderer, - style: style::Scrollbar, - scrollbar: &Scrollbar| { - //track - if style.background.is_some() - || (style.border_color != Color::TRANSPARENT - && style.border_width > 0.0) - { - renderer.fill_quad( - renderer::Quad { - bounds: scrollbar.bounds, - border_radius: style.border_radius.into(), - border_width: style.border_width, - border_color: style.border_color, - }, - style - .background - .unwrap_or(Background::Color(Color::TRANSPARENT)), - ); - } - - //thumb - if style.scroller.color != Color::TRANSPARENT - || (style.scroller.border_color != Color::TRANSPARENT - && style.scroller.border_width > 0.0) - { - renderer.fill_quad( - renderer::Quad { - bounds: scrollbar.scroller.bounds, - border_radius: style.scroller.border_radius.into(), - border_width: style.scroller.border_width, - border_color: style.scroller.border_color, - }, - style.scroller.color, - ); - } - }; - - renderer.with_layer( - Rectangle { - width: bounds.width + 2.0, - height: bounds.height + 2.0, - ..bounds - }, - |renderer| { - //draw y scrollbar - if let Some(scrollbar) = scrollbars.y { - let style = if state.y_scroller_grabbed_at.is_some() { - theme.dragging(style) - } else if mouse_over_y_scrollbar { - theme.hovered(style) - } else { - theme.active(style) - }; - - draw_scrollbar(renderer, style, &scrollbar); - } - - //draw x scrollbar - if let Some(scrollbar) = scrollbars.x { - let style = if state.x_scroller_grabbed_at.is_some() { - theme.dragging_horizontal(style) - } else if mouse_over_x_scrollbar { - theme.hovered_horizontal(style) - } else { - theme.active_horizontal(style) - }; - - draw_scrollbar(renderer, style, &scrollbar); - } - }, - ); - } else { - draw_content( - renderer, - content_layout, - cursor_position, - &Rectangle { - x: bounds.x + offset.x, - y: bounds.y + offset.y, - ..bounds - }, - ); - } -} - -fn notify_on_scroll( - state: &State, - on_scroll: &Option Message + '_>>, - bounds: Rectangle, - content_bounds: Rectangle, - shell: &mut Shell<'_, Message>, -) { - if let Some(on_scroll) = on_scroll { - if content_bounds.width <= bounds.width - && content_bounds.height <= bounds.height - { - return; - } - - let x = state.offset_x.absolute(bounds.width, content_bounds.width) - / (content_bounds.width - bounds.width); - - let y = state - .offset_y - .absolute(bounds.height, content_bounds.height) - / (content_bounds.height - bounds.height); - - shell.publish(on_scroll(RelativeOffset { x, y })) - } -} - -/// The local state of a [`Scrollable`]. -#[derive(Debug, Clone, Copy)] -pub struct State { - scroll_area_touched_at: Option, - offset_y: Offset, - y_scroller_grabbed_at: Option, - offset_x: Offset, - x_scroller_grabbed_at: Option, - keyboard_modifiers: keyboard::Modifiers, -} - -impl Default for State { - fn default() -> Self { - Self { - scroll_area_touched_at: None, - offset_y: Offset::Absolute(0.0), - y_scroller_grabbed_at: None, - offset_x: Offset::Absolute(0.0), - x_scroller_grabbed_at: None, - keyboard_modifiers: keyboard::Modifiers::default(), - } - } -} - -impl operation::Scrollable for State { - fn snap_to(&mut self, offset: RelativeOffset) { - State::snap_to(self, offset); - } -} - -#[derive(Debug, Clone, Copy)] -enum Offset { - Absolute(f32), - Relative(f32), -} - -impl Offset { - fn absolute(self, window: f32, content: f32) -> f32 { - match self { - Offset::Absolute(absolute) => { - absolute.min((content - window).max(0.0)) - } - Offset::Relative(percentage) => { - ((content - window) * percentage).max(0.0) - } - } - } -} - -impl State { - /// Creates a new [`State`] with the scrollbar(s) at the beginning. - pub fn new() -> Self { - State::default() - } - - /// Apply a scrolling offset to the current [`State`], given the bounds of - /// the [`Scrollable`] and its contents. - pub fn scroll( - &mut self, - delta: Vector, - bounds: Rectangle, - content_bounds: Rectangle, - ) { - if bounds.height < content_bounds.height { - self.offset_y = Offset::Absolute( - (self.offset_y.absolute(bounds.height, content_bounds.height) - - delta.y) - .clamp(0.0, content_bounds.height - bounds.height), - ) - } - - if bounds.width < content_bounds.width { - self.offset_x = Offset::Absolute( - (self.offset_x.absolute(bounds.width, content_bounds.width) - - delta.x) - .clamp(0.0, content_bounds.width - bounds.width), - ); - } - } - - /// Scrolls the [`Scrollable`] to a relative amount along the y axis. - /// - /// `0` represents scrollbar at the beginning, while `1` represents scrollbar at - /// the end. - pub fn scroll_y_to( - &mut self, - percentage: f32, - bounds: Rectangle, - content_bounds: Rectangle, - ) { - self.offset_y = Offset::Relative(percentage.clamp(0.0, 1.0)); - self.unsnap(bounds, content_bounds); - } - - /// Scrolls the [`Scrollable`] to a relative amount along the x axis. - /// - /// `0` represents scrollbar at the beginning, while `1` represents scrollbar at - /// the end. - pub fn scroll_x_to( - &mut self, - percentage: f32, - bounds: Rectangle, - content_bounds: Rectangle, - ) { - self.offset_x = Offset::Relative(percentage.clamp(0.0, 1.0)); - self.unsnap(bounds, content_bounds); - } - - /// Snaps the scroll position to a [`RelativeOffset`]. - pub fn snap_to(&mut self, offset: RelativeOffset) { - self.offset_x = Offset::Relative(offset.x.clamp(0.0, 1.0)); - self.offset_y = Offset::Relative(offset.y.clamp(0.0, 1.0)); - } - - /// Unsnaps the current scroll position, if snapped, given the bounds of the - /// [`Scrollable`] and its contents. - pub fn unsnap(&mut self, bounds: Rectangle, content_bounds: Rectangle) { - self.offset_x = Offset::Absolute( - self.offset_x.absolute(bounds.width, content_bounds.width), - ); - self.offset_y = Offset::Absolute( - self.offset_y.absolute(bounds.height, content_bounds.height), - ); - } - - /// Returns the scrolling offset of the [`State`], given the bounds of the - /// [`Scrollable`] and its contents. - pub fn offset( - &self, - bounds: Rectangle, - content_bounds: Rectangle, - ) -> Vector { - Vector::new( - self.offset_x.absolute(bounds.width, content_bounds.width), - self.offset_y.absolute(bounds.height, content_bounds.height), - ) - } - - /// Returns whether any scroller is currently grabbed or not. - pub fn scrollers_grabbed(&self) -> bool { - self.x_scroller_grabbed_at.is_some() - || self.y_scroller_grabbed_at.is_some() - } -} - -#[derive(Debug)] -/// State of both [`Scrollbar`]s. -struct Scrollbars { - y: Option, - x: Option, -} - -impl Scrollbars { - /// Create y and/or x scrollbar(s) if content is overflowing the [`Scrollable`] bounds. - fn new( - state: &State, - vertical: &Properties, - horizontal: Option<&Properties>, - bounds: Rectangle, - content_bounds: Rectangle, - ) -> Self { - let offset = state.offset(bounds, content_bounds); - - let show_scrollbar_x = horizontal.and_then(|h| { - if content_bounds.width > bounds.width { - Some(h) - } else { - None - } - }); - - let y_scrollbar = if content_bounds.height > bounds.height { - let Properties { - width, - margin, - scroller_width, - } = *vertical; - - // Adjust the height of the vertical scrollbar if the horizontal scrollbar - // is present - let x_scrollbar_height = show_scrollbar_x - .map_or(0.0, |h| h.width.max(h.scroller_width) + h.margin); - - let total_scrollbar_width = - width.max(scroller_width) + 2.0 * margin; - - // Total bounds of the scrollbar + margin + scroller width - let total_scrollbar_bounds = Rectangle { - x: bounds.x + bounds.width - total_scrollbar_width, - y: bounds.y, - width: total_scrollbar_width, - height: (bounds.height - x_scrollbar_height).max(0.0), - }; - - // Bounds of just the scrollbar - let scrollbar_bounds = Rectangle { - x: bounds.x + bounds.width - - total_scrollbar_width / 2.0 - - width / 2.0, - y: bounds.y, - width, - height: (bounds.height - x_scrollbar_height).max(0.0), - }; - - let ratio = bounds.height / content_bounds.height; - // min height for easier grabbing with super tall content - let scroller_height = (bounds.height * ratio).max(2.0); - let scroller_offset = offset.y * ratio; - - let scroller_bounds = Rectangle { - x: bounds.x + bounds.width - - total_scrollbar_width / 2.0 - - scroller_width / 2.0, - y: (scrollbar_bounds.y + scroller_offset - x_scrollbar_height) - .max(0.0), - width: scroller_width, - height: scroller_height, - }; - - Some(Scrollbar { - total_bounds: total_scrollbar_bounds, - bounds: scrollbar_bounds, - scroller: Scroller { - bounds: scroller_bounds, - }, - }) - } else { - None - }; - - let x_scrollbar = if let Some(horizontal) = show_scrollbar_x { - let Properties { - width, - margin, - scroller_width, - } = *horizontal; - - // Need to adjust the width of the horizontal scrollbar if the vertical scrollbar - // is present - let scrollbar_y_width = y_scrollbar.map_or(0.0, |_| { - vertical.width.max(vertical.scroller_width) + vertical.margin - }); - - let total_scrollbar_height = - width.max(scroller_width) + 2.0 * margin; - - // Total bounds of the scrollbar + margin + scroller width - let total_scrollbar_bounds = Rectangle { - x: bounds.x, - y: bounds.y + bounds.height - total_scrollbar_height, - width: (bounds.width - scrollbar_y_width).max(0.0), - height: total_scrollbar_height, - }; - - // Bounds of just the scrollbar - let scrollbar_bounds = Rectangle { - x: bounds.x, - y: bounds.y + bounds.height - - total_scrollbar_height / 2.0 - - width / 2.0, - width: (bounds.width - scrollbar_y_width).max(0.0), - height: width, - }; - - let ratio = bounds.width / content_bounds.width; - // min width for easier grabbing with extra wide content - let scroller_length = (bounds.width * ratio).max(2.0); - let scroller_offset = offset.x * ratio; - - let scroller_bounds = Rectangle { - x: (scrollbar_bounds.x + scroller_offset - scrollbar_y_width) - .max(0.0), - y: bounds.y + bounds.height - - total_scrollbar_height / 2.0 - - scroller_width / 2.0, - width: scroller_length, - height: scroller_width, - }; - - Some(Scrollbar { - total_bounds: total_scrollbar_bounds, - bounds: scrollbar_bounds, - scroller: Scroller { - bounds: scroller_bounds, - }, - }) - } else { - None - }; - - Self { - y: y_scrollbar, - x: x_scrollbar, - } - } - - fn is_mouse_over(&self, cursor_position: Point) -> (bool, bool) { - ( - self.y - .as_ref() - .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) - .unwrap_or(false), - self.x - .as_ref() - .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) - .unwrap_or(false), - ) - } - - fn grab_y_scroller(&self, cursor_position: Point) -> Option { - self.y.and_then(|scrollbar| { - if scrollbar.total_bounds.contains(cursor_position) { - Some(if scrollbar.scroller.bounds.contains(cursor_position) { - (cursor_position.y - scrollbar.scroller.bounds.y) - / scrollbar.scroller.bounds.height - } else { - 0.5 - }) - } else { - None - } - }) - } - - fn grab_x_scroller(&self, cursor_position: Point) -> Option { - self.x.and_then(|scrollbar| { - if scrollbar.total_bounds.contains(cursor_position) { - Some(if scrollbar.scroller.bounds.contains(cursor_position) { - (cursor_position.x - scrollbar.scroller.bounds.x) - / scrollbar.scroller.bounds.width - } else { - 0.5 - }) - } else { - None - } - }) - } - - fn active(&self) -> bool { - self.y.is_some() || self.x.is_some() - } -} - -/// The scrollbar of a [`Scrollable`]. -#[derive(Debug, Copy, Clone)] -struct Scrollbar { - /// The total bounds of the [`Scrollbar`], including the scrollbar, the scroller, - /// and the scrollbar margin. - total_bounds: Rectangle, - - /// The bounds of just the [`Scrollbar`]. - bounds: Rectangle, - - /// The state of this scrollbar's [`Scroller`]. - scroller: Scroller, -} - -impl Scrollbar { - /// Returns whether the mouse is over the scrollbar or not. - fn is_mouse_over(&self, cursor_position: Point) -> bool { - self.total_bounds.contains(cursor_position) - } - - /// Returns the y-axis scrolled percentage from the cursor position. - fn scroll_percentage_y( - &self, - grabbed_at: f32, - cursor_position: Point, - ) -> f32 { - if cursor_position.x < 0.0 && cursor_position.y < 0.0 { - // cursor position is unavailable! Set to either end or beginning of scrollbar depending - // on where the thumb currently is in the track - (self.scroller.bounds.y / self.total_bounds.height).round() - } else { - (cursor_position.y - - self.bounds.y - - self.scroller.bounds.height * grabbed_at) - / (self.bounds.height - self.scroller.bounds.height) - } - } - - /// Returns the x-axis scrolled percentage from the cursor position. - fn scroll_percentage_x( - &self, - grabbed_at: f32, - cursor_position: Point, - ) -> f32 { - if cursor_position.x < 0.0 && cursor_position.y < 0.0 { - (self.scroller.bounds.x / self.total_bounds.width).round() - } else { - (cursor_position.x - - self.bounds.x - - self.scroller.bounds.width * grabbed_at) - / (self.bounds.width - self.scroller.bounds.width) - } - } -} - -/// The handle of a [`Scrollbar`]. -#[derive(Debug, Clone, Copy)] -struct Scroller { - /// The bounds of the [`Scroller`]. - bounds: Rectangle, -} diff --git a/native/src/widget/slider.rs b/native/src/widget/slider.rs deleted file mode 100644 index d3715b1c..00000000 --- a/native/src/widget/slider.rs +++ /dev/null @@ -1,473 +0,0 @@ -//! Display an interactive selector of a single value from a range of values. -//! -//! A [`Slider`] has some local [`State`]. -use crate::event::{self, Event}; -use crate::layout; -use crate::mouse; -use crate::renderer; -use crate::touch; -use crate::widget::tree::{self, Tree}; -use crate::{ - Background, Clipboard, Color, Element, Layout, Length, Pixels, Point, - Rectangle, Shell, Size, Widget, -}; - -use std::ops::RangeInclusive; - -pub use iced_style::slider::{Appearance, Handle, HandleShape, StyleSheet}; - -/// An horizontal bar and a handle that selects a single value from a range of -/// values. -/// -/// A [`Slider`] will try to fill the horizontal space of its container. -/// -/// The [`Slider`] range of numeric values is generic and its step size defaults -/// to 1 unit. -/// -/// # Example -/// ``` -/// # use iced_native::widget::slider; -/// # use iced_native::renderer::Null; -/// # -/// # type Slider<'a, T, Message> = slider::Slider<'a, T, Message, Null>; -/// # -/// #[derive(Clone)] -/// pub enum Message { -/// SliderChanged(f32), -/// } -/// -/// let value = 50.0; -/// -/// Slider::new(0.0..=100.0, value, Message::SliderChanged); -/// ``` -/// -/// ![Slider drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/slider.png?raw=true) -#[allow(missing_debug_implementations)] -pub struct Slider<'a, T, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - range: RangeInclusive, - step: T, - value: T, - on_change: Box Message + 'a>, - on_release: Option, - width: Length, - height: f32, - style: ::Style, -} - -impl<'a, T, Message, Renderer> Slider<'a, T, Message, Renderer> -where - T: Copy + From + std::cmp::PartialOrd, - Message: Clone, - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - /// The default height of a [`Slider`]. - pub const DEFAULT_HEIGHT: f32 = 22.0; - - /// Creates a new [`Slider`]. - /// - /// It expects: - /// * an inclusive range of possible values - /// * the current value of the [`Slider`] - /// * a function that will be called when the [`Slider`] is dragged. - /// It receives the new value of the [`Slider`] and must produce a - /// `Message`. - pub fn new(range: RangeInclusive, value: T, on_change: F) -> Self - where - F: 'a + Fn(T) -> Message, - { - let value = if value >= *range.start() { - value - } else { - *range.start() - }; - - let value = if value <= *range.end() { - value - } else { - *range.end() - }; - - Slider { - value, - range, - step: T::from(1), - on_change: Box::new(on_change), - on_release: None, - width: Length::Fill, - height: Self::DEFAULT_HEIGHT, - style: Default::default(), - } - } - - /// Sets the release message of the [`Slider`]. - /// This is called when the mouse is released from the slider. - /// - /// Typically, the user's interaction with the slider is finished when this message is produced. - /// This is useful if you need to spawn a long-running task from the slider's result, where - /// the default on_change message could create too many events. - pub fn on_release(mut self, on_release: Message) -> Self { - self.on_release = Some(on_release); - self - } - - /// Sets the width of the [`Slider`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the height of the [`Slider`]. - pub fn height(mut self, height: impl Into) -> Self { - self.height = height.into().0; - self - } - - /// Sets the style of the [`Slider`]. - pub fn style( - mut self, - style: impl Into<::Style>, - ) -> Self { - self.style = style.into(); - self - } - - /// Sets the step size of the [`Slider`]. - pub fn step(mut self, step: T) -> Self { - self.step = step; - self - } -} - -impl<'a, T, Message, Renderer> Widget - for Slider<'a, T, Message, Renderer> -where - T: Copy + Into + num_traits::FromPrimitive, - Message: Clone, - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(State::new()) - } - - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - Length::Shrink - } - - fn layout( - &self, - _renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - let size = limits.resolve(Size::ZERO); - - layout::Node::new(size) - } - - 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 { - update( - event, - layout, - cursor_position, - shell, - tree.state.downcast_mut::(), - &mut self.value, - &self.range, - self.step, - self.on_change.as_ref(), - &self.on_release, - ) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - draw( - renderer, - layout, - cursor_position, - tree.state.downcast_ref::(), - self.value, - &self.range, - theme, - &self.style, - ) - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - mouse_interaction( - layout, - cursor_position, - tree.state.downcast_ref::(), - ) - } -} - -impl<'a, T, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - T: 'a + Copy + Into + num_traits::FromPrimitive, - Message: 'a + Clone, - Renderer: 'a + crate::Renderer, - Renderer::Theme: StyleSheet, -{ - fn from( - slider: Slider<'a, T, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(slider) - } -} - -/// Processes an [`Event`] and updates the [`State`] of a [`Slider`] -/// accordingly. -pub fn update( - event: Event, - layout: Layout<'_>, - cursor_position: Point, - shell: &mut Shell<'_, Message>, - state: &mut State, - value: &mut T, - range: &RangeInclusive, - step: T, - on_change: &dyn Fn(T) -> Message, - on_release: &Option, -) -> event::Status -where - T: Copy + Into + num_traits::FromPrimitive, - Message: Clone, -{ - let is_dragging = state.is_dragging; - - let mut change = || { - let bounds = layout.bounds(); - let new_value = if cursor_position.x <= bounds.x { - *range.start() - } else if cursor_position.x >= bounds.x + bounds.width { - *range.end() - } else { - let step = step.into(); - let start = (*range.start()).into(); - let end = (*range.end()).into(); - - let percent = f64::from(cursor_position.x - bounds.x) - / f64::from(bounds.width); - - let steps = (percent * (end - start) / step).round(); - let value = steps * step + start; - - if let Some(value) = T::from_f64(value) { - value - } else { - return; - } - }; - - if ((*value).into() - new_value.into()).abs() > f64::EPSILON { - shell.publish((on_change)(new_value)); - - *value = new_value; - } - }; - - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - if layout.bounds().contains(cursor_position) { - change(); - state.is_dragging = true; - - return event::Status::Captured; - } - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - if is_dragging { - if let Some(on_release) = on_release.clone() { - shell.publish(on_release); - } - state.is_dragging = false; - - return event::Status::Captured; - } - } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - if is_dragging { - change(); - - return event::Status::Captured; - } - } - _ => {} - } - - event::Status::Ignored -} - -/// Draws a [`Slider`]. -pub fn draw( - renderer: &mut R, - layout: Layout<'_>, - cursor_position: Point, - state: &State, - value: T, - range: &RangeInclusive, - style_sheet: &dyn StyleSheet