diff options
181 files changed, 4960 insertions, 3212 deletions
diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml index e93a01ae..d4c94fcd 100644 --- a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml +++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml @@ -62,15 +62,15 @@ body: If you are using an older release, please upgrade to the latest one before filing an issue. options: + - crates.io release - master - - 0.7 validations: required: true - type: dropdown id: os attributes: - label: Operative System - description: Which operative system are you using? + label: Operating System + description: Which operating system are you using? options: - Windows - macOS diff --git a/.github/workflows/document.yml b/.github/workflows/document.yml index e69f4d63..09a7a4d5 100644 --- a/.github/workflows/document.yml +++ b/.github/workflows/document.yml @@ -20,13 +20,13 @@ jobs: -p iced_core \ -p iced_style \ -p iced_futures \ - -p iced_native \ - -p iced_lazy \ + -p iced_runtime \ -p iced_graphics \ -p iced_wgpu \ - -p iced_glow \ + -p iced_tiny_skia \ + -p iced_renderer \ + -p iced_widget \ -p iced_winit \ - -p iced_glutin \ -p iced - name: Write CNAME file run: echo 'docs.iced.rs' > ./target/doc/CNAME @@ -12,11 +12,9 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"] categories = ["gui"] [features] -default = ["wgpu", "tiny-skia"] +default = ["wgpu"] # Enable the `wgpu` GPU-accelerated renderer backend wgpu = ["iced_renderer/wgpu"] -# Enable the `tiny-skia` software renderer backend -tiny-skia = ["iced_renderer/tiny-skia"] # Enables the `Image` widget image = ["iced_widget/image", "image_rs"] # Enables the `Svg` widget @@ -39,6 +37,8 @@ smol = ["iced_futures/smol"] palette = ["iced_core/palette"] # Enables querying system information system = ["iced_winit/system"] +# Enables broken "sRGB linear" blending to reproduce color management of the Web +web-colors = ["iced_renderer/web-colors"] # Enables the advanced module advanced = [] diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md new file mode 100644 index 00000000..809371cb --- /dev/null +++ b/DEPENDENCIES.md @@ -0,0 +1,33 @@ +# Dependencies + +Iced requires some system dependencies to work, and not +all operating systems come with them installed. + +You can follow the provided instructions for your system to +get them, if your system isn't here, add it! + +## NixOS + +You can add this `shell.nix` to your project and use it by running `nix-shell`: + +```nix +{ pkgs ? import <nixpkgs> {} }: + +pkgs.mkShell rec { + buildInputs = with pkgs; [ + expat + fontconfig + freetype + freetype.dev + libGL + pkgconfig + xorg.libX11 + xorg.libXcursor + xorg.libXi + xorg.libXrandr + ]; + + LD_LIBRARY_PATH = + builtins.foldl' (a: b: "${a}:${b}/lib") "${pkgs.vulkan-loader}/lib" buildInputs; +} +``` @@ -35,9 +35,9 @@ Inspired by [Elm]. * First-class support for async actions (use futures!) * [Modular ecosystem] split into reusable parts: * A [renderer-agnostic native runtime] enabling integration with existing systems - * Two [built-in renderers] leveraging [`wgpu`] and [`glow`] + * Two [built-in renderers] leveraging [`wgpu`] and [`tiny-skia`] * [`iced_wgpu`] supporting Vulkan, Metal and DX12 - * [`iced_glow`] supporting OpenGL 2.1+ and OpenGL ES 2.0+ + * [`iced_tiny_skia`] offering a software alternative as a fallback * A [windowing shell] * A [web runtime] leveraging the DOM @@ -52,9 +52,9 @@ __Iced is currently experimental software.__ [Take a look at the roadmap], [Modular ecosystem]: ECOSYSTEM.md [renderer-agnostic native runtime]: native/ [`wgpu`]: https://github.com/gfx-rs/wgpu -[`glow`]: https://github.com/grovesNL/glow +[`tiny-skia`]: https://github.com/RazrFalcon/tiny-skia [`iced_wgpu`]: wgpu/ -[`iced_glow`]: glow/ +[`iced_tiny_skia`]: tiny_skia/ [built-in renderers]: ECOSYSTEM.md#Renderers [windowing shell]: winit/ [`dodrio`]: https://github.com/fitzgen/dodrio @@ -196,34 +196,6 @@ end-user-oriented GUI library, while keeping [the ecosystem] modular: [`ggez`]: https://github.com/ggez/ggez [the ecosystem]: ECOSYSTEM.md -## Troubleshooting - -### `GraphicsAdapterNotFound` - -This occurs when the selected [built-in renderer] is not able to create a context. - -Often this will occur while using [`iced_wgpu`] as the renderer without -supported hardware (needs Vulkan, Metal or DX12). In this case, you could try using the -[`iced_glow`] renderer: - -First, check if it works with - -```console -cargo run --features iced/glow --package game_of_life -``` - -and then use it in your project with - -```toml -iced = { version = "0.9", default-features = false, features = ["glow"] } -``` - -__NOTE:__ Chances are you have hardware that supports at least OpenGL 2.1 or OpenGL ES 2.0, -but if you don't, right now there's no software fallback, so it means your hardware -doesn't support Iced. - -[built-in renderer]: https://github.com/iced-rs/iced/blob/master/ECOSYSTEM.md#Renderers - ## Contributing / Feedback Contributions are greatly appreciated! If you want to contribute, please diff --git a/core/Cargo.toml b/core/Cargo.toml index 92d9773f..55f2e85f 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -10,11 +10,15 @@ repository = "https://github.com/iced-rs/iced" [dependencies] bitflags = "1.2" thiserror = "1" +log = "0.4.17" twox-hash = { version = "1.5", default-features = false } [dependencies.palette] -version = "0.6" +version = "0.7" optional = true [target.'cfg(target_arch = "wasm32")'.dependencies] instant = "0.1" + +[dev-dependencies] +approx = "0.5" diff --git a/core/src/angle.rs b/core/src/angle.rs new file mode 100644 index 00000000..75a57c76 --- /dev/null +++ b/core/src/angle.rs @@ -0,0 +1,33 @@ +use crate::{Point, Rectangle, Vector}; +use std::f32::consts::PI; + +#[derive(Debug, Copy, Clone, PartialEq)] +/// Degrees +pub struct Degrees(pub f32); + +#[derive(Debug, Copy, Clone, PartialEq)] +/// Radians +pub struct Radians(pub f32); + +impl From<Degrees> for Radians { + fn from(degrees: Degrees) -> Self { + Radians(degrees.0 * PI / 180.0) + } +} + +impl Radians { + /// Calculates the line in which the [`Angle`] intercepts the `bounds`. + pub fn to_distance(&self, bounds: &Rectangle) -> (Point, Point) { + let v1 = Vector::new(f32::cos(self.0), f32::sin(self.0)); + + let distance_to_rect = f32::min( + f32::abs((bounds.y - bounds.center().y) / v1.y), + f32::abs(((bounds.x + bounds.width) - bounds.center().x) / v1.x), + ); + + let start = bounds.center() + v1 * distance_to_rect; + let end = bounds.center() - v1 * distance_to_rect; + + (start, end) + } +} diff --git a/core/src/background.rs b/core/src/background.rs index cfb95867..347c52c0 100644 --- a/core/src/background.rs +++ b/core/src/background.rs @@ -1,11 +1,14 @@ +use crate::gradient::{self, Gradient}; use crate::Color; /// The background of some element. #[derive(Debug, Clone, Copy, PartialEq)] pub enum Background { - /// A solid color + /// A solid color. Color(Color), - // TODO: Add gradient and image variants + /// Linearly interpolate between several colors. + Gradient(Gradient), + // TODO: Add image variant } impl From<Color> for Background { @@ -14,8 +17,14 @@ impl From<Color> for Background { } } -impl From<Color> for Option<Background> { - fn from(color: Color) -> Self { - Some(Background::from(color)) +impl From<Gradient> for Background { + fn from(gradient: Gradient) -> Self { + Background::Gradient(gradient) + } +} + +impl From<gradient::Linear> for Background { + fn from(gradient: gradient::Linear) -> Self { + Background::Gradient(Gradient::Linear(gradient)) } } diff --git a/core/src/border_radius.rs b/core/src/border_radius.rs new file mode 100644 index 00000000..a444dd74 --- /dev/null +++ b/core/src/border_radius.rs @@ -0,0 +1,22 @@ +/// The border radii 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<f32> 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<BorderRadius> for [f32; 4] { + fn from(radi: BorderRadius) -> Self { + radi.0 + } +} diff --git a/core/src/color.rs b/core/src/color.rs index fe0a1856..1392f28b 100644 --- a/core/src/color.rs +++ b/core/src/color.rs @@ -183,15 +183,15 @@ macro_rules! color { } #[cfg(feature = "palette")] -/// Converts from palette's `Srgba` type to a [`Color`]. +/// Converts from palette's `Rgba` type to a [`Color`]. impl From<Srgba> for Color { - fn from(srgba: Srgba) -> Self { - Color::new(srgba.red, srgba.green, srgba.blue, srgba.alpha) + fn from(rgba: Srgba) -> Self { + Color::new(rgba.red, rgba.green, rgba.blue, rgba.alpha) } } #[cfg(feature = "palette")] -/// Converts from [`Color`] to palette's `Srgba` type. +/// Converts from [`Color`] to palette's `Rgba` type. impl From<Color> for Srgba { fn from(c: Color) -> Self { Srgba::new(c.r, c.g, c.b, c.a) @@ -199,15 +199,15 @@ impl From<Color> for Srgba { } #[cfg(feature = "palette")] -/// Converts from palette's `Srgb` type to a [`Color`]. +/// Converts from palette's `Rgb` type to a [`Color`]. impl From<Srgb> for Color { - fn from(srgb: Srgb) -> Self { - Color::new(srgb.red, srgb.green, srgb.blue, 1.0) + fn from(rgb: Srgb) -> Self { + Color::new(rgb.red, rgb.green, rgb.blue, 1.0) } } #[cfg(feature = "palette")] -/// Converts from [`Color`] to palette's `Srgb` type. +/// Converts from [`Color`] to palette's `Rgb` type. impl From<Color> for Srgb { fn from(c: Color) -> Self { Srgb::new(c.r, c.g, c.b) @@ -218,12 +218,12 @@ impl From<Color> for Srgb { #[cfg(test)] mod tests { use super::*; - use palette::Blend; + use palette::blend::Blend; #[test] fn srgba_traits() { let c = Color::from_rgb(0.5, 0.4, 0.3); - // Round-trip conversion to the palette:Srgba type + // Round-trip conversion to the palette::Srgba type let s: Srgba = c.into(); let r: Color = s.into(); assert_eq!(c, r); @@ -231,6 +231,8 @@ mod tests { #[test] fn color_manipulation() { + use approx::assert_relative_eq; + let c1 = Color::from_rgb(0.5, 0.4, 0.3); let c2 = Color::from_rgb(0.2, 0.5, 0.3); @@ -238,19 +240,15 @@ mod tests { let l1 = Srgba::from(c1).into_linear(); let l2 = Srgba::from(c2).into_linear(); - // Take the lighter of each of the RGB components + // Take the lighter of each of the sRGB components let lighter = l1.lighten(l2); // Convert back to our Color - let r: Color = Srgba::from_linear(lighter).into(); - assert_eq!( - r, - Color { - r: 0.5, - g: 0.5, - b: 0.3, - a: 1.0 - } - ); + let result: Color = Srgba::from_linear(lighter).into(); + + assert_relative_eq!(result.r, 0.5); + assert_relative_eq!(result.g, 0.5); + assert_relative_eq!(result.b, 0.3); + assert_relative_eq!(result.a, 1.0); } } diff --git a/core/src/element.rs b/core/src/element.rs index 98c53737..3268f14b 100644 --- a/core/src/element.rs +++ b/core/src/element.rs @@ -5,9 +5,7 @@ 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 crate::{Clipboard, Color, Layout, Length, Rectangle, Shell, Widget}; use std::any::Any; use std::borrow::Borrow; @@ -378,7 +376,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, B>, @@ -390,7 +388,7 @@ where tree, event, layout, - cursor_position, + cursor, renderer, clipboard, &mut local_shell, @@ -408,35 +406,23 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, ) { - self.widget.draw( - tree, - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ) + self.widget + .draw(tree, renderer, theme, style, layout, cursor, viewport) } fn mouse_interaction( &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - self.widget.mouse_interaction( - tree, - layout, - cursor_position, - viewport, - renderer, - ) + self.widget + .mouse_interaction(tree, layout, cursor, viewport, renderer) } fn overlay<'b>( @@ -521,20 +507,14 @@ where state: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, 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, - ) + self.element + .widget + .on_event(state, event, layout, cursor, renderer, clipboard, shell) } fn draw( @@ -544,7 +524,7 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, ) { fn explain_layout<Renderer: crate::Renderer>( @@ -567,15 +547,9 @@ where } } - self.element.widget.draw( - state, - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ); + self.element + .widget + .draw(state, renderer, theme, style, layout, cursor, viewport); explain_layout(renderer, self.color, layout); } @@ -584,17 +558,13 @@ where &self, state: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - self.element.widget.mouse_interaction( - state, - layout, - cursor_position, - viewport, - renderer, - ) + self.element + .widget + .mouse_interaction(state, layout, cursor, viewport, renderer) } fn overlay<'b>( diff --git a/core/src/gradient.rs b/core/src/gradient.rs index 61e919d6..e19622fb 100644 --- a/core/src/gradient.rs +++ b/core/src/gradient.rs @@ -1,27 +1,40 @@ -//! For creating a Gradient. -pub mod linear; +//! Colors that transition progressively. +use crate::{Color, Radians}; -pub use linear::Linear; +use std::cmp::Ordering; -use crate::{Color, Point, Size}; - -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] /// A fill which transitions colors progressively along a direction, either linearly, radially (TBD), /// or conically (TBD). +/// +/// For a gradient which can be used as a fill on a canvas, see [`iced_graphics::Gradient`]. pub enum Gradient { - /// A linear gradient interpolates colors along a direction from its `start` to its `end` - /// point. + /// A linear gradient interpolates colors along a direction at a specific [`Angle`]. Linear(Linear), } impl Gradient { - /// Creates a new linear [`linear::Builder`]. - pub fn linear(position: impl Into<Position>) -> linear::Builder { - linear::Builder::new(position.into()) + /// Adjust the opacity of the gradient by a multiplier applied to each color stop. + pub fn mul_alpha(mut self, alpha_multiplier: f32) -> Self { + match &mut self { + Gradient::Linear(linear) => { + for stop in linear.stops.iter_mut().flatten() { + stop.color.a *= alpha_multiplier; + } + } + } + + self } } -#[derive(Debug, Clone, Copy, PartialEq)] +impl From<Linear> for Gradient { + fn from(gradient: Linear) -> Self { + Self::Linear(gradient) + } +} + +#[derive(Debug, Default, Clone, Copy, PartialEq)] /// A point along the gradient vector where the specified [`color`] is unmixed. /// /// [`color`]: Self::color @@ -35,83 +48,58 @@ pub struct ColorStop { 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, - }, +/// A linear gradient. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Linear { + /// How the [`Gradient`] is angled within its bounds. + pub angle: Radians, + /// [`ColorStop`]s along the linear gradient path. + pub stops: [Option<ColorStop>; 8], } -impl From<(Point, Point)> for Position { - fn from((start, end): (Point, Point)) -> Self { - Self::Absolute { start, end } +impl Linear { + /// Creates a new [`Linear`] gradient with the given angle in [`Radians`]. + pub fn new(angle: impl Into<Radians>) -> Self { + Self { + angle: angle.into(), + stops: [None; 8], + } } -} -#[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, -} + /// Adds a new [`ColorStop`], defined by an offset and a color, to the gradient. + /// + /// Any `offset` that is not within `0.0..=1.0` will be silently ignored. + /// + /// Any stop added after the 8th will be silently ignored. + pub fn add_stop(mut self, offset: f32, color: Color) -> Self { + if offset.is_finite() && (0.0..=1.0).contains(&offset) { + let (Ok(index) | Err(index)) = + self.stops.binary_search_by(|stop| match stop { + None => Ordering::Greater, + Some(stop) => stop.offset.partial_cmp(&offset).unwrap(), + }); -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) + if index < 8 { + self.stops[index] = Some(ColorStop { offset, color }); } + } else { + log::warn!("Gradient color stop must be within 0.0..=1.0 range."); + }; + + self + } + + /// Adds multiple [`ColorStop`]s to the gradient. + /// + /// Any stop added after the 8th will be silently ignored. + pub fn add_stops( + mut self, + stops: impl IntoIterator<Item = ColorStop>, + ) -> Self { + for stop in stops.into_iter() { + self = self.add_stop(stop.offset, stop.color) } + + self } } diff --git a/core/src/gradient/linear.rs b/core/src/gradient/linear.rs deleted file mode 100644 index c886db47..00000000 --- a/core/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<ColorStop>, -} - -/// A [`Linear`] builder. -#[derive(Debug)] -pub struct Builder { - start: Point, - end: Point, - stops: Vec<ColorStop>, - error: Option<BuilderError>, -} - -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<Gradient, BuilderError> { - 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 89dfb828..76d775e7 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -42,7 +42,9 @@ pub mod touch; pub mod widget; pub mod window; +mod angle; mod background; +mod border_radius; mod color; mod content_fit; mod element; @@ -57,7 +59,9 @@ mod size; mod vector; pub use alignment::Alignment; +pub use angle::{Degrees, Radians}; pub use background::Background; +pub use border_radius::BorderRadius; pub use clipboard::Clipboard; pub use color::Color; pub use content_fit::ContentFit; diff --git a/core/src/mouse.rs b/core/src/mouse.rs index 0c405ce6..d13a60fb 100644 --- a/core/src/mouse.rs +++ b/core/src/mouse.rs @@ -2,10 +2,12 @@ pub mod click; mod button; +mod cursor; mod event; mod interaction; pub use button::Button; pub use click::Click; +pub use cursor::Cursor; pub use event::{Event, ScrollDelta}; pub use interaction::Interaction; diff --git a/core/src/mouse/button.rs b/core/src/mouse/button.rs index aeb8a55d..3eec7f42 100644 --- a/core/src/mouse/button.rs +++ b/core/src/mouse/button.rs @@ -11,5 +11,5 @@ pub enum Button { Middle, /// Some other button. - Other(u8), + Other(u16), } diff --git a/core/src/mouse/cursor.rs b/core/src/mouse/cursor.rs new file mode 100644 index 00000000..203526e9 --- /dev/null +++ b/core/src/mouse/cursor.rs @@ -0,0 +1,52 @@ +use crate::{Point, Rectangle, Vector}; + +/// The mouse cursor state. +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub enum Cursor { + /// The cursor has a defined position. + Available(Point), + + /// The cursor is currently unavailable (i.e. out of bounds or busy). + #[default] + Unavailable, +} + +impl Cursor { + /// Returns the absolute position of the [`Cursor`], if available. + pub fn position(self) -> Option<Point> { + match self { + Cursor::Available(position) => Some(position), + Cursor::Unavailable => None, + } + } + + /// Returns the absolute position of the [`Cursor`], if available and inside + /// the given bounds. + /// + /// If the [`Cursor`] is not over the provided bounds, this method will + /// return `None`. + pub fn position_over(self, bounds: Rectangle) -> Option<Point> { + self.position().filter(|p| bounds.contains(*p)) + } + + /// 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<Point> { + self.position_over(bounds) + .map(|p| p - Vector::new(bounds.x, bounds.y)) + } + + /// Returns the relative position of the [`Cursor`] from the given origin, + /// if available. + pub fn position_from(self, origin: Point) -> Option<Point> { + self.position().map(|p| p - Vector::new(origin.x, origin.y)) + } + + /// Returns true if the [`Cursor`] is over the given `bounds`. + pub fn is_over(self, bounds: Rectangle) -> bool { + self.position_over(bounds).is_some() + } +} diff --git a/core/src/overlay.rs b/core/src/overlay.rs index b9f3c735..2e05db93 100644 --- a/core/src/overlay.rs +++ b/core/src/overlay.rs @@ -38,7 +38,7 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, ); /// Applies a [`widget::Operation`] to the [`Overlay`]. @@ -66,7 +66,7 @@ where &mut self, _event: Event, _layout: Layout<'_>, - _cursor_position: Point, + _cursor: mouse::Cursor, _renderer: &Renderer, _clipboard: &mut dyn Clipboard, _shell: &mut Shell<'_, Message>, @@ -80,7 +80,7 @@ where fn mouse_interaction( &self, _layout: Layout<'_>, - _cursor_position: Point, + _cursor: mouse::Cursor, _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { @@ -91,9 +91,23 @@ where /// /// 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 { + fn is_over( + &self, + layout: Layout<'_>, + _renderer: &Renderer, + cursor_position: Point, + ) -> bool { layout.bounds().contains(cursor_position) } + + /// Returns the nested overlay of the [`Overlay`], if there is any. + fn overlay<'a>( + &'a mut self, + _layout: Layout<'_>, + _renderer: &Renderer, + ) -> Option<Element<'a, Message, Renderer>> { + None + } } /// Returns a [`Group`] of overlay [`Element`] children. diff --git a/core/src/overlay/element.rs b/core/src/overlay/element.rs index 237d25d1..c2134343 100644 --- a/core/src/overlay/element.rs +++ b/core/src/overlay/element.rs @@ -68,35 +68,25 @@ where &mut self, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> event::Status { - self.overlay.on_event( - event, - layout, - cursor_position, - renderer, - clipboard, - shell, - ) + self.overlay + .on_event(event, layout, cursor, renderer, clipboard, shell) } /// Returns the current [`mouse::Interaction`] of the [`Element`]. pub fn mouse_interaction( &self, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - self.overlay.mouse_interaction( - layout, - cursor_position, - viewport, - renderer, - ) + self.overlay + .mouse_interaction(layout, cursor, viewport, renderer) } /// Draws the [`Element`] and its children using the given [`Layout`]. @@ -106,10 +96,9 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, ) { - self.overlay - .draw(renderer, theme, style, layout, cursor_position) + self.overlay.draw(renderer, theme, style, layout, cursor) } /// Applies a [`widget::Operation`] to the [`Element`]. @@ -123,8 +112,22 @@ where } /// 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) + pub fn is_over( + &self, + layout: Layout<'_>, + renderer: &Renderer, + cursor_position: Point, + ) -> bool { + self.overlay.is_over(layout, renderer, cursor_position) + } + + /// Returns the nested overlay of the [`Element`], if there is any. + pub fn overlay<'b>( + &'b mut self, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<Element<'b, Message, Renderer>> { + self.overlay.overlay(layout, renderer) } } @@ -215,7 +218,7 @@ where &mut self, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, B>, @@ -226,7 +229,7 @@ where let event_status = self.content.on_event( event, layout, - cursor_position, + cursor, renderer, clipboard, &mut local_shell, @@ -240,16 +243,12 @@ where fn mouse_interaction( &self, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - self.content.mouse_interaction( - layout, - cursor_position, - viewport, - renderer, - ) + self.content + .mouse_interaction(layout, cursor, viewport, renderer) } fn draw( @@ -258,13 +257,27 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, ) { - self.content - .draw(renderer, theme, style, layout, cursor_position) + self.content.draw(renderer, theme, style, layout, cursor) } - fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { - self.content.is_over(layout, cursor_position) + fn is_over( + &self, + layout: Layout<'_>, + renderer: &Renderer, + cursor_position: Point, + ) -> bool { + self.content.is_over(layout, renderer, cursor_position) + } + + fn overlay<'b>( + &'b mut self, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<Element<'b, B, Renderer>> { + self.content + .overlay(layout, renderer) + .map(|overlay| overlay.map(self.mapper)) } } diff --git a/core/src/overlay/group.rs b/core/src/overlay/group.rs index 0c48df34..deffaad0 100644 --- a/core/src/overlay/group.rs +++ b/core/src/overlay/group.rs @@ -81,7 +81,7 @@ where &mut self, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -93,7 +93,7 @@ where child.on_event( event.clone(), layout, - cursor_position, + cursor, renderer, clipboard, shell, @@ -108,17 +108,17 @@ where theme: &<Renderer as crate::Renderer>::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, ) { for (child, layout) in self.children.iter().zip(layout.children()) { - child.draw(renderer, theme, style, layout, cursor_position); + child.draw(renderer, theme, style, layout, cursor); } } fn mouse_interaction( &self, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { @@ -126,12 +126,7 @@ where .iter() .zip(layout.children()) .map(|(child, layout)| { - child.mouse_interaction( - layout, - cursor_position, - viewport, - renderer, - ) + child.mouse_interaction(layout, cursor, viewport, renderer) }) .max() .unwrap_or_default() @@ -152,11 +147,33 @@ where }); } - fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { + fn is_over( + &self, + layout: Layout<'_>, + renderer: &Renderer, + cursor_position: Point, + ) -> bool { self.children .iter() .zip(layout.children()) - .any(|(child, layout)| child.is_over(layout, cursor_position)) + .any(|(child, layout)| { + child.is_over(layout, renderer, cursor_position) + }) + } + + fn overlay<'b>( + &'b mut self, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + let children = self + .children + .iter_mut() + .zip(layout.children()) + .filter_map(|(child, layout)| child.overlay(layout, renderer)) + .collect::<Vec<_>>(); + + (!children.is_empty()).then(|| Group::with_children(children).overlay()) } } diff --git a/core/src/renderer.rs b/core/src/renderer.rs index d6247e39..7c73d2e4 100644 --- a/core/src/renderer.rs +++ b/core/src/renderer.rs @@ -6,7 +6,7 @@ mod null; pub use null::Null; use crate::layout; -use crate::{Background, Color, Element, Rectangle, Vector}; +use crate::{Background, BorderRadius, Color, Element, Rectangle, Vector}; /// A component that can be used by widgets to draw themselves on a screen. pub trait Renderer: Sized { @@ -60,29 +60,6 @@ pub struct Quad { pub border_color: Color, } -/// The border radi for the corners of a graphics primitive in the order: -/// top-left, top-right, bottom-right, bottom-left. -#[derive(Debug, Clone, Copy, PartialEq, Default)] -pub struct BorderRadius([f32; 4]); - -impl From<f32> 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<BorderRadius> for [f32; 4] { - fn from(radi: BorderRadius) -> Self { - radi.0 - } -} - /// The styling attributes of a [`Renderer`]. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { diff --git a/core/src/widget.rs b/core/src/widget.rs index 769f8659..79d86444 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -15,7 +15,7 @@ use crate::layout::{self, Layout}; use crate::mouse; use crate::overlay; use crate::renderer; -use crate::{Clipboard, Length, Point, Rectangle, Shell}; +use crate::{Clipboard, Length, Rectangle, Shell}; /// A component that displays information and allows interaction. /// @@ -67,7 +67,7 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, ); @@ -111,7 +111,7 @@ where _state: &mut Tree, _event: Event, _layout: Layout<'_>, - _cursor_position: Point, + _cursor: mouse::Cursor, _renderer: &Renderer, _clipboard: &mut dyn Clipboard, _shell: &mut Shell<'_, Message>, @@ -126,7 +126,7 @@ where &self, _state: &Tree, _layout: Layout<'_>, - _cursor_position: Point, + _cursor: mouse::Cursor, _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index 90af88b7..e934a2f5 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -1,12 +1,11 @@ //! Write some text for your users to read. use crate::alignment; use crate::layout; +use crate::mouse; use crate::renderer; use crate::text; use crate::widget::Tree; -use crate::{ - Color, Element, Layout, Length, Pixels, Point, Rectangle, Size, Widget, -}; +use crate::{Color, Element, Layout, Length, Pixels, Rectangle, Size, Widget}; use std::borrow::Cow; @@ -163,7 +162,7 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - _cursor_position: Point, + _cursor_position: mouse::Cursor, _viewport: &Rectangle, ) { draw( diff --git a/core/src/window.rs b/core/src/window.rs index 81bd7e3d..a6dbdfb4 100644 --- a/core/src/window.rs +++ b/core/src/window.rs @@ -2,12 +2,14 @@ pub mod icon; mod event; +mod level; mod mode; mod redraw_request; mod user_attention; pub use event::Event; pub use icon::Icon; +pub use level::Level; pub use mode::Mode; pub use redraw_request::RedrawRequest; pub use user_attention::UserAttention; diff --git a/core/src/window/level.rs b/core/src/window/level.rs new file mode 100644 index 00000000..3878ecac --- /dev/null +++ b/core/src/window/level.rs @@ -0,0 +1,19 @@ +/// A window level groups windows with respect to their z-position. +/// +/// The relative ordering between windows in different window levels is fixed. +/// The z-order of a window within the same window level may change dynamically +/// on user interaction. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum Level { + /// The default behavior. + #[default] + Normal, + + /// The window will always be below normal windows. + /// + /// This is useful for a widget-based app. + AlwaysOnBottom, + + /// The window will always be on top of normal windows. + AlwaysOnTop, +} diff --git a/examples/README.md b/examples/README.md index 74cf145b..111e8910 100644 --- a/examples/README.md +++ b/examples/README.md @@ -93,8 +93,7 @@ A bunch of simpler examples exist: - [`download_progress`](download_progress), a basic application that asynchronously downloads a dummy file of 100 MB and tracks the download progress. - [`events`](events), a log of native events displayed using a conditional `Subscription`. - [`geometry`](geometry), a custom widget showcasing how to draw geometry with the `Mesh2D` primitive in [`iced_wgpu`](../wgpu). -- [`integration_opengl`](integration_opengl), a demonstration of how to integrate Iced in an existing OpenGL application. -- [`integration_wgpu`](integration_wgpu), a demonstration of how to integrate Iced in an existing [`wgpu`] application. +- [`integration`](integration), a demonstration of how to integrate Iced in an existing [`wgpu`] application. - [`pane_grid`](pane_grid), a grid of panes that can be split, resized, and reorganized. - [`pick_list`](pick_list), a dropdown list of selectable options. - [`pokedex`](pokedex), an application that displays a random Pokédex entry (sprite included!) by using the [PokéAPI]. diff --git a/examples/arc/src/main.rs b/examples/arc/src/main.rs index 80ad0b5b..df565859 100644 --- a/examples/arc/src/main.rs +++ b/examples/arc/src/main.rs @@ -1,8 +1,9 @@ use std::{f32::consts::PI, time::Instant}; use iced::executor; +use iced::mouse; use iced::widget::canvas::{ - self, stroke, Cache, Canvas, Cursor, Geometry, Path, Stroke, + self, stroke, Cache, Canvas, Geometry, Path, Stroke, }; use iced::{ Application, Command, Element, Length, Point, Rectangle, Renderer, @@ -78,7 +79,7 @@ impl<Message> canvas::Program<Message> for Arc { renderer: &Renderer, theme: &Theme, bounds: Rectangle, - _cursor: Cursor, + _cursor: mouse::Cursor, ) -> Vec<Geometry> { let geometry = self.cache.draw(renderer, bounds.size(), |frame| { let palette = theme.palette(); diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index f1c83a16..310be28f 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -61,9 +61,7 @@ impl Sandbox for Example { mod bezier { use iced::mouse; use iced::widget::canvas::event::{self, Event}; - use iced::widget::canvas::{ - self, Canvas, Cursor, Frame, Geometry, Path, Stroke, - }; + use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path, Stroke}; use iced::{Element, Length, Point, Rectangle, Renderer, Theme}; #[derive(Default)] @@ -100,10 +98,10 @@ mod bezier { state: &mut Self::State, event: Event, bounds: Rectangle, - cursor: Cursor, + cursor: mouse::Cursor, ) -> (event::Status, Option<Curve>) { let cursor_position = - if let Some(position) = cursor.position_in(&bounds) { + if let Some(position) = cursor.position_in(bounds) { position } else { return (event::Status::Ignored, None); @@ -155,7 +153,7 @@ mod bezier { renderer: &Renderer, _theme: &Theme, bounds: Rectangle, - cursor: Cursor, + cursor: mouse::Cursor, ) -> Vec<Geometry> { let content = self.state.cache.draw( renderer, @@ -183,9 +181,9 @@ mod bezier { &self, _state: &Self::State, bounds: Rectangle, - cursor: Cursor, + cursor: mouse::Cursor, ) -> mouse::Interaction { - if cursor.is_over(&bounds) { + if cursor.is_over(bounds) { mouse::Interaction::Crosshair } else { mouse::Interaction::default() @@ -224,11 +222,11 @@ mod bezier { &self, renderer: &Renderer, bounds: Rectangle, - cursor: Cursor, + cursor: mouse::Cursor, ) -> Geometry { let mut frame = Frame::new(renderer, bounds.size()); - if let Some(cursor_position) = cursor.position_in(&bounds) { + if let Some(cursor_position) = cursor.position_in(bounds) { match *self { Pending::One { from } => { let line = Path::line(from, cursor_position); diff --git a/examples/checkbox/src/main.rs b/examples/checkbox/src/main.rs index ef61a974..ef1a054d 100644 --- a/examples/checkbox/src/main.rs +++ b/examples/checkbox/src/main.rs @@ -31,7 +31,7 @@ impl Application for Example { fn new(_flags: Self::Flags) -> (Self, Command<Message>) { ( Self::default(), - font::load(include_bytes!("../fonts/icons.ttf").as_ref()) + font::load(include_bytes!("../fonts/icons.ttf").as_slice()) .map(Message::FontLoaded), ) } diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index 6425e2da..fae77bc0 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -1,7 +1,6 @@ use iced::executor; -use iced::widget::canvas::{ - stroke, Cache, Cursor, Geometry, LineCap, Path, Stroke, -}; +use iced::mouse; +use iced::widget::canvas::{stroke, Cache, Geometry, LineCap, Path, Stroke}; use iced::widget::{canvas, container}; use iced::{ Application, Color, Command, Element, Length, Point, Rectangle, Renderer, @@ -92,7 +91,7 @@ impl<Message> canvas::Program<Message, Renderer> for Clock { renderer: &Renderer, _theme: &Theme, bounds: Rectangle, - _cursor: Cursor, + _cursor: mouse::Cursor, ) -> Vec<Geometry> { let clock = self.clock.draw(renderer, bounds.size(), |frame| { let center = frame.center(); diff --git a/examples/color_palette/Cargo.toml b/examples/color_palette/Cargo.toml index 8fd37202..3be732bb 100644 --- a/examples/color_palette/Cargo.toml +++ b/examples/color_palette/Cargo.toml @@ -7,4 +7,4 @@ publish = false [dependencies] iced = { path = "../..", features = ["canvas", "palette"] } -palette = "0.6.0" +palette = "0.7.0" diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs index 5c4304ee..736a9d53 100644 --- a/examples/color_palette/src/main.rs +++ b/examples/color_palette/src/main.rs @@ -1,10 +1,14 @@ -use iced::widget::canvas::{self, Canvas, Cursor, Frame, Geometry, Path}; +use iced::alignment::{self, Alignment}; +use iced::mouse; +use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path}; use iced::widget::{column, row, text, Slider}; use iced::{ - alignment, Alignment, Color, Element, Length, Point, Rectangle, Renderer, - Sandbox, Settings, Size, Vector, + Color, Element, Length, Point, Rectangle, Renderer, Sandbox, Settings, + Size, Vector, +}; +use palette::{ + self, convert::FromColor, rgb::Rgb, Darken, Hsl, Lighten, ShiftHue, }; -use palette::{self, convert::FromColor, Hsl, Srgb}; use std::marker::PhantomData; use std::ops::RangeInclusive; @@ -49,12 +53,12 @@ impl Sandbox for ColorPalette { fn update(&mut self, message: Message) { let srgb = match message { - Message::RgbColorChanged(rgb) => palette::Srgb::from(rgb), - Message::HslColorChanged(hsl) => palette::Srgb::from_color(hsl), - Message::HsvColorChanged(hsv) => palette::Srgb::from_color(hsv), - Message::HwbColorChanged(hwb) => palette::Srgb::from_color(hwb), - Message::LabColorChanged(lab) => palette::Srgb::from_color(lab), - Message::LchColorChanged(lch) => palette::Srgb::from_color(lch), + Message::RgbColorChanged(rgb) => Rgb::from(rgb), + Message::HslColorChanged(hsl) => Rgb::from_color(hsl), + Message::HsvColorChanged(hsv) => Rgb::from_color(hsv), + Message::HwbColorChanged(hwb) => Rgb::from_color(hwb), + Message::LabColorChanged(lab) => Rgb::from_color(lab), + Message::LchColorChanged(lch) => Rgb::from_color(lch), }; self.theme = Theme::new(srgb); @@ -63,7 +67,7 @@ impl Sandbox for ColorPalette { fn view(&self) -> Element<Message> { let base = self.theme.base; - let srgb = palette::Srgb::from(base); + let srgb = Rgb::from(base); let hsl = palette::Hsl::from_color(srgb); let hsv = palette::Hsv::from_color(srgb); let hwb = palette::Hwb::from_color(srgb); @@ -95,12 +99,10 @@ struct Theme { impl Theme { pub fn new(base: impl Into<Color>) -> Theme { - use palette::{Hue, Shade}; - let base = base.into(); // Convert to HSL color for manipulation - let hsl = Hsl::from_color(Srgb::from(base)); + let hsl = Hsl::from_color(Rgb::from(base)); let lower = [ hsl.shift_hue(-135.0).lighten(0.075), @@ -119,12 +121,12 @@ impl Theme { Theme { lower: lower .iter() - .map(|&color| Srgb::from_color(color).into()) + .map(|&color| Rgb::from_color(color).into()) .collect(), base, higher: higher .iter() - .map(|&color| Srgb::from_color(color).into()) + .map(|&color| Rgb::from_color(color).into()) .collect(), canvas_cache: canvas::Cache::default(), } @@ -209,14 +211,14 @@ impl Theme { text.vertical_alignment = alignment::Vertical::Bottom; - let hsl = Hsl::from_color(Srgb::from(self.base)); + let hsl = Hsl::from_color(Rgb::from(self.base)); for i in 0..self.len() { let pct = (i as f32 + 1.0) / (self.len() as f32 + 1.0); let graded = Hsl { lightness: 1.0 - pct, ..hsl }; - let color: Color = Srgb::from_color(graded).into(); + let color: Color = Rgb::from_color(graded).into(); let anchor = Point { x: (i as f32) * box_size.width, @@ -246,7 +248,7 @@ impl<Message> canvas::Program<Message> for Theme { renderer: &Renderer, _theme: &iced::Theme, bounds: Rectangle, - _cursor: Cursor, + _cursor: mouse::Cursor, ) -> Vec<Geometry> { let theme = self.canvas_cache.draw(renderer, bounds.size(), |frame| { self.draw(frame); @@ -352,7 +354,7 @@ impl ColorSpace for palette::Hsl { fn components(&self) -> [f32; 3] { [ - self.hue.to_positive_degrees(), + self.hue.into_positive_degrees(), self.saturation, self.lightness, ] @@ -361,7 +363,7 @@ impl ColorSpace for palette::Hsl { fn to_string(&self) -> String { format!( "hsl({:.1}, {:.1}%, {:.1}%)", - self.hue.to_positive_degrees(), + self.hue.into_positive_degrees(), 100.0 * self.saturation, 100.0 * self.lightness ) @@ -378,13 +380,17 @@ impl ColorSpace for palette::Hsv { } fn components(&self) -> [f32; 3] { - [self.hue.to_positive_degrees(), self.saturation, self.value] + [ + self.hue.into_positive_degrees(), + self.saturation, + self.value, + ] } fn to_string(&self) -> String { format!( "hsv({:.1}, {:.1}%, {:.1}%)", - self.hue.to_positive_degrees(), + self.hue.into_positive_degrees(), 100.0 * self.saturation, 100.0 * self.value ) @@ -406,7 +412,7 @@ impl ColorSpace for palette::Hwb { fn components(&self) -> [f32; 3] { [ - self.hue.to_positive_degrees(), + self.hue.into_positive_degrees(), self.whiteness, self.blackness, ] @@ -415,7 +421,7 @@ impl ColorSpace for palette::Hwb { fn to_string(&self) -> String { format!( "hwb({:.1}, {:.1}%, {:.1}%)", - self.hue.to_positive_degrees(), + self.hue.into_positive_degrees(), 100.0 * self.whiteness, 100.0 * self.blackness ) @@ -450,7 +456,7 @@ impl ColorSpace for palette::Lch { } fn components(&self) -> [f32; 3] { - [self.l, self.chroma, self.hue.to_positive_degrees()] + [self.l, self.chroma, self.hue.into_positive_degrees()] } fn to_string(&self) -> String { @@ -458,7 +464,7 @@ impl ColorSpace for palette::Lch { "Lch({:.1}, {:.1}, {:.1})", self.l, self.chroma, - self.hue.to_positive_degrees() + self.hue.into_positive_degrees() ) } } diff --git a/examples/custom_quad/src/main.rs b/examples/custom_quad/src/main.rs index b07f42ce..4b300116 100644 --- a/examples/custom_quad/src/main.rs +++ b/examples/custom_quad/src/main.rs @@ -3,7 +3,8 @@ mod quad { use iced::advanced::layout::{self, Layout}; use iced::advanced::renderer; use iced::advanced::widget::{self, Widget}; - use iced::{Color, Element, Length, Point, Rectangle, Size}; + use iced::mouse; + use iced::{Color, Element, Length, Rectangle, Size}; pub struct CustomQuad { size: f32, @@ -48,7 +49,7 @@ mod quad { _theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, - _cursor_position: Point, + _cursor: mouse::Cursor, _viewport: &Rectangle, ) { renderer.fill_quad( diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs index 7854548c..713bc62d 100644 --- a/examples/custom_widget/src/main.rs +++ b/examples/custom_widget/src/main.rs @@ -12,7 +12,8 @@ mod circle { use iced::advanced::layout::{self, Layout}; use iced::advanced::renderer; use iced::advanced::widget::{self, Widget}; - use iced::{Color, Element, Length, Point, Rectangle, Size}; + use iced::mouse; + use iced::{Color, Element, Length, Rectangle, Size}; pub struct Circle { radius: f32, @@ -55,7 +56,7 @@ mod circle { _theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, - _cursor_position: Point, + _cursor: mouse::Cursor, _viewport: &Rectangle, ) { renderer.fill_quad( diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index eab8908b..e951d734 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -204,15 +204,14 @@ fn view_controls<'a>( mod grid { use crate::Preset; + use iced::alignment; + use iced::mouse; use iced::touch; use iced::widget::canvas; use iced::widget::canvas::event::{self, Event}; - use iced::widget::canvas::{ - Cache, Canvas, Cursor, Frame, Geometry, Path, Text, - }; + use iced::widget::canvas::{Cache, Canvas, Frame, Geometry, Path, Text}; use iced::{ - alignment, mouse, Color, Element, Length, Point, Rectangle, Renderer, - Size, Theme, Vector, + Color, Element, Length, Point, Rectangle, Renderer, Size, Theme, Vector, }; use rustc_hash::{FxHashMap, FxHashSet}; use std::future::Future; @@ -401,14 +400,14 @@ mod grid { interaction: &mut Interaction, event: Event, bounds: Rectangle, - cursor: Cursor, + cursor: mouse::Cursor, ) -> (event::Status, Option<Message>) { if let Event::Mouse(mouse::Event::ButtonReleased(_)) = event { *interaction = Interaction::None; } let cursor_position = - if let Some(position) = cursor.position_in(&bounds) { + if let Some(position) = cursor.position_in(bounds) { position } else { return (event::Status::Ignored, None); @@ -539,7 +538,7 @@ mod grid { renderer: &Renderer, _theme: &Theme, bounds: Rectangle, - cursor: Cursor, + cursor: mouse::Cursor, ) -> Vec<Geometry> { let center = Vector::new(bounds.width / 2.0, bounds.height / 2.0); @@ -568,10 +567,9 @@ mod grid { let overlay = { let mut frame = Frame::new(renderer, bounds.size()); - let hovered_cell = - cursor.position_in(&bounds).map(|position| { - Cell::at(self.project(position, frame.size())) - }); + let hovered_cell = cursor.position_in(bounds).map(|position| { + Cell::at(self.project(position, frame.size())) + }); if let Some(cell) = hovered_cell { frame.with_save(|frame| { @@ -670,13 +668,13 @@ mod grid { &self, interaction: &Interaction, bounds: Rectangle, - cursor: Cursor, + cursor: mouse::Cursor, ) -> mouse::Interaction { match interaction { Interaction::Drawing => mouse::Interaction::Crosshair, Interaction::Erasing => mouse::Interaction::Crosshair, Interaction::Panning { .. } => mouse::Interaction::Grabbing, - Interaction::None if cursor.is_over(&bounds) => { + Interaction::None if cursor.is_over(bounds) => { mouse::Interaction::Crosshair } _ => mouse::Interaction::default(), diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index 5cb41184..29f78ea1 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -3,12 +3,12 @@ mod rainbow { use iced_graphics::primitive::{ColoredVertex2D, Primitive}; + use iced::advanced::graphics::color; 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, - }; + use iced::mouse; + use iced::{Element, Length, Rectangle, Renderer, Size, Theme, Vector}; #[derive(Debug, Clone, Copy, Default)] pub struct Rainbow; @@ -43,13 +43,13 @@ mod rainbow { _theme: &Theme, _style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, ) { use iced::advanced::Renderer as _; use iced_graphics::primitive::Mesh2D; - let b = layout.bounds(); + let bounds = layout.bounds(); // R O Y G B I V let color_r = [1.0, 0.0, 0.0, 1.0]; @@ -62,61 +62,61 @@ mod rainbow { let color_v = [0.75, 0.0, 0.5, 1.0]; let posn_center = { - if b.contains(cursor_position) { - [cursor_position.x - b.x, cursor_position.y - b.y] + if let Some(cursor_position) = cursor.position_in(bounds) { + [cursor_position.x, cursor_position.y] } else { - [b.width / 2.0, b.height / 2.0] + [bounds.width / 2.0, bounds.height / 2.0] } }; let posn_tl = [0.0, 0.0]; - let posn_t = [b.width / 2.0, 0.0]; - let posn_tr = [b.width, 0.0]; - let posn_r = [b.width, b.height / 2.0]; - let posn_br = [b.width, b.height]; - let posn_b = [(b.width / 2.0), b.height]; - let posn_bl = [0.0, b.height]; - let posn_l = [0.0, b.height / 2.0]; + let posn_t = [bounds.width / 2.0, 0.0]; + let posn_tr = [bounds.width, 0.0]; + let posn_r = [bounds.width, bounds.height / 2.0]; + let posn_br = [bounds.width, bounds.height]; + let posn_b = [(bounds.width / 2.0), bounds.height]; + let posn_bl = [0.0, bounds.height]; + let posn_l = [0.0, bounds.height / 2.0]; let mesh = Primitive::SolidMesh { - size: b.size(), + size: bounds.size(), buffers: Mesh2D { vertices: vec![ ColoredVertex2D { position: posn_center, - color: [1.0, 1.0, 1.0, 1.0], + color: color::pack([1.0, 1.0, 1.0, 1.0]), }, ColoredVertex2D { position: posn_tl, - color: color_r, + color: color::pack(color_r), }, ColoredVertex2D { position: posn_t, - color: color_o, + color: color::pack(color_o), }, ColoredVertex2D { position: posn_tr, - color: color_y, + color: color::pack(color_y), }, ColoredVertex2D { position: posn_r, - color: color_g, + color: color::pack(color_g), }, ColoredVertex2D { position: posn_br, - color: color_gb, + color: color::pack(color_gb), }, ColoredVertex2D { position: posn_b, - color: color_b, + color: color::pack(color_b), }, ColoredVertex2D { position: posn_bl, - color: color_i, + color: color::pack(color_i), }, ColoredVertex2D { position: posn_l, - color: color_v, + color: color::pack(color_v), }, ], indices: vec![ @@ -132,9 +132,12 @@ mod rainbow { }, }; - renderer.with_translation(Vector::new(b.x, b.y), |renderer| { - renderer.draw_primitive(mesh); - }); + renderer.with_translation( + Vector::new(bounds.x, bounds.y), + |renderer| { + renderer.draw_primitive(mesh); + }, + ); } } diff --git a/examples/integration/Cargo.toml b/examples/integration/Cargo.toml index 2ab5d75f..22914742 100644 --- a/examples/integration/Cargo.toml +++ b/examples/integration/Cargo.toml @@ -9,7 +9,7 @@ publish = false iced_winit = { path = "../../winit" } iced_wgpu = { path = "../../wgpu" } iced_widget = { path = "../../widget" } -iced_renderer = { path = "../../renderer", features = ["wgpu", "tiny-skia"] } +iced_renderer = { path = "../../renderer", features = ["wgpu"] } env_logger = "0.10" [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index c935aca7..342d4c69 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -6,6 +6,7 @@ use scene::Scene; use iced_wgpu::graphics::Viewport; use iced_wgpu::{wgpu, Backend, Renderer, Settings}; +use iced_winit::core::mouse; use iced_winit::core::renderer; use iced_winit::core::{Color, Size}; use iced_winit::runtime::program; @@ -14,7 +15,6 @@ use iced_winit::style::Theme; use iced_winit::{conversion, futures, winit, Clipboard}; use winit::{ - dpi::PhysicalPosition, event::{Event, ModifiersState, WindowEvent}, event_loop::{ControlFlow, EventLoop}, }; @@ -39,6 +39,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> { .and_then(|element| element.dyn_into::<HtmlCanvasElement>().ok()) .expect("Get canvas element") }; + #[cfg(not(target_arch = "wasm32"))] env_logger::init(); @@ -58,7 +59,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> { Size::new(physical_size.width, physical_size.height), window.scale_factor(), ); - let mut cursor_position = PhysicalPosition::new(-1.0, -1.0); + let mut cursor_position = None; let mut modifiers = ModifiersState::default(); let mut clipboard = Clipboard::connect(&window); @@ -165,7 +166,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> { Event::WindowEvent { event, .. } => { match event { WindowEvent::CursorMoved { position, .. } => { - cursor_position = position; + cursor_position = Some(position); } WindowEvent::ModifiersChanged(new_modifiers) => { modifiers = new_modifiers; @@ -194,13 +195,20 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> { // We update iced let _ = state.update( viewport.logical_size(), - conversion::cursor_position( - cursor_position, - viewport.scale_factor(), - ), + cursor_position + .map(|p| { + conversion::cursor_position( + p, + viewport.scale_factor(), + ) + }) + .map(mouse::Cursor::Available) + .unwrap_or(mouse::Cursor::Unavailable), &mut renderer, &Theme::Dark, - &renderer::Style { text_color: Color::WHITE }, + &renderer::Style { + text_color: Color::WHITE, + }, &mut clipboard, &mut debug, ); @@ -242,7 +250,9 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> { let program = state.program(); - let view = frame.texture.create_view(&wgpu::TextureViewDescriptor::default()); + let view = frame.texture.create_view( + &wgpu::TextureViewDescriptor::default(), + ); { // We clear the frame @@ -275,15 +285,18 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> { frame.present(); // Update the mouse cursor - window.set_cursor_icon( - iced_winit::conversion::mouse_interaction( - state.mouse_interaction(), - ), - ); + window.set_cursor_icon( + iced_winit::conversion::mouse_interaction( + state.mouse_interaction(), + ), + ); } Err(error) => match error { wgpu::SurfaceError::OutOfMemory => { - panic!("Swapchain error: {error}. Rendering cannot continue.") + panic!( + "Swapchain error: {error}. \ + Rendering cannot continue." + ) } _ => { // Try rendering again next frame. diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index f48afb69..7fcbbfe4 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -1,12 +1,15 @@ +use iced::executor; +use iced::keyboard; +use iced::subscription::{self, Subscription}; +use iced::theme; use iced::widget::{ - self, button, column, container, horizontal_space, row, text, text_input, -}; -use iced::{ - executor, keyboard, subscription, theme, Alignment, Application, Command, - Element, Event, Length, Settings, Subscription, + self, button, column, container, horizontal_space, pick_list, row, text, + text_input, }; +use iced::{Alignment, Application, Command, Element, Event, Length, Settings}; -use self::modal::Modal; +use modal::Modal; +use std::fmt; pub fn main() -> iced::Result { App::run(Settings::default()) @@ -17,6 +20,7 @@ struct App { show_modal: bool, email: String, password: String, + plan: Plan, } #[derive(Debug, Clone)] @@ -25,6 +29,7 @@ enum Message { HideModal, Email(String), Password(String), + Plan(Plan), Submit, Event(Event), } @@ -65,6 +70,10 @@ impl Application for App { self.password = password; Command::none() } + Message::Plan(plan) => { + self.plan = plan; + Command::none() + } Message::Submit => { if !self.email.is_empty() && !self.password.is_empty() { self.hide_modal(); @@ -148,6 +157,16 @@ impl Application for App { .padding(5), ] .spacing(5), + column![ + text("Plan").size(12), + pick_list( + Plan::ALL, + Some(self.plan), + Message::Plan + ) + .padding(5), + ] + .spacing(5), button(text("Submit")).on_press(Message::HideModal), ] .spacing(10) @@ -175,6 +194,29 @@ impl App { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +enum Plan { + #[default] + Basic, + Pro, + Enterprise, +} + +impl Plan { + pub const ALL: &[Self] = &[Self::Basic, Self::Pro, Self::Enterprise]; +} + +impl fmt::Display for Plan { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Plan::Basic => "Basic", + Plan::Pro => "Pro", + Plan::Enterprise => "Enterprise", + } + .fmt(f) + } +} + mod modal { use iced::advanced::layout::{self, Layout}; use iced::advanced::overlay; @@ -254,7 +296,7 @@ mod modal { state: &mut widget::Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -263,7 +305,7 @@ mod modal { &mut state.children[0], event, layout, - cursor_position, + cursor, renderer, clipboard, shell, @@ -277,7 +319,7 @@ mod modal { theme: &<Renderer as advanced::Renderer>::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, ) { self.base.as_widget().draw( @@ -286,7 +328,7 @@ mod modal { theme, style, layout, - cursor_position, + cursor, viewport, ); } @@ -312,14 +354,14 @@ mod modal { &self, state: &widget::Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { self.base.as_widget().mouse_interaction( &state.children[0], layout, - cursor_position, + cursor, viewport, renderer, ) @@ -377,7 +419,7 @@ mod modal { &mut self, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -389,7 +431,7 @@ mod modal { mouse::Button::Left, )) = &event { - if !content_bounds.contains(cursor_position) { + if !cursor.is_over(content_bounds) { shell.publish(message.clone()); return event::Status::Captured; } @@ -400,7 +442,7 @@ mod modal { self.tree, event, layout.children().next().unwrap(), - cursor_position, + cursor, renderer, clipboard, shell, @@ -413,12 +455,12 @@ mod modal { theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, ) { renderer.fill_quad( renderer::Quad { bounds: layout.bounds(), - border_radius: renderer::BorderRadius::from(0.0), + border_radius: Default::default(), border_width: 0.0, border_color: Color::TRANSPARENT, }, @@ -434,7 +476,7 @@ mod modal { theme, style, layout.children().next().unwrap(), - cursor_position, + cursor, &layout.bounds(), ); } @@ -456,18 +498,30 @@ mod modal { fn mouse_interaction( &self, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { self.content.as_widget().mouse_interaction( self.tree, layout.children().next().unwrap(), - cursor_position, + cursor, viewport, renderer, ) } + + fn overlay<'c>( + &'c mut self, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'c, Message, Renderer>> { + self.content.as_widget_mut().overlay( + self.tree, + layout.children().next().unwrap(), + renderer, + ) + } } impl<'a, Message, Renderer> From<Modal<'a, Message, Renderer>> diff --git a/examples/modern_art/Cargo.toml b/examples/modern_art/Cargo.toml deleted file mode 100644 index 4242d209..00000000 --- a/examples/modern_art/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "modern_art" -version = "0.1.0" -authors = ["Bingus <shankern@protonmail.com>"] -edition = "2021" -publish = false - -[dependencies] -iced = { path = "../..", features = ["canvas", "tokio", "debug"] } -rand = "0.8.5" -env_logger = "0.9" diff --git a/examples/modern_art/src/main.rs b/examples/modern_art/src/main.rs deleted file mode 100644 index a43a2b2b..00000000 --- a/examples/modern_art/src/main.rs +++ /dev/null @@ -1,143 +0,0 @@ -use iced::widget::canvas::{ - self, gradient::Location, gradient::Position, Cache, Canvas, Cursor, Frame, - Geometry, Gradient, -}; -use iced::{ - executor, Application, Color, Command, Element, Length, Point, Rectangle, - Renderer, Settings, Size, Theme, -}; -use rand::{thread_rng, Rng}; - -fn main() -> iced::Result { - env_logger::builder().format_timestamp(None).init(); - - ModernArt::run(Settings { - antialiasing: true, - ..Settings::default() - }) -} - -#[derive(Debug, Clone, Copy)] -enum Message {} - -struct ModernArt { - cache: Cache, -} - -impl Application for ModernArt { - type Executor = executor::Default; - type Message = Message; - type Theme = Theme; - type Flags = (); - - fn new(_flags: Self::Flags) -> (Self, Command<Self::Message>) { - ( - ModernArt { - cache: Default::default(), - }, - Command::none(), - ) - } - - fn title(&self) -> String { - String::from("Modern Art") - } - - fn update(&mut self, _message: Message) -> Command<Message> { - Command::none() - } - - fn view(&self) -> Element<'_, Self::Message, Renderer<Self::Theme>> { - Canvas::new(self) - .width(Length::Fill) - .height(Length::Fill) - .into() - } -} - -impl<Message> canvas::Program<Message, Renderer> for ModernArt { - type State = (); - - fn draw( - &self, - _state: &Self::State, - renderer: &Renderer, - _theme: &Theme, - bounds: Rectangle, - _cursor: Cursor, - ) -> Vec<Geometry> { - let geometry = self.cache.draw(renderer, bounds.size(), |frame| { - let num_squares = thread_rng().gen_range(0..1200); - - let mut i = 0; - while i <= num_squares { - generate_box(frame, bounds.size()); - i += 1; - } - }); - - vec![geometry] - } -} - -fn random_direction() -> Location { - match thread_rng().gen_range(0..8) { - 0 => Location::TopLeft, - 1 => Location::Top, - 2 => Location::TopRight, - 3 => Location::Right, - 4 => Location::BottomRight, - 5 => Location::Bottom, - 6 => Location::BottomLeft, - 7 => Location::Left, - _ => Location::TopLeft, - } -} - -fn generate_box(frame: &mut Frame, bounds: Size) -> bool { - let solid = rand::random::<bool>(); - - let random_color = || -> Color { - Color::from_rgb( - thread_rng().gen_range(0.0..1.0), - thread_rng().gen_range(0.0..1.0), - thread_rng().gen_range(0.0..1.0), - ) - }; - - let gradient = |top_left: Point, size: Size| -> Gradient { - let mut builder = Gradient::linear(Position::Relative { - top_left, - size, - start: random_direction(), - end: random_direction(), - }); - let stops = thread_rng().gen_range(1..15u32); - - let mut i = 0; - while i <= stops { - builder = builder.add_stop(i as f32 / stops as f32, random_color()); - i += 1; - } - - builder.build().unwrap() - }; - - let top_left = Point::new( - thread_rng().gen_range(0.0..bounds.width), - thread_rng().gen_range(0.0..bounds.height), - ); - - let size = Size::new( - thread_rng().gen_range(50.0..200.0), - thread_rng().gen_range(50.0..200.0), - ); - - if solid { - frame.fill_rectangle(top_left, size, random_color()); - } else { - frame.fill_rectangle(top_left, size, gradient(top_left, size)); - }; - - solid -} diff --git a/examples/multitouch/src/main.rs b/examples/multitouch/src/main.rs index 7df6c929..2830b78d 100644 --- a/examples/multitouch/src/main.rs +++ b/examples/multitouch/src/main.rs @@ -1,9 +1,10 @@ //! This example shows how to use touch events in `Canvas` to draw //! a circle around each fingertip. This only works on touch-enabled //! computers like Microsoft Surface. +use iced::mouse; use iced::widget::canvas::event; use iced::widget::canvas::stroke::{self, Stroke}; -use iced::widget::canvas::{self, Canvas, Cursor, Geometry}; +use iced::widget::canvas::{self, Canvas, Geometry}; use iced::{ executor, touch, window, Application, Color, Command, Element, Length, Point, Rectangle, Renderer, Settings, Subscription, Theme, @@ -103,7 +104,7 @@ impl canvas::Program<Message, Renderer> for State { _state: &mut Self::State, event: event::Event, _bounds: Rectangle, - _cursor: Cursor, + _cursor: mouse::Cursor, ) -> (event::Status, Option<Message>) { match event { event::Event::Touch(touch_event) => match touch_event { @@ -128,7 +129,7 @@ impl canvas::Program<Message, Renderer> for State { renderer: &Renderer, _theme: &Theme, bounds: Rectangle, - _cursor: Cursor, + _cursor: mouse::Cursor, ) -> Vec<Geometry> { let fingerweb = self.cache.draw(renderer, bounds.size(), |frame| { if self.fingers.len() < 2 { diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs index dfb80853..54c36d69 100644 --- a/examples/pane_grid/src/main.rs +++ b/examples/pane_grid/src/main.rs @@ -108,8 +108,9 @@ impl Application for Example { Message::Dragged(pane_grid::DragEvent::Dropped { pane, target, + region, }) => { - self.panes.swap(&pane, &target); + self.panes.split_with(&target, &pane, region); } Message::Dragged(_) => {} Message::TogglePin(pane) => { @@ -255,6 +256,7 @@ fn handle_hotkey(key_code: keyboard::KeyCode) -> Option<Message> { } } +#[derive(Clone, Copy)] struct Pane { id: usize, pub is_pinned: bool, @@ -298,7 +300,7 @@ fn view_content<'a>( ) ] .spacing(5) - .max_width(150); + .max_width(160); if total_panes > 1 && !is_pinned { controls = controls.push( diff --git a/examples/screenshot/Cargo.toml b/examples/screenshot/Cargo.toml new file mode 100644 index 00000000..b79300b7 --- /dev/null +++ b/examples/screenshot/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "screenshot" +version = "0.1.0" +authors = ["Bingus <shankern@protonmail.com>"] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../..", features = ["debug", "image", "advanced"] } +image = { version = "0.24.6", features = ["png"]} +env_logger = "0.10.0" diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs new file mode 100644 index 00000000..83824535 --- /dev/null +++ b/examples/screenshot/src/main.rs @@ -0,0 +1,320 @@ +use iced::alignment; +use iced::keyboard::KeyCode; +use iced::theme::{Button, Container}; +use iced::widget::{button, column, container, image, row, text, text_input}; +use iced::window::screenshot::{self, Screenshot}; +use iced::{ + event, executor, keyboard, subscription, Alignment, Application, Command, + ContentFit, Element, Event, Length, Rectangle, Renderer, Subscription, + Theme, +}; + +use ::image as img; +use ::image::ColorType; + +fn main() -> iced::Result { + env_logger::builder().format_timestamp(None).init(); + + Example::run(iced::Settings::default()) +} + +struct Example { + screenshot: Option<Screenshot>, + saved_png_path: Option<Result<String, PngError>>, + png_saving: bool, + crop_error: Option<screenshot::CropError>, + x_input_value: Option<u32>, + y_input_value: Option<u32>, + width_input_value: Option<u32>, + height_input_value: Option<u32>, +} + +#[derive(Clone, Debug)] +enum Message { + Crop, + Screenshot, + ScreenshotData(Screenshot), + Png, + PngSaved(Result<String, PngError>), + XInputChanged(Option<u32>), + YInputChanged(Option<u32>), + WidthInputChanged(Option<u32>), + HeightInputChanged(Option<u32>), +} + +impl Application for Example { + type Executor = executor::Default; + type Message = Message; + type Theme = Theme; + type Flags = (); + + fn new(_flags: Self::Flags) -> (Self, Command<Self::Message>) { + ( + Example { + screenshot: None, + saved_png_path: None, + png_saving: false, + crop_error: None, + x_input_value: None, + y_input_value: None, + width_input_value: None, + height_input_value: None, + }, + Command::none(), + ) + } + + fn title(&self) -> String { + "Screenshot".to_string() + } + + fn update(&mut self, message: Self::Message) -> Command<Self::Message> { + match message { + Message::Screenshot => { + return iced::window::screenshot(Message::ScreenshotData); + } + Message::ScreenshotData(screenshot) => { + self.screenshot = Some(screenshot); + } + Message::Png => { + if let Some(screenshot) = &self.screenshot { + self.png_saving = true; + + return Command::perform( + save_to_png(screenshot.clone()), + Message::PngSaved, + ); + } + } + Message::PngSaved(res) => { + self.png_saving = false; + self.saved_png_path = Some(res); + } + Message::XInputChanged(new_value) => { + self.x_input_value = new_value; + } + Message::YInputChanged(new_value) => { + self.y_input_value = new_value; + } + Message::WidthInputChanged(new_value) => { + self.width_input_value = new_value; + } + Message::HeightInputChanged(new_value) => { + self.height_input_value = new_value; + } + Message::Crop => { + if let Some(screenshot) = &self.screenshot { + let cropped = screenshot.crop(Rectangle::<u32> { + x: self.x_input_value.unwrap_or(0), + y: self.y_input_value.unwrap_or(0), + width: self.width_input_value.unwrap_or(0), + height: self.height_input_value.unwrap_or(0), + }); + + match cropped { + Ok(screenshot) => { + self.screenshot = Some(screenshot); + self.crop_error = None; + } + Err(crop_error) => { + self.crop_error = Some(crop_error); + } + } + } + } + } + + Command::none() + } + + fn view(&self) -> Element<'_, Self::Message, Renderer<Self::Theme>> { + let image: Element<Message> = if let Some(screenshot) = &self.screenshot + { + image(image::Handle::from_pixels( + screenshot.size.width, + screenshot.size.height, + screenshot.clone(), + )) + .content_fit(ContentFit::Contain) + .width(Length::Fill) + .height(Length::Fill) + .into() + } else { + text("Press the button to take a screenshot!").into() + }; + + let image = container(image) + .padding(10) + .style(Container::Box) + .width(Length::FillPortion(2)) + .height(Length::Fill) + .center_x() + .center_y(); + + let crop_origin_controls = row![ + text("X:") + .vertical_alignment(alignment::Vertical::Center) + .width(30), + numeric_input("0", self.x_input_value).map(Message::XInputChanged), + text("Y:") + .vertical_alignment(alignment::Vertical::Center) + .width(30), + numeric_input("0", self.y_input_value).map(Message::YInputChanged) + ] + .spacing(10) + .align_items(Alignment::Center); + + let crop_dimension_controls = row![ + text("W:") + .vertical_alignment(alignment::Vertical::Center) + .width(30), + numeric_input("0", self.width_input_value) + .map(Message::WidthInputChanged), + text("H:") + .vertical_alignment(alignment::Vertical::Center) + .width(30), + numeric_input("0", self.height_input_value) + .map(Message::HeightInputChanged) + ] + .spacing(10) + .align_items(Alignment::Center); + + let mut crop_controls = + column![crop_origin_controls, crop_dimension_controls] + .spacing(10) + .align_items(Alignment::Center); + + if let Some(crop_error) = &self.crop_error { + crop_controls = crop_controls + .push(text(format!("Crop error! \n{}", crop_error))); + } + + let mut controls = column![ + column![ + button(centered_text("Screenshot!")) + .padding([10, 20, 10, 20]) + .width(Length::Fill) + .on_press(Message::Screenshot), + if !self.png_saving { + button(centered_text("Save as png")).on_press_maybe( + self.screenshot.is_some().then(|| Message::Png), + ) + } else { + button(centered_text("Saving...")).style(Button::Secondary) + } + .style(Button::Secondary) + .padding([10, 20, 10, 20]) + .width(Length::Fill) + ] + .spacing(10), + column![ + crop_controls, + button(centered_text("Crop")) + .on_press(Message::Crop) + .style(Button::Destructive) + .padding([10, 20, 10, 20]) + .width(Length::Fill), + ] + .spacing(10) + .align_items(Alignment::Center), + ] + .spacing(40); + + if let Some(png_result) = &self.saved_png_path { + let msg = match png_result { + Ok(path) => format!("Png saved as: {:?}!", path), + Err(msg) => { + format!("Png could not be saved due to:\n{:?}", msg) + } + }; + + controls = controls.push(text(msg)); + } + + let side_content = container(controls) + .align_x(alignment::Horizontal::Center) + .width(Length::FillPortion(1)) + .height(Length::Fill) + .center_y() + .center_x(); + + let content = row![side_content, image] + .spacing(10) + .width(Length::Fill) + .height(Length::Fill) + .align_items(Alignment::Center); + + container(content) + .width(Length::Fill) + .height(Length::Fill) + .padding(10) + .center_x() + .center_y() + .into() + } + + fn subscription(&self) -> Subscription<Self::Message> { + subscription::events_with(|event, status| { + if let event::Status::Captured = status { + return None; + } + + if let Event::Keyboard(keyboard::Event::KeyPressed { + key_code: KeyCode::F5, + .. + }) = event + { + Some(Message::Screenshot) + } else { + None + } + }) + } +} + +async fn save_to_png(screenshot: Screenshot) -> Result<String, PngError> { + let path = "screenshot.png".to_string(); + img::save_buffer( + &path, + &screenshot.bytes, + screenshot.size.width, + screenshot.size.height, + ColorType::Rgba8, + ) + .map(|_| path) + .map_err(|err| PngError(format!("{:?}", err))) +} + +#[derive(Clone, Debug)] +struct PngError(String); + +fn numeric_input( + placeholder: &str, + value: Option<u32>, +) -> Element<'_, Option<u32>> { + text_input( + placeholder, + &value + .as_ref() + .map(ToString::to_string) + .unwrap_or_else(String::new), + ) + .on_input(move |text| { + if text.is_empty() { + None + } else if let Ok(new_value) = text.parse() { + Some(new_value) + } else { + value + } + }) + .width(40) + .into() +} + +fn centered_text(content: &str) -> Element<'_, Message> { + text(content) + .width(Length::Fill) + .horizontal_alignment(alignment::Horizontal::Center) + .into() +} diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index 97344c94..3038661e 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -289,18 +289,13 @@ impl Application for ScrollableDemo { } Direction::Horizontal => { progress_bar(0.0..=1.0, self.current_scroll_offset.x) - .style(theme::ProgressBar::Custom(Box::new( - ProgressBarCustomStyle, - ))) + .style(progress_bar_custom_style) .into() } Direction::Multi => column![ progress_bar(0.0..=1.0, self.current_scroll_offset.y), - progress_bar(0.0..=1.0, self.current_scroll_offset.x).style( - theme::ProgressBar::Custom(Box::new( - ProgressBarCustomStyle, - )) - ) + progress_bar(0.0..=1.0, self.current_scroll_offset.x) + .style(progress_bar_custom_style) ] .spacing(10) .into(), @@ -356,12 +351,12 @@ impl scrollable::StyleSheet for ScrollbarCustomStyle { background: style .active(&theme::Scrollable::default()) .background, - border_radius: 0.0, + border_radius: 0.0.into(), border_width: 0.0, border_color: Default::default(), scroller: Scroller { color: Color::from_rgb8(250, 85, 134), - border_radius: 0.0, + border_radius: 0.0.into(), border_width: 0.0, border_color: Default::default(), }, @@ -372,16 +367,10 @@ impl scrollable::StyleSheet for ScrollbarCustomStyle { } } -struct ProgressBarCustomStyle; - -impl progress_bar::StyleSheet for ProgressBarCustomStyle { - type Style = Theme; - - fn appearance(&self, style: &Self::Style) -> progress_bar::Appearance { - progress_bar::Appearance { - background: style.extended_palette().background.strong.color.into(), - bar: Color::from_rgb8(250, 85, 134).into(), - border_radius: 0.0, - } +fn progress_bar_custom_style(theme: &Theme) -> progress_bar::Appearance { + progress_bar::Appearance { + background: theme.extended_palette().background.strong.color.into(), + bar: Color::from_rgb8(250, 85, 134).into(), + border_radius: 0.0.into(), } } diff --git a/examples/sierpinski_triangle/src/main.rs b/examples/sierpinski_triangle/src/main.rs index 4faac6d6..885d3c63 100644 --- a/examples/sierpinski_triangle/src/main.rs +++ b/examples/sierpinski_triangle/src/main.rs @@ -1,6 +1,7 @@ use std::fmt::Debug; use iced::executor; +use iced::mouse; use iced::widget::canvas::event::{self, Event}; use iced::widget::canvas::{self, Canvas}; use iced::widget::{column, row, slider, text}; @@ -105,14 +106,14 @@ impl canvas::Program<Message> for SierpinskiGraph { _state: &mut Self::State, event: Event, bounds: Rectangle, - cursor: canvas::Cursor, + cursor: mouse::Cursor, ) -> (event::Status, Option<Message>) { - let cursor_position = - if let Some(position) = cursor.position_in(&bounds) { - position - } else { - return (event::Status::Ignored, None); - }; + let cursor_position = if let Some(position) = cursor.position_in(bounds) + { + position + } else { + return (event::Status::Ignored, None); + }; match event { Event::Mouse(mouse_event) => { @@ -137,7 +138,7 @@ impl canvas::Program<Message> for SierpinskiGraph { renderer: &Renderer, _theme: &Theme, bounds: Rectangle, - _cursor: canvas::Cursor, + _cursor: mouse::Cursor, ) -> Vec<canvas::Geometry> { let geom = self.cache.draw(renderer, bounds.size(), |frame| { frame.stroke( diff --git a/examples/solar_system/Cargo.toml b/examples/solar_system/Cargo.toml index 835396b0..1a98a87e 100644 --- a/examples/solar_system/Cargo.toml +++ b/examples/solar_system/Cargo.toml @@ -7,4 +7,5 @@ publish = false [dependencies] iced = { path = "../..", features = ["canvas", "tokio", "debug"] } +env_logger = "0.10.0" rand = "0.8.3" diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index f2606feb..58d06206 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -8,11 +8,12 @@ //! [1]: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Basic_animations#An_animated_solar_system use iced::application; use iced::executor; +use iced::mouse; use iced::theme::{self, Theme}; use iced::widget::canvas; -use iced::widget::canvas::gradient::{self, Gradient}; +use iced::widget::canvas::gradient; use iced::widget::canvas::stroke::{self, Stroke}; -use iced::widget::canvas::{Cursor, Path}; +use iced::widget::canvas::Path; use iced::window; use iced::{ Application, Color, Command, Element, Length, Point, Rectangle, Renderer, @@ -22,6 +23,8 @@ use iced::{ use std::time::Instant; pub fn main() -> iced::Result { + env_logger::builder().format_timestamp(None).init(); + SolarSystem::run(Settings { antialiasing: true, ..Settings::default() @@ -159,7 +162,7 @@ impl<Message> canvas::Program<Message> for State { renderer: &Renderer, _theme: &Theme, bounds: Rectangle, - _cursor: Cursor, + _cursor: mouse::Cursor, ) -> Vec<canvas::Geometry> { use std::f32::consts::PI; @@ -208,15 +211,12 @@ impl<Message> canvas::Program<Message> for State { let earth = Path::circle(Point::ORIGIN, Self::EARTH_RADIUS); - let earth_fill = - Gradient::linear(gradient::Position::Absolute { - start: Point::new(-Self::EARTH_RADIUS, 0.0), - end: Point::new(Self::EARTH_RADIUS, 0.0), - }) - .add_stop(0.2, Color::from_rgb(0.15, 0.50, 1.0)) - .add_stop(0.8, Color::from_rgb(0.0, 0.20, 0.47)) - .build() - .expect("Build Earth fill gradient"); + let earth_fill = gradient::Linear::new( + Point::new(-Self::EARTH_RADIUS, 0.0), + Point::new(Self::EARTH_RADIUS, 0.0), + ) + .add_stop(0.2, Color::from_rgb(0.15, 0.50, 1.0)) + .add_stop(0.8, Color::from_rgb(0.0, 0.20, 0.47)); frame.fill(&earth, earth_fill); diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index e2015bac..f8a4c80a 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -127,7 +127,9 @@ impl Sandbox for Styling { let content = column![ choose_theme, horizontal_rule(38), - row![text_input, button].spacing(10), + row![text_input, button] + .spacing(10) + .align_items(Alignment::Center), slider, progress_bar, row![ diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index 9d859258..4282ddcf 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -1,10 +1,10 @@ +use iced::executor; +use iced::keyboard; +use iced::subscription::{self, Subscription}; use iced::widget::{ self, button, column, container, pick_list, row, slider, text, text_input, }; -use iced::{ - executor, keyboard, subscription, Alignment, Application, Command, Element, - Event, Length, Settings, Subscription, -}; +use iced::{Alignment, Application, Command, Element, Event, Length, Settings}; use toast::{Status, Toast}; @@ -226,7 +226,7 @@ mod toast { }; container::Appearance { - background: pair.color.into(), + background: Some(pair.color.into()), text_color: pair.text.into(), ..Default::default() } @@ -396,7 +396,7 @@ mod toast { state: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -405,7 +405,7 @@ mod toast { &mut state.children[0], event, layout, - cursor_position, + cursor, renderer, clipboard, shell, @@ -419,7 +419,7 @@ mod toast { theme: &Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, ) { self.content.as_widget().draw( @@ -428,7 +428,7 @@ mod toast { theme, style, layout, - cursor_position, + cursor, viewport, ); } @@ -437,14 +437,14 @@ mod toast { &self, state: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { self.content.as_widget().mouse_interaction( &state.children[0], layout, - cursor_position, + cursor, viewport, renderer, ) @@ -523,7 +523,7 @@ mod toast { &mut self, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -572,7 +572,7 @@ mod toast { state, event.clone(), layout, - cursor_position, + cursor, renderer, clipboard, &mut local_shell, @@ -595,7 +595,7 @@ mod toast { theme: &<Renderer as advanced::Renderer>::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, ) { let viewport = layout.bounds(); @@ -606,13 +606,7 @@ mod toast { .zip(layout.children()) { child.as_widget().draw( - state, - renderer, - theme, - style, - layout, - cursor_position, - &viewport, + state, renderer, theme, style, layout, cursor, &viewport, ); } } @@ -639,7 +633,7 @@ mod toast { fn mouse_interaction( &self, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { @@ -649,18 +643,19 @@ mod toast { .zip(layout.children()) .map(|((child, state), layout)| { child.as_widget().mouse_interaction( - state, - layout, - cursor_position, - viewport, - renderer, + state, layout, cursor, viewport, renderer, ) }) .max() .unwrap_or_default() } - fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { + fn is_over( + &self, + layout: Layout<'_>, + _renderer: &Renderer, + cursor_position: Point, + ) -> bool { layout .children() .any(|layout| layout.bounds().contains(cursor_position)) diff --git a/examples/tour/Cargo.toml b/examples/tour/Cargo.toml index 39e83671..48471f2d 100644 --- a/examples/tour/Cargo.toml +++ b/examples/tour/Cargo.toml @@ -7,4 +7,4 @@ publish = false [dependencies] iced = { path = "../..", features = ["image", "debug"] } -env_logger = "0.8" +env_logger = "0.10.0" diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index 9c38ad0e..13bcd5ff 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -62,11 +62,8 @@ impl Sandbox for Tour { controls = controls.push(horizontal_space(Length::Fill)); if steps.can_continue() { - controls = controls.push( - button("Next") - .on_press(Message::NextPressed) - .style(theme::Button::Primary), - ); + controls = + controls.push(button("Next").on_press(Message::NextPressed)); } let content: Element<_> = column![ diff --git a/futures/src/backend/native.rs b/futures/src/backend/native.rs index 4199ad16..85af2c88 100644 --- a/futures/src/backend/native.rs +++ b/futures/src/backend/native.rs @@ -1,16 +1,12 @@ //! Backends that are only available in native platforms: Windows, macOS, or Linux. -#[cfg_attr(docsrs, doc(cfg(feature = "tokio",)))] #[cfg(feature = "tokio")] pub mod tokio; -#[cfg_attr(docsrs, doc(cfg(feature = "async-std",)))] #[cfg(feature = "async-std")] pub mod async_std; -#[cfg_attr(docsrs, doc(cfg(feature = "smol",)))] #[cfg(feature = "smol")] pub mod smol; -#[cfg_attr(docsrs, doc(cfg(feature = "thread-pool",)))] #[cfg(feature = "thread-pool")] pub mod thread_pool; diff --git a/futures/src/backend/native/smol.rs b/futures/src/backend/native/smol.rs index 30bc8291..00d13d35 100644 --- a/futures/src/backend/native/smol.rs +++ b/futures/src/backend/native/smol.rs @@ -1,9 +1,7 @@ //! A `smol` backend. - use futures::Future; /// A `smol` executor. -#[cfg_attr(docsrs, doc(cfg(feature = "smol")))] #[derive(Debug)] pub struct Executor; diff --git a/futures/src/backend/native/thread_pool.rs b/futures/src/backend/native/thread_pool.rs index da5d4b9b..c96f2682 100644 --- a/futures/src/backend/native/thread_pool.rs +++ b/futures/src/backend/native/thread_pool.rs @@ -2,7 +2,6 @@ use futures::Future; /// A thread pool executor for futures. -#[cfg_attr(docsrs, doc(cfg(feature = "thread-pool")))] pub type Executor = futures::executor::ThreadPool; impl crate::Executor for Executor { diff --git a/futures/src/lib.rs b/futures/src/lib.rs index 397fc2d2..34d81e1e 100644 --- a/futures/src/lib.rs +++ b/futures/src/lib.rs @@ -16,7 +16,7 @@ )] #![forbid(unsafe_code, rust_2018_idioms)] #![allow(clippy::inherent_to_string, clippy::type_complexity)] -#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] pub use futures; pub use iced_core as core; diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs index 801c2694..0642a924 100644 --- a/futures/src/subscription.rs +++ b/futures/src/subscription.rs @@ -417,7 +417,7 @@ where pub fn channel<I, Fut, Message>( id: I, size: usize, - f: impl Fn(mpsc::Sender<Message>) -> Fut + MaybeSend + Sync + 'static, + f: impl FnOnce(mpsc::Sender<Message>) -> Fut + MaybeSend + 'static, ) -> Subscription<Message> where I: Hash + 'static, diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index 125ea17d..02621695 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -14,9 +14,11 @@ categories = ["gui"] geometry = ["lyon_path"] opengl = [] image = ["dep:image", "kamadak-exif"] +web-colors = [] [dependencies] -glam = "0.21.3" +glam = "0.24" +half = "2.2.1" log = "0.4" raw-window-handle = "0.5" thiserror = "1.0" diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs index ae89da06..6a082576 100644 --- a/graphics/src/backend.rs +++ b/graphics/src/backend.rs @@ -6,18 +6,6 @@ use iced_core::{Font, Point, Size}; use std::borrow::Cow; -/// The graphics backend of a [`Renderer`]. -/// -/// [`Renderer`]: crate::Renderer -pub trait Backend { - /// Trims the measurements cache. - /// - /// This method is currently necessary to properly trim the text cache in - /// `iced_wgpu` and `iced_glow` because of limitations in the text rendering - /// pipeline. It will be removed in the future. - fn trim_measurements(&mut self) {} -} - /// A graphics backend that supports text rendering. pub trait Text { /// The icon font of the backend. diff --git a/graphics/src/color.rs b/graphics/src/color.rs new file mode 100644 index 00000000..92448a68 --- /dev/null +++ b/graphics/src/color.rs @@ -0,0 +1,46 @@ +//! Manage colors for shaders. +use crate::core::Color; + +use bytemuck::{Pod, Zeroable}; + +/// A color packed as 4 floats representing RGBA channels. +#[derive(Debug, Clone, Copy, PartialEq, Zeroable, Pod)] +#[repr(C)] +pub struct Packed([f32; 4]); + +impl Packed { + /// Returns the internal components of the [`Packed`] color. + pub fn components(self) -> [f32; 4] { + self.0 + } +} + +/// A flag that indicates whether the renderer should perform gamma correction. +pub const GAMMA_CORRECTION: bool = internal::GAMMA_CORRECTION; + +/// Packs a [`Color`]. +pub fn pack(color: impl Into<Color>) -> Packed { + Packed(internal::pack(color.into())) +} + +#[cfg(not(feature = "web-colors"))] +mod internal { + use crate::core::Color; + + pub const GAMMA_CORRECTION: bool = true; + + pub fn pack(color: Color) -> [f32; 4] { + color.into_linear() + } +} + +#[cfg(feature = "web-colors")] +mod internal { + use crate::core::Color; + + pub const GAMMA_CORRECTION: bool = false; + + pub fn pack(color: Color) -> [f32; 4] { + [color.r, color.g, color.b, color.a] + } +} diff --git a/graphics/src/compositor.rs b/graphics/src/compositor.rs index d55e801a..f7b86045 100644 --- a/graphics/src/compositor.rs +++ b/graphics/src/compositor.rs @@ -59,6 +59,19 @@ pub trait Compositor: Sized { background_color: Color, overlay: &[T], ) -> Result<(), SurfaceError>; + + /// Screenshots the current [`Renderer`] primitives to an offscreen texture, and returns the bytes of + /// the texture ordered as `RGBA` in the sRGB color space. + /// + /// [`Renderer`]: Self::Renderer; + fn screenshot<T: AsRef<str>>( + &mut self, + renderer: &mut Self::Renderer, + surface: &mut Self::Surface, + viewport: &Viewport, + background_color: Color, + overlay: &[T], + ) -> Vec<u8>; } /// Result of an unsuccessful call to [`Compositor::present`]. @@ -82,7 +95,7 @@ pub enum SurfaceError { OutOfMemory, } -/// Contains informations about the graphics (e.g. graphics adapter, graphics backend). +/// Contains information about the graphics (e.g. graphics adapter, graphics backend). #[derive(Debug)] pub struct Information { /// Contains the graphics adapter. diff --git a/graphics/src/damage.rs b/graphics/src/damage.rs index 5aab06b1..c6b0f759 100644 --- a/graphics/src/damage.rs +++ b/graphics/src/damage.rs @@ -1,8 +1,10 @@ +//! Track and compute the damage of graphical primitives. use crate::core::{Rectangle, Size}; use crate::Primitive; use std::sync::Arc; +/// Computes the damage regions between the two given primitives. pub fn regions(a: &Primitive, b: &Primitive) -> Vec<Rectangle> { match (a, b) { ( @@ -73,6 +75,7 @@ pub fn regions(a: &Primitive, b: &Primitive) -> Vec<Rectangle> { } } +/// Computes the damage regions between the two given lists of primitives. pub fn list(previous: &[Primitive], current: &[Primitive]) -> Vec<Rectangle> { let damage = previous .iter() @@ -95,6 +98,8 @@ pub fn list(previous: &[Primitive], current: &[Primitive]) -> Vec<Rectangle> { } } +/// Groups the given damage regions that are close together inside the given +/// bounds. pub fn group( mut damage: Vec<Rectangle>, scale_factor: f32, diff --git a/graphics/src/geometry.rs b/graphics/src/geometry.rs index 8db1594a..729c3d44 100644 --- a/graphics/src/geometry.rs +++ b/graphics/src/geometry.rs @@ -1,8 +1,4 @@ -//! 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! +//! Build and draw geometry. pub mod fill; pub mod path; pub mod stroke; @@ -16,10 +12,11 @@ pub use stroke::{LineCap, LineDash, LineJoin, Stroke}; pub use style::Style; pub use text::Text; -pub use iced_core::gradient::{self, Gradient}; +pub use crate::gradient::{self, Gradient}; use crate::Primitive; +/// A bunch of shapes that can be drawn. #[derive(Debug, Clone)] pub struct Geometry(pub Primitive); @@ -29,8 +26,8 @@ impl From<Geometry> for Primitive { } } -pub trait Renderer: iced_core::Renderer { - type Geometry; - - fn draw(&mut self, geometry: Vec<Self::Geometry>); +/// A renderer capable of drawing some [`Geometry`]. +pub trait Renderer: crate::core::Renderer { + /// Draws the given layers of [`Geometry`]. + fn draw(&mut self, layers: Vec<Geometry>); } diff --git a/graphics/src/geometry/fill.rs b/graphics/src/geometry/fill.rs index 2e8c1669..b773c99b 100644 --- a/graphics/src/geometry/fill.rs +++ b/graphics/src/geometry/fill.rs @@ -1,8 +1,9 @@ //! Fill [crate::widget::canvas::Geometry] with a certain style. -use iced_core::{Color, Gradient}; - pub use crate::geometry::Style; +use crate::core::Color; +use crate::gradient::{self, Gradient}; + /// The style used to fill geometry. #[derive(Debug, Clone)] pub struct Fill { @@ -49,6 +50,15 @@ impl From<Gradient> for Fill { } } +impl From<gradient::Linear> for Fill { + fn from(gradient: gradient::Linear) -> Self { + Fill { + style: Style::Gradient(Gradient::Linear(gradient)), + ..Default::default() + } + } +} + /// The fill rule defines how to determine what is inside and what is outside of /// a shape. /// diff --git a/graphics/src/geometry/path.rs b/graphics/src/geometry/path.rs index c3127bdf..3d8fc6fa 100644 --- a/graphics/src/geometry/path.rs +++ b/graphics/src/geometry/path.rs @@ -53,11 +53,13 @@ impl Path { Self::new(|p| p.circle(center, radius)) } + /// Returns the internal [`lyon_path::Path`]. #[inline] pub fn raw(&self) -> &lyon_path::Path { &self.raw } + /// Returns the current [`Path`] with the given transform applied to it. #[inline] pub fn transform(&self, transform: &lyon_path::math::Transform) -> Path { Path { diff --git a/graphics/src/geometry/style.rs b/graphics/src/geometry/style.rs index be9ee376..a0f4b08a 100644 --- a/graphics/src/geometry/style.rs +++ b/graphics/src/geometry/style.rs @@ -1,4 +1,5 @@ -use iced_core::{Color, Gradient}; +use crate::core::Color; +use crate::geometry::Gradient; /// The coloring style of some drawing. #[derive(Debug, Clone, PartialEq)] diff --git a/graphics/src/gradient.rs b/graphics/src/gradient.rs new file mode 100644 index 00000000..3f5d0509 --- /dev/null +++ b/graphics/src/gradient.rs @@ -0,0 +1,195 @@ +//! A gradient that can be used as a [`Fill`] for some geometry. +//! +//! For a gradient that you can use as a background variant for a widget, see [`Gradient`]. +//! +//! [`Gradient`]: crate::core::Gradient; +use crate::color; +use crate::core::gradient::ColorStop; +use crate::core::{self, Color, Point, Rectangle}; + +use half::f16; +use std::cmp::Ordering; + +#[derive(Debug, Clone, PartialEq)] +/// A fill which linearly interpolates colors along a direction. +/// +/// For a gradient which can be used as a fill for a background of a widget, see [`crate::core::Gradient`]. +pub enum Gradient { + /// A linear gradient interpolates colors along a direction from its `start` to its `end` + /// point. + Linear(Linear), +} + +impl From<Linear> for Gradient { + fn from(gradient: Linear) -> Self { + Self::Linear(gradient) + } +} + +impl Gradient { + /// Packs the [`Gradient`] for use in shader code. + pub fn pack(&self) -> Packed { + match self { + Gradient::Linear(linear) => linear.pack(), + } + } +} + +/// A linear gradient that can be used in the style of [`Fill`] or [`Stroke`]. +/// +/// [`Fill`]: crate::geometry::Fill; +/// [`Stroke`]: crate::geometry::Stroke; +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Linear { + /// The absolute starting position of the gradient. + pub start: Point, + + /// The absolute ending position of the gradient. + pub end: Point, + + /// [`ColorStop`]s along the linear gradient direction. + pub stops: [Option<ColorStop>; 8], +} + +impl Linear { + /// Creates a new [`Builder`]. + pub fn new(start: Point, end: Point) -> Self { + Self { + start, + end, + stops: [None; 8], + } + } + + /// Adds a new [`ColorStop`], defined by an offset and a color, to the gradient. + /// + /// Any `offset` that is not within `0.0..=1.0` will be silently ignored. + /// + /// Any stop added after the 8th will be silently ignored. + pub fn add_stop(mut self, offset: f32, color: Color) -> Self { + if offset.is_finite() && (0.0..=1.0).contains(&offset) { + let (Ok(index) | Err(index)) = + self.stops.binary_search_by(|stop| match stop { + None => Ordering::Greater, + Some(stop) => stop.offset.partial_cmp(&offset).unwrap(), + }); + + if index < 8 { + self.stops[index] = Some(ColorStop { offset, color }); + } + } else { + log::warn!("Gradient: ColorStop must be within 0.0..=1.0 range."); + }; + + self + } + + /// Adds multiple [`ColorStop`]s to the gradient. + /// + /// Any stop added after the 8th will be silently ignored. + pub fn add_stops( + mut self, + stops: impl IntoIterator<Item = ColorStop>, + ) -> Self { + for stop in stops.into_iter() { + self = self.add_stop(stop.offset, stop.color) + } + + self + } + + /// Packs the [`Gradient`] for use in shader code. + pub fn pack(&self) -> Packed { + let mut colors = [[0u32; 2]; 8]; + let mut offsets = [f16::from(0u8); 8]; + + for (index, stop) in self.stops.iter().enumerate() { + let [r, g, b, a] = + color::pack(stop.map_or(Color::default(), |s| s.color)) + .components(); + + colors[index] = [ + pack_f16s([f16::from_f32(r), f16::from_f32(g)]), + pack_f16s([f16::from_f32(b), f16::from_f32(a)]), + ]; + + offsets[index] = + stop.map_or(f16::from_f32(2.0), |s| f16::from_f32(s.offset)); + } + + let offsets = [ + pack_f16s([offsets[0], offsets[1]]), + pack_f16s([offsets[2], offsets[3]]), + pack_f16s([offsets[4], offsets[5]]), + pack_f16s([offsets[6], offsets[7]]), + ]; + + let direction = [self.start.x, self.start.y, self.end.x, self.end.y]; + + Packed { + colors, + offsets, + direction, + } + } +} + +/// Packed [`Gradient`] data for use in shader code. +#[derive(Debug, Copy, Clone, PartialEq)] +#[repr(C)] +pub struct Packed { + // 8 colors, each channel = 16 bit float, 2 colors packed into 1 u32 + colors: [[u32; 2]; 8], + // 8 offsets, 8x 16 bit floats packed into 4 u32s + offsets: [u32; 4], + direction: [f32; 4], +} + +/// Creates a new [`Packed`] gradient for use in shader code. +pub fn pack(gradient: &core::Gradient, bounds: Rectangle) -> Packed { + match gradient { + core::Gradient::Linear(linear) => { + let mut colors = [[0u32; 2]; 8]; + let mut offsets = [f16::from(0u8); 8]; + + for (index, stop) in linear.stops.iter().enumerate() { + let [r, g, b, a] = + color::pack(stop.map_or(Color::default(), |s| s.color)) + .components(); + + colors[index] = [ + pack_f16s([f16::from_f32(r), f16::from_f32(g)]), + pack_f16s([f16::from_f32(b), f16::from_f32(a)]), + ]; + + offsets[index] = stop + .map_or(f16::from_f32(2.0), |s| f16::from_f32(s.offset)); + } + + let offsets = [ + pack_f16s([offsets[0], offsets[1]]), + pack_f16s([offsets[2], offsets[3]]), + pack_f16s([offsets[4], offsets[5]]), + pack_f16s([offsets[6], offsets[7]]), + ]; + + let (start, end) = linear.angle.to_distance(&bounds); + + let direction = [start.x, start.y, end.x, end.y]; + + Packed { + colors, + offsets, + direction, + } + } + } +} + +/// Packs two f16s into one u32. +fn pack_f16s(f: [f16; 2]) -> u32 { + let one = (f[0].to_bits() as u32) << 16; + let two = f[1].to_bits() as u32; + + one | two +} diff --git a/graphics/src/image.rs b/graphics/src/image.rs index 2f634252..6b43f4a8 100644 --- a/graphics/src/image.rs +++ b/graphics/src/image.rs @@ -5,6 +5,7 @@ use bitflags::bitflags; pub use ::image as image_rs; +/// Tries to load an image by its [`Handle`]. pub fn load(handle: &Handle) -> image_rs::ImageResult<image_rs::DynamicImage> { match handle.data() { Data::Path(path) => { diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index e3de4025..226b245b 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, @@ -20,15 +20,17 @@ )] #![forbid(rust_2018_idioms)] #![allow(clippy::inherent_to_string, clippy::type_complexity)] -#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] mod antialiasing; mod error; mod transformation; mod viewport; pub mod backend; +pub mod color; pub mod compositor; pub mod damage; +pub mod gradient; pub mod primitive; pub mod renderer; @@ -39,9 +41,9 @@ pub mod geometry; pub mod image; pub use antialiasing::Antialiasing; -pub use backend::Backend; pub use compositor::Compositor; pub use error::Error; +pub use gradient::Gradient; pub use primitive::Primitive; pub use renderer::Renderer; pub use transformation::Transformation; diff --git a/graphics/src/primitive.rs b/graphics/src/primitive.rs index d814c757..187c6c6c 100644 --- a/graphics/src/primitive.rs +++ b/graphics/src/primitive.rs @@ -1,8 +1,11 @@ +//! Draw using different graphical primitives. +use crate::color; use crate::core::alignment; use crate::core::image; use crate::core::svg; use crate::core::text; -use crate::core::{Background, Color, Font, Gradient, Rectangle, Size, Vector}; +use crate::core::{Background, Color, Font, Rectangle, Size, Vector}; +use crate::gradient; use bytemuck::{Pod, Zeroable}; use std::sync::Arc; @@ -38,7 +41,7 @@ pub enum Primitive { bounds: Rectangle, /// The background of the quad background: Background, - /// The border radius of the quad + /// The border radii of the quad border_radius: [f32; 4], /// The border width of the quad border_width: f32, @@ -80,28 +83,35 @@ pub enum Primitive { /// It can be used to render many kinds of geometry freely. GradientMesh { /// The vertices and indices of the mesh. - buffers: Mesh2D<Vertex2D>, + buffers: Mesh2D<GradientVertex2D>, /// The size of the drawable region of the mesh. /// /// Any geometry that falls out of this region will be clipped. size: Size, - - /// The [`Gradient`] to apply to the mesh. - gradient: Gradient, }, + /// A [`tiny_skia`] path filled with some paint. #[cfg(feature = "tiny-skia")] Fill { + /// The path to fill. path: tiny_skia::Path, + /// The paint to use. paint: tiny_skia::Paint<'static>, + /// The fill rule to follow. rule: tiny_skia::FillRule, + /// The transform to apply to the path. transform: tiny_skia::Transform, }, + /// A [`tiny_skia`] path stroked with some paint. #[cfg(feature = "tiny-skia")] Stroke { + /// The path to stroke. path: tiny_skia::Path, + /// The paint to use. paint: tiny_skia::Paint<'static>, + /// The stroke settings. stroke: tiny_skia::Stroke, + /// The transform to apply to the path. transform: tiny_skia::Transform, }, /// A group of primitives @@ -135,10 +145,12 @@ pub enum Primitive { } impl Primitive { + /// Creates a [`Primitive::Group`]. pub fn group(primitives: Vec<Self>) -> Self { Self::Group { primitives } } + /// Creates a [`Primitive::Clip`]. pub fn clip(self, bounds: Rectangle) -> Self { Self::Clip { bounds, @@ -146,6 +158,7 @@ impl Primitive { } } + /// Creates a [`Primitive::Translate`]. pub fn translate(self, translation: Vector) -> Self { Self::Translate { translation, @@ -153,6 +166,7 @@ impl Primitive { } } + /// Returns the bounds of the [`Primitive`]. pub fn bounds(&self) -> Rectangle { match self { Self::Text { @@ -227,25 +241,34 @@ pub struct Mesh2D<T> { pub indices: Vec<u32>, } -/// A two-dimensional vertex. +/// A two-dimensional vertex with a color. #[derive(Copy, Clone, Debug, PartialEq, Zeroable, Pod)] #[repr(C)] -pub struct Vertex2D { +pub struct ColoredVertex2D { /// The vertex position in 2D space. pub position: [f32; 2], + + /// The color of the vertex in __linear__ RGBA. + pub color: color::Packed, } -/// A two-dimensional vertex with a color. -#[derive(Copy, Clone, Debug, PartialEq, Zeroable, Pod)] +/// A vertex which contains 2D position & packed gradient data. +#[derive(Copy, Clone, Debug, PartialEq)] #[repr(C)] -pub struct ColoredVertex2D { +pub struct GradientVertex2D { /// The vertex position in 2D space. pub position: [f32; 2], - /// The color of the vertex in __linear__ RGBA. - pub color: [f32; 4], + /// The packed vertex data of the gradient. + pub gradient: gradient::Packed, } +#[allow(unsafe_code)] +unsafe impl Zeroable for GradientVertex2D {} + +#[allow(unsafe_code)] +unsafe impl Pod for GradientVertex2D {} + impl From<()> for Primitive { fn from(_: ()) -> Self { Self::Group { primitives: vec![] } diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index aaf1737a..4241d45c 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -1,5 +1,5 @@ //! Create a renderer from a [`Backend`]. -use crate::backend::{self, Backend}; +use crate::backend; use crate::Primitive; use iced_core::image; @@ -16,13 +16,13 @@ use std::marker::PhantomData; /// A backend-agnostic renderer that supports all the built-in widgets. #[derive(Debug)] -pub struct Renderer<B: Backend, Theme> { +pub struct Renderer<B, Theme> { backend: B, primitives: Vec<Primitive>, theme: PhantomData<Theme>, } -impl<B: Backend, T> Renderer<B, T> { +impl<B, T> Renderer<B, T> { /// Creates a new [`Renderer`] from the given [`Backend`]. pub fn new(backend: B) -> Self { Self { @@ -52,10 +52,7 @@ impl<B: Backend, T> Renderer<B, T> { } } -impl<B, T> iced_core::Renderer for Renderer<B, T> -where - B: Backend, -{ +impl<B, T> iced_core::Renderer for Renderer<B, T> { type Theme = T; fn layout<Message>( @@ -63,11 +60,7 @@ where element: &Element<'_, Message, Self>, limits: &layout::Limits, ) -> layout::Node { - let layout = element.as_widget().layout(self, limits); - - self.backend.trim_measurements(); - - layout + element.as_widget().layout(self, limits) } fn with_layer(&mut self, bounds: Rectangle, f: impl FnOnce(&mut Self)) { @@ -116,7 +109,7 @@ where impl<B, T> text::Renderer for Renderer<B, T> where - B: Backend + backend::Text, + B: backend::Text, { type Font = Font; @@ -195,7 +188,7 @@ where impl<B, T> image::Renderer for Renderer<B, T> where - B: Backend + backend::Image, + B: backend::Image, { type Handle = image::Handle; @@ -210,7 +203,7 @@ where impl<B, T> svg::Renderer for Renderer<B, T> where - B: Backend + backend::Svg, + B: backend::Svg, { fn dimensions(&self, handle: &svg::Handle) -> Size<u32> { self.backend().viewport_dimensions(handle) @@ -231,13 +224,8 @@ where } #[cfg(feature = "geometry")] -impl<B, T> crate::geometry::Renderer for Renderer<B, T> -where - B: Backend, -{ - type Geometry = crate::Geometry; - - fn draw(&mut self, layers: Vec<Self::Geometry>) { +impl<B, T> crate::geometry::Renderer for Renderer<B, T> { + fn draw(&mut self, layers: Vec<crate::Geometry>) { self.primitives .extend(layers.into_iter().map(crate::Geometry::into)); } diff --git a/graphics/src/triangle.rs b/graphics/src/triangle.rs deleted file mode 100644 index 09b61767..00000000 --- a/graphics/src/triangle.rs +++ /dev/null @@ -1 +0,0 @@ -//! Draw geometry using meshes of triangles. diff --git a/renderer/Cargo.toml b/renderer/Cargo.toml index 17657888..ddfb6445 100644 --- a/renderer/Cargo.toml +++ b/renderer/Cargo.toml @@ -5,11 +5,11 @@ edition = "2021" [features] wgpu = ["iced_wgpu"] -tiny-skia = ["iced_tiny_skia"] -image = ["iced_wgpu/image", "iced_tiny_skia/image"] -svg = ["iced_wgpu/svg", "iced_tiny_skia/svg"] -geometry = ["iced_graphics/geometry", "iced_wgpu?/geometry", "iced_tiny_skia?/geometry"] -tracing = ["iced_wgpu/tracing"] +image = ["iced_tiny_skia/image", "iced_wgpu?/image"] +svg = ["iced_tiny_skia/svg", "iced_wgpu?/svg"] +geometry = ["iced_graphics/geometry", "iced_tiny_skia/geometry", "iced_wgpu?/geometry"] +tracing = ["iced_wgpu?/tracing"] +web-colors = ["iced_wgpu?/web-colors"] [dependencies] raw-window-handle = "0.5" @@ -19,12 +19,11 @@ thiserror = "1" version = "0.8" path = "../graphics" -[dependencies.iced_wgpu] -version = "0.10" -path = "../wgpu" -optional = true - [dependencies.iced_tiny_skia] version = "0.1" path = "../tiny_skia" + +[dependencies.iced_wgpu] +version = "0.10" +path = "../wgpu" optional = true diff --git a/renderer/src/backend.rs b/renderer/src/backend.rs index c1bec6af..18f9f3fc 100644 --- a/renderer/src/backend.rs +++ b/renderer/src/backend.rs @@ -6,29 +6,21 @@ use std::borrow::Cow; #[allow(clippy::large_enum_variant)] pub enum Backend { + TinySkia(iced_tiny_skia::Backend), #[cfg(feature = "wgpu")] Wgpu(iced_wgpu::Backend), - #[cfg(feature = "tiny-skia")] - TinySkia(iced_tiny_skia::Backend), } macro_rules! delegate { ($backend:expr, $name:ident, $body:expr) => { match $backend { + Self::TinySkia($name) => $body, #[cfg(feature = "wgpu")] Self::Wgpu($name) => $body, - #[cfg(feature = "tiny-skia")] - Self::TinySkia($name) => $body, } }; } -impl iced_graphics::Backend for Backend { - fn trim_measurements(&mut self) { - delegate!(self, backend, backend.trim_measurements()); - } -} - impl backend::Text for Backend { const ICON_FONT: Font = Font::with_name("Iced-Icons"); const CHECKMARK_ICON: char = '\u{f00c}'; diff --git a/renderer/src/compositor.rs b/renderer/src/compositor.rs index 48ed4b1f..57317b28 100644 --- a/renderer/src/compositor.rs +++ b/renderer/src/compositor.rs @@ -7,17 +7,15 @@ use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; use std::env; pub enum Compositor<Theme> { + TinySkia(iced_tiny_skia::window::Compositor<Theme>), #[cfg(feature = "wgpu")] Wgpu(iced_wgpu::window::Compositor<Theme>), - #[cfg(feature = "tiny-skia")] - TinySkia(iced_tiny_skia::window::Compositor<Theme>), } pub enum Surface { + TinySkia(iced_tiny_skia::window::Surface), #[cfg(feature = "wgpu")] Wgpu(iced_wgpu::window::Surface), - #[cfg(feature = "tiny-skia")] - TinySkia(iced_tiny_skia::window::Surface), } impl<Theme> crate::graphics::Compositor for Compositor<Theme> { @@ -55,14 +53,13 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { height: u32, ) -> Surface { match self { + Self::TinySkia(compositor) => Surface::TinySkia( + compositor.create_surface(window, width, height), + ), #[cfg(feature = "wgpu")] Self::Wgpu(compositor) => { Surface::Wgpu(compositor.create_surface(window, width, height)) } - #[cfg(feature = "tiny-skia")] - Self::TinySkia(compositor) => Surface::TinySkia( - compositor.create_surface(window, width, height), - ), } } @@ -73,12 +70,11 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { height: u32, ) { match (self, surface) { - #[cfg(feature = "wgpu")] - (Self::Wgpu(compositor), Surface::Wgpu(surface)) => { + (Self::TinySkia(compositor), Surface::TinySkia(surface)) => { compositor.configure_surface(surface, width, height); } - #[cfg(feature = "tiny-skia")] - (Self::TinySkia(compositor), Surface::TinySkia(surface)) => { + #[cfg(feature = "wgpu")] + (Self::Wgpu(compositor), Surface::Wgpu(surface)) => { compositor.configure_surface(surface, width, height); } #[allow(unreachable_patterns)] @@ -90,10 +86,9 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { fn fetch_information(&self) -> Information { match self { + Self::TinySkia(compositor) => compositor.fetch_information(), #[cfg(feature = "wgpu")] Self::Wgpu(compositor) => compositor.fetch_information(), - #[cfg(feature = "tiny-skia")] - Self::TinySkia(compositor) => compositor.fetch_information(), } } @@ -107,13 +102,11 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { ) -> Result<(), SurfaceError> { renderer.with_primitives(|backend, primitives| { match (self, backend, surface) { - #[cfg(feature = "wgpu")] ( - Self::Wgpu(compositor), - crate::Backend::Wgpu(backend), - Surface::Wgpu(surface), - ) => iced_wgpu::window::compositor::present( - compositor, + Self::TinySkia(_compositor), + crate::Backend::TinySkia(backend), + Surface::TinySkia(surface), + ) => iced_tiny_skia::window::compositor::present( backend, surface, primitives, @@ -121,12 +114,13 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { background_color, overlay, ), - #[cfg(feature = "tiny-skia")] + #[cfg(feature = "wgpu")] ( - Self::TinySkia(_compositor), - crate::Backend::TinySkia(backend), - Surface::TinySkia(surface), - ) => iced_tiny_skia::window::compositor::present( + Self::Wgpu(compositor), + crate::Backend::Wgpu(backend), + Surface::Wgpu(surface), + ) => iced_wgpu::window::compositor::present( + compositor, backend, surface, primitives, @@ -142,6 +136,36 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { } }) } + + fn screenshot<T: AsRef<str>>( + &mut self, + renderer: &mut Self::Renderer, + surface: &mut Self::Surface, + viewport: &Viewport, + background_color: Color, + overlay: &[T], + ) -> Vec<u8> { + renderer.with_primitives(|backend, primitives| match (self, backend, surface) { + (Self::TinySkia(_compositor), crate::Backend::TinySkia(backend), Surface::TinySkia(surface)) => { + iced_tiny_skia::window::compositor::screenshot(surface, backend, primitives, viewport, background_color, overlay) + }, + #[cfg(feature = "wgpu")] + (Self::Wgpu(compositor), crate::Backend::Wgpu(backend), Surface::Wgpu(_)) => { + iced_wgpu::window::compositor::screenshot( + compositor, + backend, + primitives, + viewport, + background_color, + overlay, + ) + }, + #[allow(unreachable_patterns)] + _ => panic!( + "The provided renderer or backend are not compatible with the compositor." + ), + }) + } } enum Candidate { @@ -154,7 +178,6 @@ impl Candidate { vec![ #[cfg(feature = "wgpu")] Self::Wgpu, - #[cfg(feature = "tiny-skia")] Self::TinySkia, ] } @@ -181,6 +204,20 @@ impl Candidate { _compatible_window: Option<&W>, ) -> Result<(Compositor<Theme>, Renderer<Theme>), Error> { match self { + Self::TinySkia => { + 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, + }, + ); + + Ok(( + Compositor::TinySkia(compositor), + Renderer::new(crate::Backend::TinySkia(backend)), + )) + } #[cfg(feature = "wgpu")] Self::Wgpu => { let (compositor, backend) = iced_wgpu::window::compositor::new( @@ -198,31 +235,10 @@ impl Candidate { Renderer::new(crate::Backend::Wgpu(backend)), )) } - #[cfg(feature = "tiny-skia")] - Self::TinySkia => { - 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, - }, - ); - - Ok(( - Compositor::TinySkia(compositor), - Renderer::new(crate::Backend::TinySkia(backend)), - )) - } #[cfg(not(feature = "wgpu"))] Self::Wgpu => { panic!("`wgpu` feature was not enabled in `iced_renderer`") } - #[cfg(not(feature = "tiny-skia"))] - Self::TinySkia => { - panic!( - "`tiny-skia` feature was not enabled in `iced_renderer`" - ); - } } } } diff --git a/renderer/src/geometry.rs b/renderer/src/geometry.rs index 21ef2c06..26e2fed0 100644 --- a/renderer/src/geometry.rs +++ b/renderer/src/geometry.rs @@ -7,19 +7,17 @@ use crate::graphics::geometry::{Fill, Geometry, Path, Stroke, Text}; use crate::Backend; pub enum Frame { + TinySkia(iced_tiny_skia::geometry::Frame), #[cfg(feature = "wgpu")] Wgpu(iced_wgpu::geometry::Frame), - #[cfg(feature = "tiny-skia")] - TinySkia(iced_tiny_skia::geometry::Frame), } macro_rules! delegate { ($frame:expr, $name:ident, $body:expr) => { match $frame { + Self::TinySkia($name) => $body, #[cfg(feature = "wgpu")] Self::Wgpu($name) => $body, - #[cfg(feature = "tiny-skia")] - Self::TinySkia($name) => $body, } }; } @@ -27,14 +25,13 @@ macro_rules! delegate { impl Frame { pub fn new<Theme>(renderer: &crate::Renderer<Theme>, size: Size) -> Self { match renderer.backend() { + Backend::TinySkia(_) => { + Frame::TinySkia(iced_tiny_skia::geometry::Frame::new(size)) + } #[cfg(feature = "wgpu")] Backend::Wgpu(_) => { Frame::Wgpu(iced_wgpu::geometry::Frame::new(size)) } - #[cfg(feature = "tiny-skia")] - Backend::TinySkia(_) => { - Frame::TinySkia(iced_tiny_skia::geometry::Frame::new(size)) - } } } @@ -127,28 +124,26 @@ impl Frame { #[inline] pub fn with_clip(&mut self, region: Rectangle, f: impl FnOnce(&mut Frame)) { let mut frame = match self { + Self::TinySkia(_) => Self::TinySkia( + iced_tiny_skia::geometry::Frame::new(region.size()), + ), #[cfg(feature = "wgpu")] Self::Wgpu(_) => { Self::Wgpu(iced_wgpu::geometry::Frame::new(region.size())) } - #[cfg(feature = "tiny-skia")] - Self::TinySkia(_) => Self::TinySkia( - iced_tiny_skia::geometry::Frame::new(region.size()), - ), }; f(&mut frame); - let translation = Vector::new(region.x, region.y); + let origin = Point::new(region.x, region.y); match (self, frame) { + (Self::TinySkia(target), Self::TinySkia(frame)) => { + target.clip(frame, origin); + } #[cfg(feature = "wgpu")] (Self::Wgpu(target), Self::Wgpu(frame)) => { - target.clip(frame, translation); - } - #[cfg(feature = "tiny-skia")] - (Self::TinySkia(target), Self::TinySkia(frame)) => { - target.clip(frame, translation); + target.clip(frame, origin); } #[allow(unreachable_patterns)] _ => unreachable!(), diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index ba737b11..22ec7bd1 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -1,6 +1,3 @@ -#[cfg(not(any(feature = "wgpu", feature = "tiny-skia")))] -compile_error!("No backend selected. Enable at least one backend feature: `wgpu` or `tiny-skia`."); - pub mod compositor; #[cfg(feature = "geometry")] diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 8a277e47..4bbf9687 100644 --- a/runtime/src/lib.rs +++ b/runtime/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, @@ -42,11 +42,12 @@ clippy::useless_conversion )] #![forbid(unsafe_code, rust_2018_idioms)] -#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] pub mod clipboard; pub mod command; pub mod font; pub mod keyboard; +pub mod overlay; pub mod program; pub mod system; pub mod user_interface; diff --git a/runtime/src/overlay.rs b/runtime/src/overlay.rs new file mode 100644 index 00000000..03390980 --- /dev/null +++ b/runtime/src/overlay.rs @@ -0,0 +1,4 @@ +//! Overlays for user interfaces. +mod nested; + +pub use nested::Nested; diff --git a/runtime/src/overlay/nested.rs b/runtime/src/overlay/nested.rs new file mode 100644 index 00000000..b729f769 --- /dev/null +++ b/runtime/src/overlay/nested.rs @@ -0,0 +1,353 @@ +use crate::core::event; +use crate::core::layout; +use crate::core::mouse; +use crate::core::overlay; +use crate::core::renderer; +use crate::core::widget; +use crate::core::{Clipboard, Event, Layout, Point, Rectangle, Shell, Size}; + +/// An [`Overlay`] container that displays nested overlays +#[allow(missing_debug_implementations)] +pub struct Nested<'a, Message, Renderer> { + overlay: overlay::Element<'a, Message, Renderer>, +} + +impl<'a, Message, Renderer> Nested<'a, Message, Renderer> +where + Renderer: renderer::Renderer, +{ + /// Creates a nested overlay from the provided [`overlay::Element`] + pub fn new(element: overlay::Element<'a, Message, Renderer>) -> Self { + Self { overlay: element } + } + + /// Returns the position of the [`Nested`] overlay. + pub fn position(&self) -> Point { + self.overlay.position() + } + + /// Returns the layout [`Node`] of the [`Nested`] overlay. + pub fn layout( + &mut self, + renderer: &Renderer, + bounds: Size, + position: Point, + ) -> layout::Node { + fn recurse<Message, Renderer>( + element: &mut overlay::Element<'_, Message, Renderer>, + renderer: &Renderer, + bounds: Size, + position: Point, + ) -> layout::Node + where + Renderer: renderer::Renderer, + { + let translation = position - Point::ORIGIN; + + let node = element.layout(renderer, bounds, translation); + + if let Some(mut nested) = + element.overlay(Layout::new(&node), renderer) + { + layout::Node::with_children( + node.size(), + vec![ + node, + recurse(&mut nested, renderer, bounds, position), + ], + ) + } else { + layout::Node::with_children(node.size(), vec![node]) + } + } + + recurse(&mut self.overlay, renderer, bounds, position) + } + + /// Draws the [`Nested`] overlay using the associated `Renderer`. + pub fn draw( + &mut self, + renderer: &mut Renderer, + theme: &<Renderer as renderer::Renderer>::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + ) { + fn recurse<Message, Renderer>( + element: &mut overlay::Element<'_, Message, Renderer>, + layout: Layout<'_>, + renderer: &mut Renderer, + theme: &<Renderer as renderer::Renderer>::Theme, + style: &renderer::Style, + cursor: mouse::Cursor, + ) where + Renderer: renderer::Renderer, + { + let mut layouts = layout.children(); + + if let Some(layout) = layouts.next() { + let nested_layout = layouts.next(); + + let is_over = cursor + .position() + .zip(nested_layout) + .and_then(|(cursor_position, nested_layout)| { + element.overlay(layout, renderer).map(|nested| { + nested.is_over( + nested_layout.children().next().unwrap(), + renderer, + cursor_position, + ) + }) + }) + .unwrap_or_default(); + + renderer.with_layer(layout.bounds(), |renderer| { + element.draw( + renderer, + theme, + style, + layout, + if is_over { + mouse::Cursor::Unavailable + } else { + cursor + }, + ); + }); + + if let Some((mut nested, nested_layout)) = + element.overlay(layout, renderer).zip(nested_layout) + { + recurse( + &mut nested, + nested_layout, + renderer, + theme, + style, + cursor, + ); + } + } + } + + recurse(&mut self.overlay, layout, renderer, theme, style, cursor); + } + + /// Applies a [`widget::Operation`] to the [`Nested`] overlay. + pub fn operate( + &mut self, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation<Message>, + ) { + fn recurse<Message, Renderer>( + element: &mut overlay::Element<'_, Message, Renderer>, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation<Message>, + ) where + Renderer: renderer::Renderer, + { + let mut layouts = layout.children(); + + if let Some(layout) = layouts.next() { + element.operate(layout, renderer, operation); + + if let Some((mut nested, nested_layout)) = + element.overlay(layout, renderer).zip(layouts.next()) + { + recurse(&mut nested, nested_layout, renderer, operation); + } + } + } + + recurse(&mut self.overlay, layout, renderer, operation) + } + + /// Processes a runtime [`Event`]. + pub fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + fn recurse<Message, Renderer>( + element: &mut overlay::Element<'_, Message, Renderer>, + layout: Layout<'_>, + event: Event, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> (event::Status, bool) + where + Renderer: renderer::Renderer, + { + let mut layouts = layout.children(); + + if let Some(layout) = layouts.next() { + let (nested_status, nested_is_over) = + if let Some((mut nested, nested_layout)) = + element.overlay(layout, renderer).zip(layouts.next()) + { + recurse( + &mut nested, + nested_layout, + event.clone(), + cursor, + renderer, + clipboard, + shell, + ) + } else { + (event::Status::Ignored, false) + }; + + if matches!(nested_status, event::Status::Ignored) { + let is_over = nested_is_over + || cursor + .position() + .map(|cursor_position| { + element.is_over( + layout, + renderer, + cursor_position, + ) + }) + .unwrap_or_default(); + + ( + element.on_event( + event, + layout, + if nested_is_over { + mouse::Cursor::Unavailable + } else { + cursor + }, + renderer, + clipboard, + shell, + ), + is_over, + ) + } else { + (nested_status, nested_is_over) + } + } else { + (event::Status::Ignored, false) + } + } + + let (status, _) = recurse( + &mut self.overlay, + layout, + event, + cursor, + renderer, + clipboard, + shell, + ); + + status + } + + /// Returns the current [`mouse::Interaction`] of the [`Nested`] overlay. + pub fn mouse_interaction( + &mut self, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + fn recurse<Message, Renderer>( + element: &mut overlay::Element<'_, Message, Renderer>, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> Option<mouse::Interaction> + where + Renderer: renderer::Renderer, + { + let mut layouts = layout.children(); + + let layout = layouts.next()?; + let cursor_position = cursor.position()?; + + if !element.is_over(layout, renderer, cursor_position) { + return None; + } + + Some( + element + .overlay(layout, renderer) + .zip(layouts.next()) + .and_then(|(mut overlay, layout)| { + recurse( + &mut overlay, + layout, + cursor, + viewport, + renderer, + ) + }) + .unwrap_or_else(|| { + element.mouse_interaction( + layout, cursor, viewport, renderer, + ) + }), + ) + } + + recurse(&mut self.overlay, layout, cursor, viewport, renderer) + .unwrap_or_default() + } + + /// Returns true if the cursor is over the [`Nested`] overlay. + pub fn is_over( + &mut self, + layout: Layout<'_>, + renderer: &Renderer, + cursor_position: Point, + ) -> bool { + fn recurse<Message, Renderer>( + element: &mut overlay::Element<'_, Message, Renderer>, + layout: Layout<'_>, + renderer: &Renderer, + cursor_position: Point, + ) -> bool + where + Renderer: renderer::Renderer, + { + let mut layouts = layout.children(); + + if let Some(layout) = layouts.next() { + if element.is_over(layout, renderer, cursor_position) { + return true; + } + + if let Some((mut nested, nested_layout)) = + element.overlay(layout, renderer).zip(layouts.next()) + { + recurse( + &mut nested, + nested_layout, + renderer, + cursor_position, + ) + } else { + false + } + } else { + false + } + } + + recurse(&mut self.overlay, layout, renderer, cursor_position) + } +} diff --git a/runtime/src/program/state.rs b/runtime/src/program/state.rs index 2fa9934d..d83e3f54 100644 --- a/runtime/src/program/state.rs +++ b/runtime/src/program/state.rs @@ -1,7 +1,7 @@ use crate::core::event::{self, Event}; use crate::core::mouse; use crate::core::renderer; -use crate::core::{Clipboard, Point, Size}; +use crate::core::{Clipboard, Size}; use crate::user_interface::{self, UserInterface}; use crate::{Command, Debug, Program}; @@ -88,7 +88,7 @@ where pub fn update( &mut self, bounds: Size, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &mut P::Renderer, theme: &<P::Renderer as iced_core::Renderer>::Theme, style: &renderer::Style, @@ -108,7 +108,7 @@ where let (_, event_statuses) = user_interface.update( &self.queued_events, - cursor_position, + cursor, renderer, clipboard, &mut messages, @@ -131,7 +131,7 @@ where let command = if messages.is_empty() { debug.draw_started(); self.mouse_interaction = - user_interface.draw(renderer, theme, style, cursor_position); + user_interface.draw(renderer, theme, style, cursor); debug.draw_finished(); self.cache = Some(user_interface.into_cache()); @@ -163,7 +163,7 @@ where debug.draw_started(); self.mouse_interaction = - user_interface.draw(renderer, theme, style, cursor_position); + user_interface.draw(renderer, theme, style, cursor); debug.draw_finished(); self.cache = Some(user_interface.into_cache()); diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index c29de7db..619423fd 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -5,8 +5,8 @@ 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}; +use crate::core::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size}; +use crate::overlay; /// A set of interactive graphical elements with a specific [`Layout`]. /// @@ -16,11 +16,10 @@ use crate::core::{Element, Layout, Shell}; /// charge of using this type in your system in any way you want. /// /// # Example -/// The [`integration_opengl`] & [`integration_wgpu`] examples use a -/// [`UserInterface`] to integrate Iced in an existing graphical application. +/// The [`integration`] example uses a [`UserInterface`] to integrate Iced in an +/// existing graphical application. /// -/// [`integration_opengl`]: https://github.com/iced-rs/iced/tree/0.9/examples/integration_opengl -/// [`integration_wgpu`]: https://github.com/iced-rs/iced/tree/0.9/examples/integration_wgpu +/// [`integration`]: https://github.com/iced-rs/iced/tree/0.9/examples/integration #[allow(missing_debug_implementations)] pub struct UserInterface<'a, Message, Renderer> { root: Element<'a, Message, Renderer>, @@ -129,7 +128,9 @@ where /// # pub fn view(&self) -> iced_core::Element<(), Renderer> { unimplemented!() } /// # pub fn update(&mut self, _: ()) {} /// # } - /// use iced_runtime::core::{clipboard, Size, Point}; + /// use iced_runtime::core::clipboard; + /// use iced_runtime::core::mouse; + /// use iced_runtime::core::Size; /// use iced_runtime::user_interface::{self, UserInterface}; /// use iced_wgpu::Renderer; /// @@ -137,7 +138,7 @@ where /// let mut cache = user_interface::Cache::new(); /// let mut renderer = Renderer::new(); /// let mut window_size = Size::new(1024.0, 768.0); - /// let mut cursor_position = Point::default(); + /// let mut cursor = mouse::Cursor::default(); /// let mut clipboard = clipboard::Null; /// /// // Initialize our event storage @@ -157,7 +158,7 @@ where /// // Update the user interface /// let (state, event_statuses) = user_interface.update( /// &events, - /// cursor_position, + /// cursor, /// &mut renderer, /// &mut clipboard, /// &mut messages @@ -174,7 +175,7 @@ where pub fn update( &mut self, events: &[Event], - cursor_position: Point, + cursor: mouse::Cursor, renderer: &mut Renderer, clipboard: &mut dyn Clipboard, messages: &mut Vec<Message>, @@ -184,18 +185,18 @@ where let mut outdated = false; let mut redraw_request = None; - let mut manual_overlay = - ManuallyDrop::new(self.root.as_widget_mut().overlay( - &mut self.state, - Layout::new(&self.base), - renderer, - )); + let mut manual_overlay = ManuallyDrop::new( + self.root + .as_widget_mut() + .overlay(&mut self.state, Layout::new(&self.base), renderer) + .map(overlay::Nested::new), + ); let (base_cursor, overlay_statuses) = if manual_overlay.is_some() { let bounds = self.bounds; let mut overlay = manual_overlay.as_mut().unwrap(); - let mut layout = overlay.layout(renderer, bounds, Vector::ZERO); + let mut layout = overlay.layout(renderer, bounds, Point::ORIGIN); let mut event_statuses = Vec::new(); for event in events.iter().cloned() { @@ -204,7 +205,7 @@ where let event_status = overlay.on_event( event, Layout::new(&layout), - cursor_position, + cursor, renderer, clipboard, &mut shell, @@ -230,12 +231,16 @@ where &layout::Limits::new(Size::ZERO, self.bounds), ); - manual_overlay = - ManuallyDrop::new(self.root.as_widget_mut().overlay( - &mut self.state, - Layout::new(&self.base), - renderer, - )); + manual_overlay = ManuallyDrop::new( + self.root + .as_widget_mut() + .overlay( + &mut self.state, + Layout::new(&self.base), + renderer, + ) + .map(overlay::Nested::new), + ); if manual_overlay.is_none() { break; @@ -244,7 +249,8 @@ where overlay = manual_overlay.as_mut().unwrap(); shell.revalidate_layout(|| { - layout = overlay.layout(renderer, bounds, Vector::ZERO); + layout = + overlay.layout(renderer, bounds, Point::ORIGIN); }); } @@ -253,22 +259,29 @@ where } } - let base_cursor = manual_overlay - .as_ref() - .filter(|overlay| { - overlay.is_over(Layout::new(&layout), cursor_position) - }) - .map(|_| { - // TODO: Type-safe cursor availability - Point::new(-1.0, -1.0) + let base_cursor = if manual_overlay + .as_mut() + .and_then(|overlay| { + cursor.position().map(|cursor_position| { + overlay.is_over( + Layout::new(&layout), + renderer, + cursor_position, + ) + }) }) - .unwrap_or(cursor_position); + .unwrap_or_default() + { + mouse::Cursor::Unavailable + } else { + cursor + }; self.overlay = Some(layout); (base_cursor, event_statuses) } else { - (cursor_position, vec![event::Status::Ignored; events.len()]) + (cursor, vec![event::Status::Ignored; events.len()]) }; let _ = ManuallyDrop::into_inner(manual_overlay); @@ -360,8 +373,9 @@ where /// # pub fn update(&mut self, _: ()) {} /// # } /// use iced_runtime::core::clipboard; + /// use iced_runtime::core::mouse; /// use iced_runtime::core::renderer; - /// use iced_runtime::core::{Element, Size, Point}; + /// use iced_runtime::core::{Element, Size}; /// use iced_runtime::user_interface::{self, UserInterface}; /// use iced_wgpu::{Renderer, Theme}; /// @@ -369,7 +383,7 @@ where /// let mut cache = user_interface::Cache::new(); /// let mut renderer = Renderer::new(); /// let mut window_size = Size::new(1024.0, 768.0); - /// let mut cursor_position = Point::default(); + /// let mut cursor = mouse::Cursor::default(); /// let mut clipboard = clipboard::Null; /// let mut events = Vec::new(); /// let mut messages = Vec::new(); @@ -388,14 +402,14 @@ where /// // Update the user interface /// let event_statuses = user_interface.update( /// &events, - /// cursor_position, + /// cursor, /// &mut renderer, /// &mut clipboard, /// &mut messages /// ); /// /// // Draw the user interface - /// let mouse_cursor = user_interface.draw(&mut renderer, &theme, &renderer::Style::default(), cursor_position); + /// let mouse_interaction = user_interface.draw(&mut renderer, &theme, &renderer::Style::default(), cursor); /// /// cache = user_interface.into_cache(); /// @@ -412,35 +426,44 @@ where renderer: &mut Renderer, theme: &Renderer::Theme, style: &renderer::Style, - cursor_position: Point, + cursor: mouse::Cursor, ) -> mouse::Interaction { // TODO: Move to shell level (?) renderer.clear(); let viewport = Rectangle::with_size(self.bounds); - let base_cursor = if let Some(overlay) = self + let base_cursor = if let Some(mut overlay) = self .root .as_widget_mut() .overlay(&mut self.state, Layout::new(&self.base), renderer) + .map(overlay::Nested::new) { let overlay_layout = self.overlay.take().unwrap_or_else(|| { - overlay.layout(renderer, self.bounds, Vector::ZERO) + overlay.layout(renderer, self.bounds, Point::ORIGIN) }); - let new_cursor_position = if overlay - .is_over(Layout::new(&overlay_layout), cursor_position) + let cursor = if cursor + .position() + .map(|cursor_position| { + overlay.is_over( + Layout::new(&overlay_layout), + renderer, + cursor_position, + ) + }) + .unwrap_or_default() { - Point::new(-1.0, -1.0) + mouse::Cursor::Unavailable } else { - cursor_position + cursor }; self.overlay = Some(overlay_layout); - new_cursor_position + cursor } else { - cursor_position + cursor }; self.root.as_widget().draw( @@ -456,7 +479,7 @@ where let base_interaction = self.root.as_widget().mouse_interaction( &self.state, Layout::new(&self.base), - cursor_position, + base_cursor, &viewport, renderer, ); @@ -478,10 +501,11 @@ where .and_then(|layout| { root.as_widget_mut() .overlay(&mut self.state, Layout::new(base), renderer) - .map(|overlay| { + .map(overlay::Nested::new) + .map(|mut overlay| { let overlay_interaction = overlay.mouse_interaction( Layout::new(layout), - cursor_position, + cursor, &viewport, renderer, ); @@ -494,11 +518,20 @@ where theme, style, Layout::new(layout), - cursor_position, + cursor, ); }); - if overlay.is_over(Layout::new(layout), cursor_position) + if cursor + .position() + .map(|cursor_position| { + overlay.is_over( + Layout::new(layout), + renderer, + cursor_position, + ) + }) + .unwrap_or_default() { overlay_interaction } else { @@ -522,14 +555,15 @@ where operation, ); - if let Some(mut overlay) = self.root.as_widget_mut().overlay( - &mut self.state, - Layout::new(&self.base), - renderer, - ) { + if let Some(mut overlay) = self + .root + .as_widget_mut() + .overlay(&mut self.state, Layout::new(&self.base), renderer) + .map(overlay::Nested::new) + { if self.overlay.is_none() { self.overlay = - Some(overlay.layout(renderer, self.bounds, Vector::ZERO)); + Some(overlay.layout(renderer, self.bounds, Point::ORIGIN)); } overlay.operate( diff --git a/runtime/src/window.rs b/runtime/src/window.rs index 833a1125..e448edef 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -1,11 +1,14 @@ //! Build window-based GUI applications. mod action; +pub mod screenshot; + pub use action::Action; +pub use screenshot::Screenshot; use crate::command::{self, Command}; use crate::core::time::Instant; -use crate::core::window::{Event, Icon, Mode, UserAttention}; +use crate::core::window::{Event, Icon, Level, Mode, UserAttention}; use crate::futures::subscription::{self, Subscription}; /// Subscribes to the frames of the window of the running application. @@ -53,7 +56,7 @@ pub fn move_to<Message>(x: i32, y: i32) -> Command<Message> { Command::single(command::Action::Window(Action::Move { x, y })) } -/// Sets the [`Mode`] of the window. +/// Changes the [`Mode`] of the window. pub fn change_mode<Message>(mode: Mode) -> Command<Message> { Command::single(command::Action::Window(Action::ChangeMode(mode))) } @@ -99,9 +102,9 @@ pub fn gain_focus<Message>() -> Command<Message> { Command::single(command::Action::Window(Action::GainFocus)) } -/// Changes whether or not the window will always be on top of other windows. -pub fn change_always_on_top<Message>(on_top: bool) -> Command<Message> { - Command::single(command::Action::Window(Action::ChangeAlwaysOnTop(on_top))) +/// Changes the window [`Level`]. +pub fn change_level<Message>(level: Level) -> Command<Message> { + Command::single(command::Action::Window(Action::ChangeLevel(level))) } /// Fetches an identifier unique to the window. @@ -115,3 +118,10 @@ pub fn fetch_id<Message>( pub fn change_icon<Message>(icon: Icon) -> Command<Message> { Command::single(command::Action::Window(Action::ChangeIcon(icon))) } + +/// Captures a [`Screenshot`] from the window. +pub fn screenshot<Message>( + f: impl FnOnce(Screenshot) -> Message + Send + 'static, +) -> Command<Message> { + Command::single(command::Action::Window(Action::Screenshot(Box::new(f)))) +} diff --git a/runtime/src/window/action.rs b/runtime/src/window/action.rs index 83b71c75..09be1810 100644 --- a/runtime/src/window/action.rs +++ b/runtime/src/window/action.rs @@ -1,13 +1,14 @@ -use crate::core::window::{Icon, Mode, UserAttention}; +use crate::core::window::{Icon, Level, Mode, UserAttention}; use crate::futures::MaybeSend; +use crate::window::Screenshot; use std::fmt; /// An operation to be performed on some window. pub enum Action<T> { - /// Closes the current window and exits the application. + /// Close the current window and exits the application. Close, - /// Moves the window with the left mouse button until the button is + /// Move the window with the left mouse button until the button is /// released. /// /// There’s no guarantee that this will work unless the left mouse @@ -20,7 +21,7 @@ pub enum Action<T> { /// The new logical height of the window height: u32, }, - /// Sets the window to maximized or back + /// Set the window to maximized or back Maximize(bool), /// Set the window to minimized or back Minimize(bool), @@ -70,15 +71,11 @@ pub enum Action<T> { /// /// - **Web / Wayland:** Unsupported. GainFocus, - /// Change whether or not the window will always be on top of other windows. - /// - /// ## Platform-specific - /// - /// - **Web / Wayland:** Unsupported. - ChangeAlwaysOnTop(bool), + /// Change the window [`Level`]. + ChangeLevel(Level), /// Fetch an identifier unique to the window. FetchId(Box<dyn FnOnce(u64) -> T + 'static>), - /// Changes the window [`Icon`]. + /// Change the window [`Icon`]. /// /// On Windows and X11, this is typically the small icon in the top-left /// corner of the titlebar. @@ -93,6 +90,8 @@ pub enum Action<T> { /// - **X11:** Has no universal guidelines for icon sizes, so you're at the whims of the WM. That /// said, it's usually in the same ballpark as on Windows. ChangeIcon(Icon), + /// Screenshot the viewport of the window. + Screenshot(Box<dyn FnOnce(Screenshot) -> T + 'static>), } impl<T> Action<T> { @@ -119,11 +118,14 @@ impl<T> Action<T> { Action::RequestUserAttention(attention_type) } Self::GainFocus => Action::GainFocus, - Self::ChangeAlwaysOnTop(on_top) => { - Action::ChangeAlwaysOnTop(on_top) - } + Self::ChangeLevel(level) => Action::ChangeLevel(level), Self::FetchId(o) => Action::FetchId(Box::new(move |s| f(o(s)))), Self::ChangeIcon(icon) => Action::ChangeIcon(icon), + Self::Screenshot(tag) => { + Action::Screenshot(Box::new(move |screenshot| { + f(tag(screenshot)) + })) + } } } } @@ -154,13 +156,14 @@ impl<T> fmt::Debug for Action<T> { write!(f, "Action::RequestUserAttention") } Self::GainFocus => write!(f, "Action::GainFocus"), - Self::ChangeAlwaysOnTop(on_top) => { - write!(f, "Action::AlwaysOnTop({on_top})") + Self::ChangeLevel(level) => { + write!(f, "Action::ChangeLevel({level:?})") } Self::FetchId(_) => write!(f, "Action::FetchId"), Self::ChangeIcon(_icon) => { write!(f, "Action::ChangeIcon(icon)") } + Self::Screenshot(_) => write!(f, "Action::Screenshot"), } } } diff --git a/runtime/src/window/screenshot.rs b/runtime/src/window/screenshot.rs new file mode 100644 index 00000000..c84286b6 --- /dev/null +++ b/runtime/src/window/screenshot.rs @@ -0,0 +1,92 @@ +//! Take screenshots of a window. +use crate::core::{Rectangle, Size}; + +use std::fmt::{Debug, Formatter}; +use std::sync::Arc; + +/// Data of a screenshot, captured with `window::screenshot()`. +/// +/// The `bytes` of this screenshot will always be ordered as `RGBA` in the sRGB color space. +#[derive(Clone)] +pub struct Screenshot { + /// The bytes of the [`Screenshot`]. + pub bytes: Arc<Vec<u8>>, + /// The size of the [`Screenshot`]. + pub size: Size<u32>, +} + +impl Debug for Screenshot { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Screenshot: {{ \n bytes: {}\n size: {:?} }}", + self.bytes.len(), + self.size + ) + } +} + +impl Screenshot { + /// Creates a new [`Screenshot`]. + pub fn new(bytes: Vec<u8>, size: Size<u32>) -> Self { + Self { + bytes: Arc::new(bytes), + size, + } + } + + /// Crops a [`Screenshot`] to the provided `region`. This will always be relative to the + /// top-left corner of the [`Screenshot`]. + pub fn crop(&self, region: Rectangle<u32>) -> Result<Self, CropError> { + if region.width == 0 || region.height == 0 { + return Err(CropError::Zero); + } + + if region.x + region.width > self.size.width + || region.y + region.height > self.size.height + { + return Err(CropError::OutOfBounds); + } + + // Image is always RGBA8 = 4 bytes per pixel + const PIXEL_SIZE: usize = 4; + + let bytes_per_row = self.size.width as usize * PIXEL_SIZE; + let row_range = region.y as usize..(region.y + region.height) as usize; + let column_range = region.x as usize * PIXEL_SIZE + ..(region.x + region.width) as usize * PIXEL_SIZE; + + let chopped = self.bytes.chunks(bytes_per_row).enumerate().fold( + vec![], + |mut acc, (row, bytes)| { + if row_range.contains(&row) { + acc.extend(&bytes[column_range.clone()]); + } + + acc + }, + ); + + Ok(Self { + bytes: Arc::new(chopped), + size: Size::new(region.width, region.height), + }) + } +} + +impl AsRef<[u8]> for Screenshot { + fn as_ref(&self) -> &[u8] { + &self.bytes + } +} + +#[derive(Debug, thiserror::Error)] +/// Errors that can occur when cropping a [`Screenshot`]. +pub enum CropError { + #[error("The cropped region is out of bounds.")] + /// The cropped region's size is out of bounds. + OutOfBounds, + #[error("The cropped region is not visible.")] + /// The cropped region's size is zero. + Zero, +} @@ -163,7 +163,7 @@ )] #![forbid(rust_2018_idioms, unsafe_code)] #![allow(clippy::inherent_to_string, clippy::type_complexity)] -#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] use iced_widget::graphics; use iced_widget::renderer; use iced_widget::style; @@ -188,9 +188,10 @@ pub use style::theme; pub use crate::core::alignment; pub use crate::core::event; +pub use crate::core::gradient; pub use crate::core::{ - color, Alignment, Background, Color, ContentFit, Length, Padding, Pixels, - Point, Rectangle, Size, Vector, + color, Alignment, Background, Color, ContentFit, Degrees, Gradient, Length, + Padding, Pixels, Point, Radians, Rectangle, Size, Vector, }; pub use crate::runtime::Command; @@ -229,7 +230,9 @@ pub mod keyboard { pub mod mouse { //! Listen and react to mouse events. - pub use crate::core::mouse::{Button, Event, Interaction, ScrollDelta}; + pub use crate::core::mouse::{ + Button, Cursor, Event, Interaction, ScrollDelta, + }; } pub mod subscription { @@ -275,6 +278,7 @@ pub mod widget { mod native {} mod renderer {} mod style {} + mod runtime {} } pub use application::Application; diff --git a/src/window/icon.rs b/src/window/icon.rs index b67b2ea3..0fe010ca 100644 --- a/src/window/icon.rs +++ b/src/window/icon.rs @@ -12,7 +12,6 @@ use std::path::Path; /// /// This will return an error in case the file is missing at run-time. You may prefer [`Self::from_file_data`] instead. #[cfg(feature = "image")] -#[cfg_attr(docsrs, doc(cfg(feature = "image")))] pub fn from_file<P: AsRef<Path>>(icon_path: P) -> Result<Icon, Error> { let icon = image_rs::io::Reader::open(icon_path)?.decode()?.to_rgba8(); @@ -24,7 +23,6 @@ pub fn from_file<P: AsRef<Path>>(icon_path: P) -> Result<Icon, Error> { /// This content can be included in your application at compile-time, e.g. using the `include_bytes!` macro. /// You can pass an explicit file format. Otherwise, the file format will be guessed at runtime. #[cfg(feature = "image")] -#[cfg_attr(docsrs, doc(cfg(feature = "image")))] pub fn from_file_data( data: &[u8], explicit_format: Option<image_rs::ImageFormat>, @@ -60,7 +58,6 @@ pub enum Error { /// The `image` crate reported an error. #[cfg(feature = "image")] - #[cfg_attr(docsrs, doc(cfg(feature = "image")))] #[error("Unable to create icon from a file: {0}")] ImageError(#[from] image_rs::error::ImageError), } diff --git a/src/window/settings.rs b/src/window/settings.rs index 3c8da62f..458b9232 100644 --- a/src/window/settings.rs +++ b/src/window/settings.rs @@ -1,4 +1,4 @@ -use crate::window::{Icon, Position}; +use crate::window::{Icon, Level, Position}; pub use iced_winit::settings::PlatformSpecific; @@ -29,8 +29,8 @@ pub struct Settings { /// Whether the window should be transparent. pub transparent: bool, - /// Whether the window will always be on top of other windows. - pub always_on_top: bool, + /// The window [`Level`]. + pub level: Level, /// The icon of the window. pub icon: Option<Icon>, @@ -50,7 +50,7 @@ impl Default for Settings { resizable: true, decorations: true, transparent: false, - always_on_top: false, + level: Level::default(), icon: None, platform_specific: Default::default(), } @@ -68,7 +68,7 @@ impl From<Settings> for iced_winit::settings::Window { resizable: settings.resizable, decorations: settings.decorations, transparent: settings.transparent, - always_on_top: settings.always_on_top, + level: settings.level, icon: settings.icon.map(Icon::into), platform_specific: settings.platform_specific, } diff --git a/style/Cargo.toml b/style/Cargo.toml index 0bb354e0..8af4a9b3 100644 --- a/style/Cargo.toml +++ b/style/Cargo.toml @@ -16,7 +16,7 @@ path = "../core" features = ["palette"] [dependencies.palette] -version = "0.6" +version = "0.7" [dependencies.once_cell] version = "1.15" diff --git a/style/src/button.rs b/style/src/button.rs index a564a2b7..e49ad94a 100644 --- a/style/src/button.rs +++ b/style/src/button.rs @@ -1,5 +1,5 @@ //! Change the apperance of a button. -use iced_core::{Background, Color, Vector}; +use iced_core::{Background, BorderRadius, Color, Vector}; /// The appearance of a button. #[derive(Debug, Clone, Copy)] @@ -9,7 +9,7 @@ pub struct Appearance { /// The [`Background`] of the button. pub background: Option<Background>, /// The border radius of the button. - pub border_radius: f32, + pub border_radius: BorderRadius, /// The border width of the button. pub border_width: f32, /// The border [`Color`] of the button. @@ -23,7 +23,7 @@ impl std::default::Default for Appearance { Self { shadow_offset: Vector::default(), background: None, - border_radius: 0.0, + border_radius: 0.0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, text_color: Color::BLACK, @@ -68,6 +68,9 @@ pub trait StyleSheet { a: color.a * 0.5, ..color }), + Background::Gradient(gradient) => { + Background::Gradient(gradient.mul_alpha(0.5)) + } }), text_color: Color { a: active.text_color.a * 0.5, diff --git a/style/src/checkbox.rs b/style/src/checkbox.rs index 52b90ec9..cf52c05d 100644 --- a/style/src/checkbox.rs +++ b/style/src/checkbox.rs @@ -1,5 +1,5 @@ //! Change the appearance of a checkbox. -use iced_core::{Background, Color}; +use iced_core::{Background, BorderRadius, Color}; /// The appearance of a checkbox. #[derive(Debug, Clone, Copy)] @@ -9,7 +9,7 @@ pub struct Appearance { /// The icon [`Color`] of the checkbox. pub icon_color: Color, /// The border radius of the checkbox. - pub border_radius: f32, + pub border_radius: BorderRadius, /// The border width of the checkbox. pub border_width: f32, /// The border [`Color`] of the checkbox. diff --git a/style/src/container.rs b/style/src/container.rs index 560b2d5b..ec543ae4 100644 --- a/style/src/container.rs +++ b/style/src/container.rs @@ -1,5 +1,5 @@ //! Change the appearance of a container. -use iced_core::{Background, Color}; +use iced_core::{Background, BorderRadius, Color}; /// The appearance of a container. #[derive(Debug, Clone, Copy)] @@ -9,7 +9,7 @@ pub struct Appearance { /// The [`Background`] of the container. pub background: Option<Background>, /// The border radius of the container. - pub border_radius: f32, + pub border_radius: BorderRadius, /// The border width of the container. pub border_width: f32, /// The border [`Color`] of the container. @@ -21,7 +21,7 @@ impl std::default::Default for Appearance { Self { text_color: None, background: None, - border_radius: 0.0, + border_radius: 0.0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, } diff --git a/style/src/menu.rs b/style/src/menu.rs index 7d878748..dbf19dae 100644 --- a/style/src/menu.rs +++ b/style/src/menu.rs @@ -1,5 +1,5 @@ //! Change the appearance of menus. -use iced_core::{Background, Color}; +use iced_core::{Background, BorderRadius, Color}; /// The appearance of a menu. #[derive(Debug, Clone, Copy)] @@ -11,7 +11,7 @@ pub struct Appearance { /// The border width of the menu. pub border_width: f32, /// The border radius of the menu. - pub border_radius: f32, + pub border_radius: BorderRadius, /// The border [`Color`] of the menu. pub border_color: Color, /// The text [`Color`] of a selected option in the menu. diff --git a/style/src/pane_grid.rs b/style/src/pane_grid.rs index fd8fc05f..b99af955 100644 --- a/style/src/pane_grid.rs +++ b/style/src/pane_grid.rs @@ -1,16 +1,17 @@ //! Change the appearance of a pane grid. -use iced_core::Color; +use iced_core::{Background, BorderRadius, Color}; -/// A set of rules that dictate the style of a container. -pub trait StyleSheet { - /// The supported style of the [`StyleSheet`]. - type Style: Default; - - /// The [`Line`] to draw when a split is picked. - fn picked_split(&self, style: &Self::Style) -> Option<Line>; - - /// The [`Line`] to draw when a split is hovered. - fn hovered_split(&self, style: &Self::Style) -> Option<Line>; +/// The appearance of the hovered region of a pane grid. +#[derive(Debug, Clone, Copy)] +pub struct Appearance { + /// The [`Background`] of the hovered pane region. + pub background: Background, + /// The border width of the hovered pane region. + pub border_width: f32, + /// The border [`Color`] of the hovered pane region. + pub border_color: Color, + /// The border radius of the hovered pane region. + pub border_radius: BorderRadius, } /// A line. @@ -24,3 +25,18 @@ pub struct Line { /// The width of the [`Line`]. pub width: f32, } + +/// A set of rules that dictate the style of a container. +pub trait StyleSheet { + /// The supported style of the [`StyleSheet`]. + type Style: Default; + + /// The [`Region`] to draw when a pane is hovered. + fn hovered_region(&self, style: &Self::Style) -> Appearance; + + /// The [`Line`] to draw when a split is picked. + fn picked_split(&self, style: &Self::Style) -> Option<Line>; + + /// The [`Line`] to draw when a split is hovered. + fn hovered_split(&self, style: &Self::Style) -> Option<Line>; +} diff --git a/style/src/pick_list.rs b/style/src/pick_list.rs index 11e13b01..961c1e93 100644 --- a/style/src/pick_list.rs +++ b/style/src/pick_list.rs @@ -1,5 +1,5 @@ //! Change the appearance of a pick list. -use iced_core::{Background, Color}; +use iced_core::{Background, BorderRadius, Color}; /// The appearance of a pick list. #[derive(Debug, Clone, Copy)] @@ -13,7 +13,7 @@ pub struct Appearance { /// The [`Background`] of the pick list. pub background: Background, /// The border radius of the pick list. - pub border_radius: f32, + pub border_radius: BorderRadius, /// The border width of the pick list. pub border_width: f32, /// The border color of the pick list. diff --git a/style/src/progress_bar.rs b/style/src/progress_bar.rs index fb1819fc..c05a6ee4 100644 --- a/style/src/progress_bar.rs +++ b/style/src/progress_bar.rs @@ -1,5 +1,5 @@ //! Change the appearance of a progress bar. -use iced_core::Background; +use iced_core::{Background, BorderRadius}; /// The appearance of a progress bar. #[derive(Debug, Clone, Copy)] @@ -9,7 +9,7 @@ pub struct Appearance { /// The [`Background`] of the bar of the progress bar. pub bar: Background, /// The border radius of the progress bar. - pub border_radius: f32, + pub border_radius: BorderRadius, } /// A set of rules that dictate the style of a progress bar. diff --git a/style/src/rule.rs b/style/src/rule.rs index b7380747..afae085c 100644 --- a/style/src/rule.rs +++ b/style/src/rule.rs @@ -1,5 +1,5 @@ //! Change the appearance of a rule. -use iced_core::Color; +use iced_core::{BorderRadius, Color}; /// The appearance of a rule. #[derive(Debug, Clone, Copy)] @@ -9,7 +9,7 @@ pub struct Appearance { /// The width (thickness) of the rule line. pub width: u16, /// The radius of the line corners. - pub radius: f32, + pub radius: BorderRadius, /// The [`FillMode`] of the rule. pub fill_mode: FillMode, } diff --git a/style/src/scrollable.rs b/style/src/scrollable.rs index b528c444..952c11e1 100644 --- a/style/src/scrollable.rs +++ b/style/src/scrollable.rs @@ -1,5 +1,5 @@ //! Change the appearance of a scrollable. -use iced_core::{Background, Color}; +use iced_core::{Background, BorderRadius, Color}; /// The appearance of a scrollable. #[derive(Debug, Clone, Copy)] @@ -7,7 +7,7 @@ pub struct Scrollbar { /// The [`Background`] of a scrollable. pub background: Option<Background>, /// The border radius of a scrollable. - pub border_radius: f32, + pub border_radius: BorderRadius, /// The border width of a scrollable. pub border_width: f32, /// The border [`Color`] of a scrollable. @@ -22,7 +22,7 @@ pub struct Scroller { /// The [`Color`] of the scroller. pub color: Color, /// The border radius of the scroller. - pub border_radius: f32, + pub border_radius: BorderRadius, /// The border width of the scroller. pub border_width: f32, /// The border [`Color`] of the scroller. diff --git a/style/src/slider.rs b/style/src/slider.rs index 884d3871..f0068558 100644 --- a/style/src/slider.rs +++ b/style/src/slider.rs @@ -1,5 +1,5 @@ //! Change the apperance of a slider. -use iced_core::Color; +use iced_core::{BorderRadius, Color}; /// The appearance of a slider. #[derive(Debug, Clone, Copy)] @@ -17,6 +17,8 @@ pub struct Rail { pub colors: (Color, Color), /// The width of the stroke of a slider rail. pub width: f32, + /// The border radius of the corners of the rail. + pub border_radius: BorderRadius, } /// The appearance of the handle of a slider. @@ -45,7 +47,7 @@ pub enum HandleShape { /// The width of the rectangle. width: u16, /// The border radius of the corners of the rectangle. - border_radius: f32, + border_radius: BorderRadius, }, } diff --git a/style/src/text_input.rs b/style/src/text_input.rs index 2616ad5a..90251b5c 100644 --- a/style/src/text_input.rs +++ b/style/src/text_input.rs @@ -1,5 +1,5 @@ //! Change the appearance of a text input. -use iced_core::{Background, Color}; +use iced_core::{Background, BorderRadius, Color}; /// The appearance of a text input. #[derive(Debug, Clone, Copy)] @@ -7,7 +7,7 @@ pub struct Appearance { /// The [`Background`] of the text input. pub background: Background, /// The border radius of the text input. - pub border_radius: f32, + pub border_radius: BorderRadius, /// The border width of the text input. pub border_width: f32, /// The border [`Color`] of the text input. diff --git a/style/src/theme.rs b/style/src/theme.rs index d9893bcf..64497181 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -105,7 +105,7 @@ impl application::StyleSheet for Theme { } } -impl application::StyleSheet for fn(&Theme) -> application::Appearance { +impl<T: Fn(&Theme) -> application::Appearance> application::StyleSheet for T { type Style = Theme; fn appearance(&self, style: &Self::Style) -> application::Appearance { @@ -113,8 +113,10 @@ impl application::StyleSheet for fn(&Theme) -> application::Appearance { } } -impl From<fn(&Theme) -> application::Appearance> for Application { - fn from(f: fn(&Theme) -> application::Appearance) -> Self { +impl<T: Fn(&Theme) -> application::Appearance + 'static> From<T> + for Application +{ + fn from(f: T) -> Self { Self::Custom(Box::new(f)) } } @@ -139,6 +141,15 @@ pub enum Button { Custom(Box<dyn button::StyleSheet<Style = Theme>>), } +impl Button { + /// Creates a custom [`Button`] style variant. + pub fn custom( + style_sheet: impl button::StyleSheet<Style = Theme> + 'static, + ) -> Self { + Self::Custom(Box::new(style_sheet)) + } +} + impl button::StyleSheet for Theme { type Style = Button; @@ -146,7 +157,7 @@ impl button::StyleSheet for Theme { let palette = self.extended_palette(); let appearance = button::Appearance { - border_radius: 2.0, + border_radius: 2.0.into(), ..button::Appearance::default() }; @@ -217,6 +228,9 @@ impl button::StyleSheet for Theme { a: color.a * 0.5, ..color }), + Background::Gradient(gradient) => { + Background::Gradient(gradient.mul_alpha(0.5)) + } }), text_color: Color { a: active.text_color.a * 0.5, @@ -332,7 +346,7 @@ fn checkbox_appearance( base.color }), icon_color, - border_radius: 2.0, + border_radius: 2.0.into(), border_width: 1.0, border_color: accent.color, text_color: None, @@ -351,8 +365,8 @@ pub enum Container { Custom(Box<dyn container::StyleSheet<Style = Theme>>), } -impl From<fn(&Theme) -> container::Appearance> for Container { - fn from(f: fn(&Theme) -> container::Appearance) -> Self { +impl<T: Fn(&Theme) -> container::Appearance + 'static> From<T> for Container { + fn from(f: T) -> Self { Self::Custom(Box::new(f)) } } @@ -368,8 +382,8 @@ impl container::StyleSheet for Theme { container::Appearance { text_color: None, - background: palette.background.weak.color.into(), - border_radius: 2.0, + background: Some(palette.background.weak.color.into()), + border_radius: 2.0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, } @@ -379,7 +393,7 @@ impl container::StyleSheet for Theme { } } -impl container::StyleSheet for fn(&Theme) -> container::Appearance { +impl<T: Fn(&Theme) -> container::Appearance> container::StyleSheet for T { type Style = Theme; fn appearance(&self, style: &Self::Style) -> container::Appearance { @@ -408,7 +422,7 @@ impl slider::StyleSheet for Theme { let handle = slider::Handle { shape: slider::HandleShape::Rectangle { width: 8, - border_radius: 4.0, + border_radius: 4.0.into(), }, color: Color::WHITE, border_color: Color::WHITE, @@ -419,9 +433,10 @@ impl slider::StyleSheet for Theme { rail: slider::Rail { colors: ( palette.primary.base.color, - palette.primary.base.color, + palette.secondary.base.color, ), - width: 2.0, + width: 4.0, + border_radius: 2.0.into(), }, handle: slider::Handle { color: palette.background.base.color, @@ -493,7 +508,7 @@ impl menu::StyleSheet for Theme { text_color: palette.background.weak.text, background: palette.background.weak.color.into(), border_width: 1.0, - border_radius: 0.0, + border_radius: 0.0.into(), border_color: palette.background.strong.color, selected_text_color: palette.primary.strong.text, selected_background: palette.primary.strong.color.into(), @@ -539,7 +554,7 @@ impl pick_list::StyleSheet for Theme { background: palette.background.weak.color.into(), placeholder_color: palette.background.strong.color, handle_color: palette.background.weak.text, - border_radius: 2.0, + border_radius: 2.0.into(), border_width: 1.0, border_color: palette.background.strong.color, } @@ -558,7 +573,7 @@ impl pick_list::StyleSheet for Theme { background: palette.background.weak.color.into(), placeholder_color: palette.background.strong.color, handle_color: palette.background.weak.text, - border_radius: 2.0, + border_radius: 2.0.into(), border_width: 1.0, border_color: palette.primary.strong.color, } @@ -703,6 +718,25 @@ pub enum PaneGrid { impl pane_grid::StyleSheet for Theme { type Style = PaneGrid; + fn hovered_region(&self, style: &Self::Style) -> pane_grid::Appearance { + match style { + PaneGrid::Default => { + let palette = self.extended_palette(); + + pane_grid::Appearance { + background: Background::Color(Color { + a: 0.5, + ..palette.primary.base.color + }), + border_width: 2.0, + border_color: palette.primary.strong.color, + border_radius: 0.0.into(), + } + } + PaneGrid::Custom(custom) => custom.hovered_region(self), + } + } + fn picked_split(&self, style: &Self::Style) -> Option<pane_grid::Line> { match style { PaneGrid::Default => { @@ -746,8 +780,10 @@ pub enum ProgressBar { Custom(Box<dyn progress_bar::StyleSheet<Style = Theme>>), } -impl From<fn(&Theme) -> progress_bar::Appearance> for ProgressBar { - fn from(f: fn(&Theme) -> progress_bar::Appearance) -> Self { +impl<T: Fn(&Theme) -> progress_bar::Appearance + 'static> From<T> + for ProgressBar +{ + fn from(f: T) -> Self { Self::Custom(Box::new(f)) } } @@ -765,7 +801,7 @@ impl progress_bar::StyleSheet for Theme { let from_palette = |bar: Color| progress_bar::Appearance { background: palette.background.strong.color.into(), bar: bar.into(), - border_radius: 2.0, + border_radius: 2.0.into(), }; match style { @@ -777,7 +813,7 @@ impl progress_bar::StyleSheet for Theme { } } -impl progress_bar::StyleSheet for fn(&Theme) -> progress_bar::Appearance { +impl<T: Fn(&Theme) -> progress_bar::Appearance> progress_bar::StyleSheet for T { type Style = Theme; fn appearance(&self, style: &Self::Style) -> progress_bar::Appearance { @@ -795,8 +831,8 @@ pub enum Rule { Custom(Box<dyn rule::StyleSheet<Style = Theme>>), } -impl From<fn(&Theme) -> rule::Appearance> for Rule { - fn from(f: fn(&Theme) -> rule::Appearance) -> Self { +impl<T: Fn(&Theme) -> rule::Appearance + 'static> From<T> for Rule { + fn from(f: T) -> Self { Self::Custom(Box::new(f)) } } @@ -811,7 +847,7 @@ impl rule::StyleSheet for Theme { Rule::Default => rule::Appearance { color: palette.background.strong.color, width: 1, - radius: 0.0, + radius: 0.0.into(), fill_mode: rule::FillMode::Full, }, Rule::Custom(custom) => custom.appearance(self), @@ -819,7 +855,7 @@ impl rule::StyleSheet for Theme { } } -impl rule::StyleSheet for fn(&Theme) -> rule::Appearance { +impl<T: Fn(&Theme) -> rule::Appearance> rule::StyleSheet for T { type Style = Theme; fn appearance(&self, style: &Self::Style) -> rule::Appearance { @@ -893,13 +929,13 @@ impl scrollable::StyleSheet for Theme { let palette = self.extended_palette(); scrollable::Scrollbar { - background: palette.background.weak.color.into(), - border_radius: 2.0, + background: Some(palette.background.weak.color.into()), + border_radius: 2.0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, scroller: scrollable::Scroller { color: palette.background.strong.color, - border_radius: 2.0, + border_radius: 2.0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, }, @@ -920,13 +956,13 @@ impl scrollable::StyleSheet for Theme { let palette = self.extended_palette(); scrollable::Scrollbar { - background: palette.background.weak.color.into(), - border_radius: 2.0, + background: Some(palette.background.weak.color.into()), + border_radius: 2.0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, scroller: scrollable::Scroller { color: palette.primary.strong.color, - border_radius: 2.0, + border_radius: 2.0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, }, @@ -1028,7 +1064,7 @@ impl text_input::StyleSheet for Theme { text_input::Appearance { background: palette.background.base.color.into(), - border_radius: 2.0, + border_radius: 2.0.into(), border_width: 1.0, border_color: palette.background.strong.color, icon_color: palette.background.weak.text, @@ -1044,7 +1080,7 @@ impl text_input::StyleSheet for Theme { text_input::Appearance { background: palette.background.base.color.into(), - border_radius: 2.0, + border_radius: 2.0.into(), border_width: 1.0, border_color: palette.background.base.text, icon_color: palette.background.weak.text, @@ -1060,7 +1096,7 @@ impl text_input::StyleSheet for Theme { text_input::Appearance { background: palette.background.base.color.into(), - border_radius: 2.0, + border_radius: 2.0.into(), border_width: 1.0, border_color: palette.primary.strong.color, icon_color: palette.background.weak.text, @@ -1106,7 +1142,7 @@ impl text_input::StyleSheet for Theme { text_input::Appearance { background: palette.background.weak.color.into(), - border_radius: 2.0, + border_radius: 2.0.into(), border_width: 1.0, border_color: palette.background.strong.color, icon_color: palette.background.strong.color, diff --git a/style/src/theme/palette.rs b/style/src/theme/palette.rs index 0f15494b..aaeb799d 100644 --- a/style/src/theme/palette.rs +++ b/style/src/theme/palette.rs @@ -2,7 +2,9 @@ use iced_core::Color; use once_cell::sync::Lazy; -use palette::{FromColor, Hsl, Mix, RelativeContrast, Srgb}; +use palette::color_difference::Wcag21RelativeContrast; +use palette::rgb::Rgb; +use palette::{FromColor, Hsl, Mix}; /// A color palette. #[derive(Debug, Clone, Copy, PartialEq)] @@ -298,11 +300,11 @@ fn deviate(color: Color, amount: f32) -> Color { } fn mix(a: Color, b: Color, factor: f32) -> Color { - let a_lin = Srgb::from(a).into_linear(); - let b_lin = Srgb::from(b).into_linear(); + let a_lin = Rgb::from(a).into_linear(); + let b_lin = Rgb::from(b).into_linear(); - let mixed = a_lin.mix(&b_lin, factor); - Srgb::from_linear(mixed).into() + let mixed = a_lin.mix(b_lin, factor); + Rgb::from_linear(mixed).into() } fn readable(background: Color, text: Color) -> Color { @@ -320,16 +322,16 @@ fn is_dark(color: Color) -> bool { } fn is_readable(a: Color, b: Color) -> bool { - let a_srgb = Srgb::from(a); - let b_srgb = Srgb::from(b); + let a_srgb = Rgb::from(a); + let b_srgb = Rgb::from(b); - a_srgb.has_enhanced_contrast_text(&b_srgb) + a_srgb.has_enhanced_contrast_text(b_srgb) } fn to_hsl(color: Color) -> Hsl { - Hsl::from_color(Srgb::from(color)) + Hsl::from_color(Rgb::from(color)) } fn from_hsl(hsl: Hsl) -> Color { - Srgb::from_color(hsl).into() + Rgb::from_color(hsl).into() } diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml index 400eee6a..ef993fb9 100644 --- a/tiny_skia/Cargo.toml +++ b/tiny_skia/Cargo.toml @@ -24,7 +24,7 @@ features = ["tiny-skia"] [dependencies.cosmic-text] git = "https://github.com/hecrj/cosmic-text.git" -rev = "b85d6a4f2376f8a8a7dadc0f8bcb89d4db10a1c9" +rev = "c3cd24dc972bb8fd55d016c81ac9fa637e0a4ada" [dependencies.twox-hash] version = "1.6" diff --git a/tiny_skia/fonts/Iced-Icons.ttf b/tiny_skia/fonts/Iced-Icons.ttf Binary files differnew file mode 100644 index 00000000..e3273141 --- /dev/null +++ b/tiny_skia/fonts/Iced-Icons.ttf diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 87738174..ba038052 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -1,4 +1,5 @@ use crate::core::text; +use crate::core::Gradient; use crate::core::{Background, Color, Font, Point, Rectangle, Size, Vector}; use crate::graphics::backend; use crate::graphics::{Primitive, Viewport}; @@ -91,6 +92,7 @@ impl Backend { background_color, )), anti_alias: false, + blend_mode: tiny_skia::BlendMode::Source, ..Default::default() }, tiny_skia::FillRule::default(), @@ -194,6 +196,48 @@ impl Backend { *color, )) } + Background::Gradient(Gradient::Linear(linear)) => { + let (start, end) = + linear.angle.to_distance(bounds); + + let stops: Vec<tiny_skia::GradientStop> = + linear + .stops + .into_iter() + .flatten() + .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::LinearGradient::new( + tiny_skia::Point { + x: start.x, + y: start.y, + }, + tiny_skia::Point { x: end.x, y: end.y }, + if stops.is_empty() { + vec![tiny_skia::GradientStop::new( + 0.0, + tiny_skia::Color::BLACK, + )] + } else { + stops + }, + tiny_skia::SpreadMode::Pad, + tiny_skia::Transform::identity(), + ) + .expect("Create linear gradient") + } }, anti_alias: true, ..tiny_skia::Paint::default() @@ -722,12 +766,6 @@ fn adjust_clip_mask(clip_mask: &mut tiny_skia::Mask, bounds: Rectangle) { ); } -impl iced_graphics::Backend for Backend { - fn trim_measurements(&mut self) { - self.text_pipeline.trim_measurement_cache(); - } -} - impl backend::Text for Backend { const ICON_FONT: Font = Font::with_name("Iced-Icons"); const CHECKMARK_ICON: char = '\u{f00c}'; diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs index a445b561..ee347c73 100644 --- a/tiny_skia/src/geometry.rs +++ b/tiny_skia/src/geometry.rs @@ -1,8 +1,8 @@ -use crate::core::Gradient; use crate::core::{Point, Rectangle, Size, Vector}; use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::stroke::{self, Stroke}; use crate::graphics::geometry::{Path, Style, Text}; +use crate::graphics::Gradient; use crate::graphics::Primitive; pub struct Frame { @@ -127,9 +127,9 @@ impl Frame { self.transform = self.stack.pop().expect("Pop transform"); } - pub fn clip(&mut self, frame: Self, translation: Vector) { + pub fn clip(&mut self, frame: Self, at: Point) { self.primitives.push(Primitive::Translate { - translation, + translation: Vector::new(at.x, at.y), content: Box::new(frame.into_primitive()), }); } @@ -231,18 +231,11 @@ pub fn into_paint(style: Style) -> tiny_skia::Paint<'static> { .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 + Gradient::Linear(linear) => { + let stops: Vec<tiny_skia::GradientStop> = linear + .stops .into_iter() + .flatten() .map(|stop| { tiny_skia::GradientStop::new( stop.offset, @@ -255,11 +248,30 @@ pub fn into_paint(style: Style) -> tiny_skia::Paint<'static> { .expect("Create color"), ) }) - .collect(), - tiny_skia::SpreadMode::Pad, - tiny_skia::Transform::identity(), - ) - .expect("Create linear gradient"), + .collect(); + + 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, + }, + if stops.is_empty() { + vec![tiny_skia::GradientStop::new( + 0.0, + tiny_skia::Color::BLACK, + )] + } else { + stops + }, + tiny_skia::SpreadMode::Pad, + tiny_skia::Transform::identity(), + ) + .expect("Create linear gradient") + } }, }, anti_alias: true, diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index a34c7317..3441da8f 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -14,8 +14,7 @@ use std::sync::Arc; pub struct Pipeline { font_system: RefCell<cosmic_text::FontSystem>, glyph_cache: GlyphCache, - measurement_cache: RefCell<Cache>, - render_cache: Cache, + cache: RefCell<Cache>, } impl Pipeline { @@ -23,14 +22,12 @@ impl Pipeline { Pipeline { font_system: RefCell::new(cosmic_text::FontSystem::new_with_fonts( [cosmic_text::fontdb::Source::Binary(Arc::new( - include_bytes!("../../wgpu/fonts/Iced-Icons.ttf") - .as_slice(), + include_bytes!("../fonts/Iced-Icons.ttf").as_slice(), ))] .into_iter(), )), glyph_cache: GlyphCache::new(), - measurement_cache: RefCell::new(Cache::new()), - render_cache: Cache::new(), + cache: RefCell::new(Cache::new()), } } @@ -38,6 +35,8 @@ impl Pipeline { self.font_system.get_mut().db_mut().load_font_source( cosmic_text::fontdb::Source::Binary(Arc::new(bytes.into_owned())), ); + + self.cache = RefCell::new(Cache::new()); } pub fn draw( @@ -55,20 +54,11 @@ impl Pipeline { pixels: &mut tiny_skia::PixmapMut<'_>, clip_mask: Option<&tiny_skia::Mask>, ) { - let line_height = - f32::from(line_height.to_absolute(Pixels(size))) * scale_factor; - - let bounds = bounds * scale_factor; - let size = size * scale_factor; + let line_height = f32::from(line_height.to_absolute(Pixels(size))); let font_system = self.font_system.get_mut(); let key = Key { - bounds: { - let size = bounds.size(); - - // TODO: Reuse buffers from layouting - Size::new(size.width.ceil(), size.height.ceil()) - }, + bounds: bounds.size(), content, font, size, @@ -76,7 +66,7 @@ impl Pipeline { shaping, }; - let (_, buffer) = self.render_cache.allocate(font_system, key); + let (_, buffer) = self.cache.get_mut().allocate(font_system, key); let (total_lines, max_width) = buffer .layout_runs() @@ -85,7 +75,10 @@ impl Pipeline { (i + 1, buffer.line_w.max(max)) }); - let total_height = total_lines as f32 * line_height; + let total_height = total_lines as f32 * line_height * scale_factor; + let max_width = max_width * scale_factor; + + let bounds = bounds * scale_factor; let x = match horizontal_alignment { alignment::Horizontal::Left => bounds.x, @@ -99,16 +92,14 @@ impl Pipeline { alignment::Vertical::Bottom => bounds.y - total_height, }; - // TODO: Subpixel glyph positioning - let x = x.round() as i32; - let y = y.round() as i32; - let mut swash = cosmic_text::SwashCache::new(); for run in buffer.layout_runs() { for glyph in run.glyphs { + let physical_glyph = glyph.physical((x, y), scale_factor); + if let Some((buffer, placement)) = self.glyph_cache.allocate( - glyph.cache_key, + physical_glyph.cache_key, color, font_system, &mut swash, @@ -121,8 +112,9 @@ impl Pipeline { .expect("Create glyph pixel map"); pixels.draw_pixmap( - x + glyph.x_int + placement.left, - y - glyph.y_int - placement.top + run.line_y as i32, + physical_glyph.x + placement.left, + physical_glyph.y - placement.top + + (run.line_y * scale_factor).round() as i32, pixmap, &tiny_skia::PixmapPaint::default(), tiny_skia::Transform::identity(), @@ -134,7 +126,7 @@ impl Pipeline { } pub fn trim_cache(&mut self) { - self.render_cache.trim(); + self.cache.get_mut().trim(); self.glyph_cache.trim(); } @@ -147,7 +139,7 @@ impl Pipeline { bounds: Size, shaping: Shaping, ) -> (f32, f32) { - let mut measurement_cache = self.measurement_cache.borrow_mut(); + let mut measurement_cache = self.cache.borrow_mut(); let line_height = f32::from(line_height.to_absolute(Pixels(size))); @@ -184,7 +176,7 @@ impl Pipeline { point: Point, _nearest_only: bool, ) -> Option<Hit> { - let mut measurement_cache = self.measurement_cache.borrow_mut(); + let mut measurement_cache = self.cache.borrow_mut(); let line_height = f32::from(line_height.to_absolute(Pixels(size))); @@ -204,10 +196,6 @@ impl Pipeline { Some(Hit::CharOffset(cursor.index)) } - - pub fn trim_measurement_cache(&mut self) { - self.measurement_cache.borrow_mut().trim(); - } } fn to_family(family: font::Family) -> cosmic_text::Family<'static> { diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 9999a188..f3be3f16 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -1,5 +1,5 @@ -use crate::core::{Color, Rectangle}; -use crate::graphics::compositor::{self, Information, SurfaceError}; +use crate::core::{Color, Rectangle, Size}; +use crate::graphics::compositor::{self, Information}; use crate::graphics::damage; use crate::graphics::{Error, Primitive, Viewport}; use crate::{Backend, Renderer, Settings}; @@ -79,7 +79,7 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { viewport: &Viewport, background_color: Color, overlay: &[T], - ) -> Result<(), SurfaceError> { + ) -> Result<(), compositor::SurfaceError> { renderer.with_primitives(|backend, primitives| { present( backend, @@ -91,6 +91,26 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { ) }) } + + fn screenshot<T: AsRef<str>>( + &mut self, + renderer: &mut Self::Renderer, + surface: &mut Self::Surface, + viewport: &Viewport, + background_color: Color, + overlay: &[T], + ) -> Vec<u8> { + renderer.with_primitives(|backend, primitives| { + screenshot( + surface, + backend, + primitives, + viewport, + background_color, + overlay, + ) + }) + } } pub fn new<Theme>(settings: Settings) -> (Compositor<Theme>, Backend) { @@ -156,3 +176,53 @@ pub fn present<T: AsRef<str>>( Ok(()) } + +pub fn screenshot<T: AsRef<str>>( + surface: &mut Surface, + backend: &mut Backend, + primitives: &[Primitive], + viewport: &Viewport, + background_color: Color, + overlay: &[T], +) -> Vec<u8> { + let size = viewport.physical_size(); + + let mut offscreen_buffer: Vec<u32> = + vec![0; size.width as usize * size.height as usize]; + + backend.draw( + &mut tiny_skia::PixmapMut::from_bytes( + bytemuck::cast_slice_mut(&mut offscreen_buffer), + size.width, + size.height, + ) + .expect("Create offscreen pixel map"), + &mut surface.clip_mask, + primitives, + viewport, + &[Rectangle::with_size(Size::new( + size.width as f32, + size.height as f32, + ))], + background_color, + overlay, + ); + + offscreen_buffer.iter().fold( + Vec::with_capacity(offscreen_buffer.len() * 4), + |mut acc, pixel| { + const A_MASK: u32 = 0xFF_00_00_00; + const R_MASK: u32 = 0x00_FF_00_00; + const G_MASK: u32 = 0x00_00_FF_00; + const B_MASK: u32 = 0x00_00_00_FF; + + let a = ((A_MASK & pixel) >> 24) as u8; + let r = ((R_MASK & pixel) >> 16) as u8; + let g = ((G_MASK & pixel) >> 8) as u8; + let b = (B_MASK & pixel) as u8; + + acc.extend([r, g, b, a]); + acc + }, + ) +} diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 41eb4c23..15db5b5d 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -11,6 +11,7 @@ repository = "https://github.com/iced-rs/iced" geometry = ["iced_graphics/geometry", "lyon"] image = ["iced_graphics/image"] svg = ["resvg"] +web-colors = ["iced_graphics/web-colors"] [dependencies] wgpu = "0.16" @@ -44,14 +45,10 @@ path = "../graphics" [dependencies.glyphon] version = "0.2" git = "https://github.com/hecrj/glyphon.git" -rev = "f145067d292082abdd1f2b2481812d4a52c394ec" - -[dependencies.encase] -version = "0.3.0" -features = ["glam"] +rev = "8324f20158a62f8520bad4ed09f6aa5552f8f2a6" [dependencies.glam] -version = "0.21.3" +version = "0.24" [dependencies.lyon] version = "1.0" diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index def80a81..eecba2f1 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -1,6 +1,7 @@ use crate::core; use crate::core::{Color, Font, Point, Size}; use crate::graphics::backend; +use crate::graphics::color; use crate::graphics::{Primitive, Transformation, Viewport}; use crate::quad; use crate::text; @@ -239,7 +240,7 @@ impl Backend { load: match clear_color { Some(background_color) => wgpu::LoadOp::Clear({ let [r, g, b, a] = - background_color.into_linear(); + color::pack(background_color).components(); wgpu::Color { r: f64::from(r), @@ -265,8 +266,12 @@ impl Backend { } if !layer.quads.is_empty() { - self.quad_pipeline - .render(quad_layer, bounds, &mut render_pass); + self.quad_pipeline.render( + quad_layer, + bounds, + &layer.quads, + &mut render_pass, + ); quad_layer += 1; } @@ -329,12 +334,6 @@ impl Backend { } } -impl iced_graphics::Backend for Backend { - fn trim_measurements(&mut self) { - self.text_pipeline.trim_measurement_cache() - } -} - impl backend::Text for Backend { const ICON_FONT: Font = Font::with_name("Iced-Icons"); const CHECKMARK_ICON: char = '\u{f00c}'; diff --git a/wgpu/src/buffer.rs b/wgpu/src/buffer.rs index c210dd4e..94122187 100644 --- a/wgpu/src/buffer.rs +++ b/wgpu/src/buffer.rs @@ -1,7 +1,3 @@ -//! Utilities for buffer operations. -pub mod dynamic; -pub mod r#static; - use std::marker::PhantomData; use std::ops::RangeBounds; @@ -10,7 +6,8 @@ pub struct Buffer<T> { label: &'static str, size: u64, usage: wgpu::BufferUsages, - raw: wgpu::Buffer, + pub(crate) raw: wgpu::Buffer, + offsets: Vec<wgpu::BufferAddress>, type_: PhantomData<T>, } @@ -35,6 +32,7 @@ impl<T: bytemuck::Pod> Buffer<T> { size, usage, raw, + offsets: Vec::new(), type_: PhantomData, } } @@ -43,6 +41,8 @@ impl<T: bytemuck::Pod> Buffer<T> { let new_size = (std::mem::size_of::<T>() * new_count) as u64; if self.size < new_size { + self.offsets.clear(); + self.raw = device.create_buffer(&wgpu::BufferDescriptor { label: Some(self.label), size: new_size, @@ -58,17 +58,19 @@ impl<T: bytemuck::Pod> Buffer<T> { } } + /// Returns the size of the written bytes. pub fn write( - &self, + &mut self, queue: &wgpu::Queue, - offset_count: usize, + offset: usize, contents: &[T], - ) { - queue.write_buffer( - &self.raw, - (std::mem::size_of::<T>() * offset_count) as u64, - bytemuck::cast_slice(contents), - ); + ) -> usize { + let bytes: &[u8] = bytemuck::cast_slice(contents); + queue.write_buffer(&self.raw, offset as u64, bytes); + + self.offsets.push(offset as u64); + + bytes.len() } pub fn slice( @@ -77,6 +79,21 @@ impl<T: bytemuck::Pod> Buffer<T> { ) -> wgpu::BufferSlice<'_> { self.raw.slice(bounds) } + + /// Returns the slice calculated from the offset stored at the given index. + pub fn slice_from_index(&self, index: usize) -> wgpu::BufferSlice<'_> { + self.raw.slice(self.offset_at(index)..) + } + + /// Clears any temporary data (i.e. offsets) from the buffer. + pub fn clear(&mut self) { + self.offsets.clear() + } + + /// Returns the offset at `index`, if it exists. + fn offset_at(&self, index: usize) -> &wgpu::BufferAddress { + self.offsets.get(index).expect("No offset at index.") + } } fn next_copy_size<T>(amount: usize) -> u64 { diff --git a/wgpu/src/buffer/dynamic.rs b/wgpu/src/buffer/dynamic.rs deleted file mode 100644 index 43fc47ac..00000000 --- a/wgpu/src/buffer/dynamic.rs +++ /dev/null @@ -1,202 +0,0 @@ -//! Utilities for uniform buffer operations. -use encase::private::WriteInto; -use encase::ShaderType; - -use std::fmt; -use std::marker::PhantomData; - -/// A dynamic buffer is any type of buffer which does not have a static offset. -#[derive(Debug)] -pub struct Buffer<T: ShaderType> { - offsets: Vec<wgpu::DynamicOffset>, - cpu: Internal, - gpu: wgpu::Buffer, - label: &'static str, - size: u64, - _data: PhantomData<T>, -} - -impl<T: ShaderType + WriteInto> Buffer<T> { - /// Creates a new dynamic uniform buffer. - pub fn uniform(device: &wgpu::Device, label: &'static str) -> Self { - Buffer::new( - device, - Internal::Uniform(encase::DynamicUniformBuffer::new(Vec::new())), - label, - wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - ) - } - - #[cfg(not(target_arch = "wasm32"))] - /// Creates a new dynamic storage buffer. - pub fn storage(device: &wgpu::Device, label: &'static str) -> Self { - Buffer::new( - device, - Internal::Storage(encase::DynamicStorageBuffer::new(Vec::new())), - label, - wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, - ) - } - - fn new( - device: &wgpu::Device, - dynamic_buffer_type: Internal, - label: &'static str, - usage: wgpu::BufferUsages, - ) -> Self { - let initial_size = u64::from(T::min_size()); - - Self { - offsets: Vec::new(), - cpu: dynamic_buffer_type, - gpu: Buffer::<T>::create_gpu_buffer( - device, - label, - usage, - initial_size, - ), - label, - size: initial_size, - _data: Default::default(), - } - } - - fn create_gpu_buffer( - device: &wgpu::Device, - label: &'static str, - usage: wgpu::BufferUsages, - size: u64, - ) -> wgpu::Buffer { - device.create_buffer(&wgpu::BufferDescriptor { - label: Some(label), - size, - usage, - mapped_at_creation: false, - }) - } - - /// Write a new value to the CPU buffer with proper alignment. Stores the returned offset value - /// in the buffer for future use. - pub fn push(&mut self, value: &T) { - //this write operation on the cpu buffer will adjust for uniform alignment requirements - let offset = self.cpu.write(value); - self.offsets.push(offset); - } - - /// Resize buffer contents if necessary. This will re-create the GPU buffer if current size is - /// less than the newly computed size from the CPU buffer. - /// - /// If the gpu buffer is resized, its bind group will need to be recreated! - pub fn resize(&mut self, device: &wgpu::Device) -> bool { - let new_size = self.cpu.get_ref().len() as u64; - - if self.size < new_size { - let usages = match self.cpu { - Internal::Uniform(_) => { - wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST - } - #[cfg(not(target_arch = "wasm32"))] - Internal::Storage(_) => { - wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST - } - }; - - self.gpu = Buffer::<T>::create_gpu_buffer( - device, self.label, usages, new_size, - ); - self.size = new_size; - true - } else { - false - } - } - - /// Write the contents of this dynamic buffer to the GPU via staging belt command. - pub fn write(&mut self, queue: &wgpu::Queue) { - queue.write_buffer(&self.gpu, 0, self.cpu.get_ref()); - } - - // Gets the aligned offset at the given index from the CPU buffer. - pub fn offset_at_index(&self, index: usize) -> wgpu::DynamicOffset { - let offset = self - .offsets - .get(index) - .copied() - .expect("Index not found in offsets."); - - offset - } - - /// Returns a reference to the GPU buffer. - pub fn raw(&self) -> &wgpu::Buffer { - &self.gpu - } - - /// Reset the buffer. - pub fn clear(&mut self) { - self.offsets.clear(); - self.cpu.clear(); - } -} - -// Currently supported dynamic buffers. -enum Internal { - Uniform(encase::DynamicUniformBuffer<Vec<u8>>), - #[cfg(not(target_arch = "wasm32"))] - //storage buffers are not supported on wgpu wasm target (yet) - Storage(encase::DynamicStorageBuffer<Vec<u8>>), -} - -impl Internal { - /// Writes the current value to its CPU buffer with proper alignment. - pub(super) fn write<T: ShaderType + WriteInto>( - &mut self, - value: &T, - ) -> wgpu::DynamicOffset { - match self { - Internal::Uniform(buf) => buf - .write(value) - .expect("Error when writing to dynamic uniform buffer.") - as u32, - #[cfg(not(target_arch = "wasm32"))] - Internal::Storage(buf) => buf - .write(value) - .expect("Error when writing to dynamic storage buffer.") - as u32, - } - } - - /// Returns bytearray of aligned CPU buffer. - pub(super) fn get_ref(&self) -> &[u8] { - match self { - Internal::Uniform(buf) => buf.as_ref(), - #[cfg(not(target_arch = "wasm32"))] - Internal::Storage(buf) => buf.as_ref(), - } - } - - /// Resets the CPU buffer. - pub(super) fn clear(&mut self) { - match self { - Internal::Uniform(buf) => { - buf.as_mut().clear(); - buf.set_offset(0); - } - #[cfg(not(target_arch = "wasm32"))] - Internal::Storage(buf) => { - buf.as_mut().clear(); - buf.set_offset(0); - } - } - } -} - -impl fmt::Debug for Internal { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Uniform(_) => write!(f, "Internal::Uniform(_)"), - #[cfg(not(target_arch = "wasm32"))] - Self::Storage(_) => write!(f, "Internal::Storage(_)"), - } - } -} diff --git a/wgpu/src/buffer/static.rs b/wgpu/src/buffer/static.rs deleted file mode 100644 index d8ae116e..00000000 --- a/wgpu/src/buffer/static.rs +++ /dev/null @@ -1,107 +0,0 @@ -use bytemuck::{Pod, Zeroable}; -use std::marker::PhantomData; -use std::mem; - -const DEFAULT_COUNT: wgpu::BufferAddress = 128; - -/// A generic buffer struct useful for items which have no alignment requirements -/// (e.g. Vertex, Index buffers) & no dynamic offsets. -#[derive(Debug)] -pub struct Buffer<T> { - //stored sequentially per mesh iteration; refers to the offset index in the GPU buffer - offsets: Vec<wgpu::BufferAddress>, - label: &'static str, - usages: wgpu::BufferUsages, - gpu: wgpu::Buffer, - size: wgpu::BufferAddress, - _data: PhantomData<T>, -} - -impl<T: Pod + Zeroable> Buffer<T> { - /// Initialize a new static buffer. - pub fn new( - device: &wgpu::Device, - label: &'static str, - usages: wgpu::BufferUsages, - ) -> Self { - let size = (mem::size_of::<T>() as u64) * DEFAULT_COUNT; - - Self { - offsets: Vec::new(), - label, - usages, - gpu: Self::gpu_buffer(device, label, size, usages), - size, - _data: PhantomData, - } - } - - fn gpu_buffer( - device: &wgpu::Device, - label: &'static str, - size: wgpu::BufferAddress, - usage: wgpu::BufferUsages, - ) -> wgpu::Buffer { - device.create_buffer(&wgpu::BufferDescriptor { - label: Some(label), - size, - usage, - mapped_at_creation: false, - }) - } - - /// Returns whether or not the buffer needs to be recreated. This can happen whenever mesh data - /// changes & a redraw is requested. - pub fn resize(&mut self, device: &wgpu::Device, new_count: usize) -> bool { - let size = (mem::size_of::<T>() * new_count) as u64; - - if self.size < size { - self.size = - (mem::size_of::<T>() * (new_count + new_count / 2)) as u64; - - self.gpu = - Self::gpu_buffer(device, self.label, self.size, self.usages); - - self.offsets.clear(); - true - } else { - false - } - } - - /// Writes the current vertex data to the gpu buffer with a memcpy & stores its offset. - /// - /// Returns the size of the written bytes. - pub fn write( - &mut self, - queue: &wgpu::Queue, - offset: u64, - content: &[T], - ) -> u64 { - let bytes = bytemuck::cast_slice(content); - let bytes_size = bytes.len() as u64; - - queue.write_buffer(&self.gpu, offset, bytes); - self.offsets.push(offset); - - bytes_size - } - - fn offset_at(&self, index: usize) -> &wgpu::BufferAddress { - self.offsets - .get(index) - .expect("Offset at index does not exist.") - } - - /// Returns the slice calculated from the offset stored at the given index. - /// e.g. to calculate the slice for the 2nd mesh in the layer, this would be the offset at index - /// 1 that we stored earlier when writing. - pub fn slice_from_index(&self, index: usize) -> wgpu::BufferSlice<'_> { - self.gpu.slice(self.offset_at(index)..) - } - - /// Clears any temporary data from the buffer. - pub fn clear(&mut self) { - self.offsets.clear() - } -} diff --git a/wgpu/src/color.rs b/wgpu/src/color.rs new file mode 100644 index 00000000..a1025601 --- /dev/null +++ b/wgpu/src/color.rs @@ -0,0 +1,165 @@ +use std::borrow::Cow; + +pub fn convert( + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + source: wgpu::Texture, + format: wgpu::TextureFormat, +) -> wgpu::Texture { + if source.format() == format { + return source; + } + + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("iced_wgpu.offscreen.sampler"), + ..Default::default() + }); + + //sampler in 0 + let sampler_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("iced_wgpu.offscreen.blit.sampler_layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler( + wgpu::SamplerBindingType::NonFiltering, + ), + count: None, + }], + }); + + let sampler_bind_group = + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("iced_wgpu.offscreen.sampler.bind_group"), + layout: &sampler_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Sampler(&sampler), + }], + }); + + let texture_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("iced_wgpu.offscreen.blit.texture_layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { + filterable: false, + }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }], + }); + + let pipeline_layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("iced_wgpu.offscreen.blit.pipeline_layout"), + bind_group_layouts: &[&sampler_layout, &texture_layout], + push_constant_ranges: &[], + }); + + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("iced_wgpu.offscreen.blit.shader"), + source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!( + "shader/blit.wgsl" + ))), + }); + + let pipeline = + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("iced_wgpu.offscreen.blit.pipeline"), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "vs_main", + buffers: &[], + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs_main", + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState { + color: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::SrcAlpha, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + alpha: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + }), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + front_face: wgpu::FrontFace::Cw, + ..Default::default() + }, + depth_stencil: None, + multisample: Default::default(), + multiview: None, + }); + + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("iced_wgpu.offscreen.conversion.source_texture"), + size: source.size(), + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::COPY_SRC, + view_formats: &[], + }); + + let view = &texture.create_view(&wgpu::TextureViewDescriptor::default()); + + let texture_bind_group = + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("iced_wgpu.offscreen.blit.texture_bind_group"), + layout: &texture_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView( + &source + .create_view(&wgpu::TextureViewDescriptor::default()), + ), + }], + }); + + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("iced_wgpu.offscreen.blit.render_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: true, + }, + })], + depth_stencil_attachment: None, + }); + + pass.set_pipeline(&pipeline); + pass.set_bind_group(0, &sampler_bind_group, &[]); + pass.set_bind_group(1, &texture_bind_group, &[]); + pass.draw(0..6, 0..1); + + texture +} + +#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +#[repr(C)] +struct Vertex { + ndc: [f32; 2], + uv: [f32; 2], +} diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index 8cfed1e5..f81b5b2f 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -1,17 +1,19 @@ -use crate::core::{Gradient, Point, Rectangle, Size, Vector}; +//! Build and draw geometry. +use crate::core::{Point, Rectangle, Size, Vector}; +use crate::graphics::color; use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::{ LineCap, LineDash, LineJoin, Path, Stroke, Style, Text, }; use crate::graphics::primitive::{self, Primitive}; +use crate::graphics::Gradient; +use iced_graphics::gradient; use lyon::geom::euclid; use lyon::tessellation; use std::borrow::Cow; -/// The frame of a [`Canvas`]. -/// -/// [`Canvas`]: crate::widget::Canvas +/// A frame for drawing some geometry. #[allow(missing_debug_implementations)] pub struct Frame { size: Size, @@ -24,10 +26,7 @@ pub struct Frame { enum Buffer { Solid(tessellation::VertexBuffers<primitive::ColoredVertex2D, u32>), - Gradient( - tessellation::VertexBuffers<primitive::Vertex2D, u32>, - Gradient, - ), + Gradient(tessellation::VertexBuffers<primitive::GradientVertex2D, u32>), } struct BufferStack { @@ -49,12 +48,11 @@ impl BufferStack { )); } }, - Style::Gradient(gradient) => match self.stack.last() { - Some(Buffer::Gradient(_, last)) if gradient == last => {} + Style::Gradient(_) => match self.stack.last() { + Some(Buffer::Gradient(_)) => {} _ => { self.stack.push(Buffer::Gradient( tessellation::VertexBuffers::new(), - gradient.clone(), )); } }, @@ -71,12 +69,17 @@ impl BufferStack { (Style::Solid(color), Buffer::Solid(buffer)) => { Box::new(tessellation::BuffersBuilder::new( buffer, - TriangleVertex2DBuilder(color.into_linear()), + TriangleVertex2DBuilder(color::pack(*color)), + )) + } + (Style::Gradient(gradient), Buffer::Gradient(buffer)) => { + Box::new(tessellation::BuffersBuilder::new( + buffer, + GradientVertex2DBuilder { + gradient: gradient.pack(), + }, )) } - (Style::Gradient(_), Buffer::Gradient(buffer, _)) => Box::new( - tessellation::BuffersBuilder::new(buffer, Vertex2DBuilder), - ), _ => unreachable!(), } } @@ -89,12 +92,17 @@ impl BufferStack { (Style::Solid(color), Buffer::Solid(buffer)) => { Box::new(tessellation::BuffersBuilder::new( buffer, - TriangleVertex2DBuilder(color.into_linear()), + TriangleVertex2DBuilder(color::pack(*color)), + )) + } + (Style::Gradient(gradient), Buffer::Gradient(buffer)) => { + Box::new(tessellation::BuffersBuilder::new( + buffer, + GradientVertex2DBuilder { + gradient: gradient.pack(), + }, )) } - (Style::Gradient(_), Buffer::Gradient(buffer, _)) => Box::new( - tessellation::BuffersBuilder::new(buffer, Vertex2DBuilder), - ), _ => unreachable!(), } } @@ -132,11 +140,13 @@ impl Transform { } 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); + match &mut gradient { + Gradient::Linear(linear) => { + self.transform_point(&mut linear.start); + self.transform_point(&mut linear.end); + } + } + gradient } } @@ -353,10 +363,12 @@ impl Frame { self.pop_transform(); } + /// Pushes the current transform in the transform stack. pub fn push_transform(&mut self) { self.transforms.previous.push(self.transforms.current); } + /// Pops a transform from the transform stack and sets it as the current transform. pub fn pop_transform(&mut self) { self.transforms.current = self.transforms.previous.pop().unwrap(); } @@ -373,14 +385,16 @@ impl Frame { f(&mut frame); - let translation = Vector::new(region.x, region.y); + let origin = Point::new(region.x, region.y); - self.clip(frame, translation); + self.clip(frame, origin); } - pub fn clip(&mut self, frame: Frame, translation: Vector) { + /// Draws the clipped contents of the given [`Frame`] with origin at the given [`Point`]. + pub fn clip(&mut self, frame: Frame, at: Point) { let size = frame.size(); let primitives = frame.into_primitives(); + let translation = Vector::new(at.x, at.y); let (text, meshes) = primitives .into_iter() @@ -459,7 +473,7 @@ impl Frame { }) } } - Buffer::Gradient(buffer, gradient) => { + Buffer::Gradient(buffer) => { if !buffer.indices.is_empty() { self.primitives.push(Primitive::GradientMesh { buffers: primitive::Mesh2D { @@ -467,7 +481,6 @@ impl Frame { indices: buffer.indices, }, size: self.size, - gradient, }) } } @@ -478,39 +491,43 @@ impl Frame { } } -struct Vertex2DBuilder; +struct GradientVertex2DBuilder { + gradient: gradient::Packed, +} -impl tessellation::FillVertexConstructor<primitive::Vertex2D> - for Vertex2DBuilder +impl tessellation::FillVertexConstructor<primitive::GradientVertex2D> + for GradientVertex2DBuilder { fn new_vertex( &mut self, vertex: tessellation::FillVertex<'_>, - ) -> primitive::Vertex2D { + ) -> primitive::GradientVertex2D { let position = vertex.position(); - primitive::Vertex2D { + primitive::GradientVertex2D { position: [position.x, position.y], + gradient: self.gradient, } } } -impl tessellation::StrokeVertexConstructor<primitive::Vertex2D> - for Vertex2DBuilder +impl tessellation::StrokeVertexConstructor<primitive::GradientVertex2D> + for GradientVertex2DBuilder { fn new_vertex( &mut self, vertex: tessellation::StrokeVertex<'_, '_>, - ) -> primitive::Vertex2D { + ) -> primitive::GradientVertex2D { let position = vertex.position(); - primitive::Vertex2D { + primitive::GradientVertex2D { position: [position.x, position.y], + gradient: self.gradient, } } } -struct TriangleVertex2DBuilder([f32; 4]); +struct TriangleVertex2DBuilder(color::Packed); impl tessellation::FillVertexConstructor<primitive::ColoredVertex2D> for TriangleVertex2DBuilder diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs index 263bcfa2..553ba330 100644 --- a/wgpu/src/image.rs +++ b/wgpu/src/image.rs @@ -121,7 +121,7 @@ impl Layer { ); let _ = self.instances.resize(device, instances.len()); - self.instances.write(queue, 0, instances); + let _ = self.instances.write(queue, 0, instances); self.instance_count = instances.len(); } @@ -278,7 +278,7 @@ impl Pipeline { let vertices = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("iced_wgpu::image vertex buffer"), - contents: bytemuck::cast_slice(&QUAD_VERTS), + contents: bytemuck::cast_slice(&QUAD_VERTICES), usage: wgpu::BufferUsages::VERTEX, }); @@ -369,7 +369,6 @@ impl Pipeline { layer::Image::Raster { handle, bounds } => { if let Some(atlas_entry) = raster_cache.upload( device, - queue, encoder, handle, &mut self.texture_atlas, @@ -395,7 +394,6 @@ impl Pipeline { if let Some(atlas_entry) = vector_cache.upload( device, - queue, encoder, handle, *color, @@ -500,7 +498,7 @@ pub struct Vertex { const QUAD_INDICES: [u16; 6] = [0, 1, 2, 0, 2, 3]; -const QUAD_VERTS: [Vertex; 4] = [ +const QUAD_VERTICES: [Vertex; 4] = [ Vertex { _position: [0.0, 0.0], }, diff --git a/wgpu/src/image/atlas.rs b/wgpu/src/image/atlas.rs index 366fe623..e3de1290 100644 --- a/wgpu/src/image/atlas.rs +++ b/wgpu/src/image/atlas.rs @@ -13,6 +13,7 @@ use allocator::Allocator; pub const SIZE: u32 = 2048; use crate::core::Size; +use crate::graphics::color; #[derive(Debug)] pub struct Atlas { @@ -35,7 +36,11 @@ impl Atlas { mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8UnormSrgb, + format: if color::GAMMA_CORRECTION { + wgpu::TextureFormat::Rgba8UnormSrgb + } else { + wgpu::TextureFormat::Rgba8Unorm + }, usage: wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::TEXTURE_BINDING, @@ -65,7 +70,6 @@ impl Atlas { pub fn upload( &mut self, device: &wgpu::Device, - queue: &wgpu::Queue, encoder: &mut wgpu::CommandEncoder, width: u32, height: u32, @@ -112,7 +116,8 @@ impl Atlas { padding, 0, allocation, - queue, + device, + encoder, ); } Entry::Fragmented { fragments, .. } => { @@ -127,7 +132,8 @@ impl Atlas { padding, offset, &fragment.allocation, - queue, + device, + encoder, ); } } @@ -280,8 +286,11 @@ impl Atlas { padding: u32, offset: usize, allocation: &Allocation, - queue: &wgpu::Queue, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, ) { + use wgpu::util::DeviceExt; + let (x, y) = allocation.position(); let Size { width, height } = allocation.size(); let layer = allocation.layer(); @@ -292,7 +301,22 @@ impl Atlas { depth_or_array_layers: 1, }; - queue.write_texture( + let buffer = + device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("image upload buffer"), + contents: data, + usage: wgpu::BufferUsages::COPY_SRC, + }); + + encoder.copy_buffer_to_texture( + wgpu::ImageCopyBuffer { + buffer: &buffer, + layout: wgpu::ImageDataLayout { + offset: offset as u64, + bytes_per_row: Some(4 * image_width + padding), + rows_per_image: Some(image_height), + }, + }, wgpu::ImageCopyTexture { texture: &self.texture, mip_level: 0, @@ -303,12 +327,6 @@ impl Atlas { }, aspect: wgpu::TextureAspect::default(), }, - data, - wgpu::ImageDataLayout { - offset: offset as u64, - bytes_per_row: Some(4 * image_width + padding), - rows_per_image: Some(image_height), - }, extent, ); } @@ -333,7 +351,11 @@ impl Atlas { mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8UnormSrgb, + format: if color::GAMMA_CORRECTION { + wgpu::TextureFormat::Rgba8UnormSrgb + } else { + wgpu::TextureFormat::Rgba8Unorm + }, usage: wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::TEXTURE_BINDING, diff --git a/wgpu/src/image/raster.rs b/wgpu/src/image/raster.rs index 9b38dce4..a6cba76a 100644 --- a/wgpu/src/image/raster.rs +++ b/wgpu/src/image/raster.rs @@ -63,7 +63,6 @@ impl Cache { pub fn upload( &mut self, device: &wgpu::Device, - queue: &wgpu::Queue, encoder: &mut wgpu::CommandEncoder, handle: &image::Handle, atlas: &mut Atlas, @@ -73,8 +72,7 @@ impl Cache { if let Memory::Host(image) = memory { let (width, height) = image.dimensions(); - let entry = - atlas.upload(device, queue, encoder, width, height, image)?; + let entry = atlas.upload(device, encoder, width, height, image)?; *memory = Memory::Device(entry); } diff --git a/wgpu/src/image/vector.rs b/wgpu/src/image/vector.rs index 58bdf64a..6b9be651 100644 --- a/wgpu/src/image/vector.rs +++ b/wgpu/src/image/vector.rs @@ -74,7 +74,6 @@ impl Cache { pub fn upload( &mut self, device: &wgpu::Device, - queue: &wgpu::Queue, encoder: &mut wgpu::CommandEncoder, handle: &svg::Handle, color: Option<Color>, @@ -138,8 +137,8 @@ impl Cache { }); } - let allocation = atlas - .upload(device, queue, encoder, width, height, &rgba)?; + let allocation = + atlas.upload(device, encoder, width, height, &rgba)?; log::debug!("allocating {} {}x{}", id, width, height); diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index 8af72b9d..71570e3d 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -1,19 +1,19 @@ //! 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::core; use crate::core::alignment; -use crate::core::{Background, Color, Font, Point, Rectangle, Size, Vector}; +use crate::core::{Color, Font, Point, Rectangle, Size, Vector}; +use crate::graphics::color; use crate::graphics::{Primitive, Viewport}; +use crate::quad::{self, Quad}; /// A group of primitives that should be clipped together. #[derive(Debug)] @@ -22,7 +22,7 @@ pub struct Layer<'a> { pub bounds: Rectangle, /// The quads of the [`Layer`]. - pub quads: Vec<Quad>, + pub quads: quad::Batch, /// The triangle meshes of the [`Layer`]. pub meshes: Vec<Mesh<'a>>, @@ -39,7 +39,7 @@ impl<'a> Layer<'a> { pub fn new(bounds: Rectangle) -> Self { Self { bounds, - quads: Vec::new(), + quads: quad::Batch::default(), meshes: Vec::new(), text: Vec::new(), images: Vec::new(), @@ -145,20 +145,18 @@ impl<'a> Layer<'a> { } => { let layer = &mut layers[current_layer]; - // TODO: Move some of these computations to the GPU (?) - layer.quads.push(Quad { + let quad = 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_color: color::pack(*border_color), border_radius: *border_radius, border_width: *border_width, - border_color: border_color.into_linear(), - }); + }; + + layer.quads.add(quad, background); } Primitive::Image { handle, bounds } => { let layer = &mut layers[current_layer]; @@ -198,11 +196,7 @@ impl<'a> Layer<'a> { }); } } - Primitive::GradientMesh { - buffers, - size, - gradient, - } => { + Primitive::GradientMesh { buffers, size } => { let layer = &mut layers[current_layer]; let bounds = Rectangle::new( @@ -216,7 +210,6 @@ impl<'a> Layer<'a> { origin: Point::new(translation.x, translation.y), buffers, clip_bounds, - gradient, }); } } diff --git a/wgpu/src/layer/mesh.rs b/wgpu/src/layer/mesh.rs index 9dd14391..b7dd9a0b 100644 --- a/wgpu/src/layer/mesh.rs +++ b/wgpu/src/layer/mesh.rs @@ -1,5 +1,5 @@ //! A collection of triangle primitives. -use crate::core::{Gradient, Point, Rectangle}; +use crate::core::{Point, Rectangle}; use crate::graphics::primitive; /// A mesh of triangles. @@ -22,13 +22,10 @@ pub enum Mesh<'a> { origin: Point, /// The vertex and index buffers of the [`Mesh`]. - buffers: &'a primitive::Mesh2D<primitive::Vertex2D>, + buffers: &'a primitive::Mesh2D<primitive::GradientVertex2D>, /// The clipping bounds of the [`Mesh`]. clip_bounds: Rectangle<f32>, - - /// The gradient to apply to the [`Mesh`]. - gradient: &'a Gradient, }, } @@ -65,9 +62,15 @@ pub struct AttributeCount { /// The total amount of solid vertices. pub solid_vertices: usize, + /// The total amount of solid meshes. + pub solids: usize, + /// The total amount of gradient vertices. pub gradient_vertices: usize, + /// The total amount of gradient meshes. + pub gradients: usize, + /// The total amount of indices. pub indices: usize, } @@ -79,10 +82,12 @@ pub fn attribute_count_of<'a>(meshes: &'a [Mesh<'a>]) -> AttributeCount { .fold(AttributeCount::default(), |mut count, mesh| { match mesh { Mesh::Solid { buffers, .. } => { + count.solids += 1; count.solid_vertices += buffers.vertices.len(); count.indices += buffers.indices.len(); } Mesh::Gradient { buffers, .. } => { + count.gradients += 1; count.gradient_vertices += buffers.vertices.len(); count.indices += buffers.indices.len(); } diff --git a/wgpu/src/layer/quad.rs b/wgpu/src/layer/quad.rs deleted file mode 100644 index 0d8bde9d..00000000 --- a/wgpu/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/wgpu/src/lib.rs b/wgpu/src/lib.rs index 0b169140..86a962a5 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, @@ -36,7 +36,7 @@ )] #![forbid(rust_2018_idioms)] #![allow(clippy::inherent_to_string, clippy::type_complexity)] -#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] pub mod layer; pub mod settings; pub mod window; @@ -46,10 +46,13 @@ pub mod geometry; mod backend; mod buffer; +mod color; mod quad; mod text; mod triangle; +use buffer::Buffer; + pub use iced_graphics as graphics; pub use iced_graphics::core; @@ -59,8 +62,6 @@ pub use backend::Backend; pub use layer::Layer; pub use settings::Settings; -use buffer::Buffer; - #[cfg(any(feature = "image", feature = "svg"))] mod image; diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs index 8fa7359e..37d0c623 100644 --- a/wgpu/src/quad.rs +++ b/wgpu/src/quad.rs @@ -1,18 +1,27 @@ -use crate::core::Rectangle; -use crate::graphics::Transformation; -use crate::layer; -use crate::Buffer; +mod gradient; +mod solid; + +use gradient::Gradient; +use solid::Solid; + +use crate::core::{Background, Rectangle}; +use crate::graphics::color; +use crate::graphics::{self, Transformation}; use bytemuck::{Pod, Zeroable}; -use std::mem; use wgpu::util::DeviceExt; +use std::mem; + #[cfg(feature = "tracing")] use tracing::info_span; +const INITIAL_INSTANCES: usize = 2_000; + #[derive(Debug)] pub struct Pipeline { - pipeline: wgpu::RenderPipeline, + solid: solid::Pipeline, + gradient: gradient::Pipeline, constant_layout: wgpu::BindGroupLayout, vertices: wgpu::Buffer, indices: wgpu::Buffer, @@ -39,107 +48,28 @@ impl Pipeline { }], }); - let layout = - device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("iced_wgpu::quad pipeline layout"), - push_constant_ranges: &[], - bind_group_layouts: &[&constant_layout], - }); - - let shader = - device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("iced_wgpu quad shader"), - source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( - include_str!("shader/quad.wgsl"), - )), - }); - - let pipeline = - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("iced_wgpu::quad pipeline"), - layout: Some(&layout), - vertex: wgpu::VertexState { - module: &shader, - entry_point: "vs_main", - buffers: &[ - wgpu::VertexBufferLayout { - array_stride: mem::size_of::<Vertex>() as u64, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &[wgpu::VertexAttribute { - shader_location: 0, - format: wgpu::VertexFormat::Float32x2, - offset: 0, - }], - }, - wgpu::VertexBufferLayout { - array_stride: mem::size_of::<layer::Quad>() as u64, - step_mode: wgpu::VertexStepMode::Instance, - attributes: &wgpu::vertex_attr_array!( - 1 => Float32x2, - 2 => Float32x2, - 3 => Float32x4, - 4 => Float32x4, - 5 => Float32x4, - 6 => Float32, - ), - }, - ], - }, - fragment: Some(wgpu::FragmentState { - module: &shader, - entry_point: "fs_main", - targets: &[Some(wgpu::ColorTargetState { - format, - blend: Some(wgpu::BlendState { - color: wgpu::BlendComponent { - src_factor: wgpu::BlendFactor::SrcAlpha, - dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, - operation: wgpu::BlendOperation::Add, - }, - alpha: wgpu::BlendComponent { - src_factor: wgpu::BlendFactor::One, - dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, - operation: wgpu::BlendOperation::Add, - }, - }), - write_mask: wgpu::ColorWrites::ALL, - })], - }), - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - front_face: wgpu::FrontFace::Cw, - ..Default::default() - }, - depth_stencil: None, - multisample: wgpu::MultisampleState { - count: 1, - mask: !0, - alpha_to_coverage_enabled: false, - }, - multiview: None, - }); - let vertices = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("iced_wgpu::quad vertex buffer"), - contents: bytemuck::cast_slice(&QUAD_VERTS), + contents: bytemuck::cast_slice(&VERTICES), usage: wgpu::BufferUsages::VERTEX, }); let indices = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("iced_wgpu::quad index buffer"), - contents: bytemuck::cast_slice(&QUAD_INDICES), + contents: bytemuck::cast_slice(&INDICES), usage: wgpu::BufferUsages::INDEX, }); - Pipeline { - pipeline, - constant_layout, + Self { vertices, indices, + solid: solid::Pipeline::new(device, format, &constant_layout), + gradient: gradient::Pipeline::new(device, format, &constant_layout), layers: Vec::new(), prepare_layer: 0, + constant_layout, } } @@ -147,7 +77,7 @@ impl Pipeline { &mut self, device: &wgpu::Device, queue: &wgpu::Queue, - instances: &[layer::Quad], + quads: &Batch, transformation: Transformation, scale: f32, ) { @@ -156,7 +86,7 @@ impl Pipeline { } let layer = &mut self.layers[self.prepare_layer]; - layer.prepare(device, queue, instances, transformation, scale); + layer.prepare(device, queue, quads, transformation, scale); self.prepare_layer += 1; } @@ -165,25 +95,49 @@ impl Pipeline { &'a self, layer: usize, bounds: Rectangle<u32>, + quads: &Batch, render_pass: &mut wgpu::RenderPass<'a>, ) { if let Some(layer) = self.layers.get(layer) { - render_pass.set_pipeline(&self.pipeline); - render_pass.set_scissor_rect( bounds.x, bounds.y, bounds.width, bounds.height, ); - render_pass.set_index_buffer( self.indices.slice(..), wgpu::IndexFormat::Uint16, ); render_pass.set_vertex_buffer(0, self.vertices.slice(..)); - layer.draw(render_pass); + let mut solid_offset = 0; + let mut gradient_offset = 0; + + for (kind, count) in &quads.order { + match kind { + Kind::Solid => { + self.solid.render( + render_pass, + &layer.constants, + &layer.solid, + solid_offset..(solid_offset + count), + ); + + solid_offset += count; + } + Kind::Gradient => { + self.gradient.render( + render_pass, + &layer.constants, + &layer.gradient, + gradient_offset..(gradient_offset + count), + ); + + gradient_offset += count; + } + } + } } } @@ -196,8 +150,8 @@ impl Pipeline { struct Layer { constants: wgpu::BindGroup, constants_buffer: wgpu::Buffer, - instances: Buffer<layer::Quad>, - instance_count: usize, + solid: solid::Layer, + gradient: gradient::Layer, } impl Layer { @@ -221,18 +175,11 @@ impl Layer { }], }); - let instances = Buffer::new( - device, - "iced_wgpu::quad instance buffer", - INITIAL_INSTANCES, - wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - ); - Self { constants, constants_buffer, - instances, - instance_count: 0, + solid: solid::Layer::new(device), + gradient: gradient::Layer::new(device), } } @@ -240,7 +187,7 @@ impl Layer { &mut self, device: &wgpu::Device, queue: &wgpu::Queue, - instances: &[layer::Quad], + quads: &Batch, transformation: Transformation, scale: f32, ) { @@ -255,35 +202,138 @@ impl Layer { bytemuck::bytes_of(&uniforms), ); - let _ = self.instances.resize(device, instances.len()); - self.instances.write(queue, 0, instances); - self.instance_count = instances.len(); + self.solid.prepare(device, queue, &quads.solids); + self.gradient.prepare(device, queue, &quads.gradients); } +} - pub fn draw<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { - #[cfg(feature = "tracing")] - let _ = info_span!("Wgpu::Quad", "DRAW").entered(); +/// The properties of a quad. +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +#[repr(C)] +pub struct Quad { + /// The position of the [`Quad`]. + pub position: [f32; 2], - render_pass.set_bind_group(0, &self.constants, &[]); - render_pass.set_vertex_buffer(1, self.instances.slice(..)); + /// The size of the [`Quad`]. + pub size: [f32; 2], - render_pass.draw_indexed( - 0..QUAD_INDICES.len() as u32, - 0, - 0..self.instance_count as u32, - ); + /// The border color of the [`Quad`], in __linear RGB__. + pub border_color: color::Packed, + + /// The border radii of the [`Quad`]. + pub border_radius: [f32; 4], + + /// The border width of the [`Quad`]. + pub border_width: f32, +} + +/// A group of [`Quad`]s rendered together. +#[derive(Default, Debug)] +pub struct Batch { + /// The solid quads of the [`Layer`]. + solids: Vec<Solid>, + + /// The gradient quads of the [`Layer`]. + gradients: Vec<Gradient>, + + /// The quad order of the [`Layer`]; stored as a tuple of the quad type & its count. + order: Vec<(Kind, usize)>, +} + +impl Batch { + /// Returns true if there are no quads of any type in [`Quads`]. + pub fn is_empty(&self) -> bool { + self.solids.is_empty() && self.gradients.is_empty() + } + + /// Adds a [`Quad`] with the provided `Background` type to the quad [`Layer`]. + pub fn add(&mut self, quad: Quad, background: &Background) { + let kind = match background { + Background::Color(color) => { + self.solids.push(Solid { + color: color::pack(*color), + quad, + }); + + Kind::Solid + } + Background::Gradient(gradient) => { + self.gradients.push(Gradient { + gradient: graphics::gradient::pack( + gradient, + Rectangle::new(quad.position.into(), quad.size.into()), + ), + quad, + }); + + Kind::Gradient + } + }; + + match self.order.last_mut() { + Some((last_kind, count)) if kind == *last_kind => { + *count += 1; + } + _ => { + self.order.push((kind, 1)); + } + } } } +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +/// The kind of a quad. +enum Kind { + /// A solid quad + Solid, + /// A gradient quad + Gradient, +} + +fn color_target_state( + format: wgpu::TextureFormat, +) -> [Option<wgpu::ColorTargetState>; 1] { + [Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState { + color: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::SrcAlpha, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + alpha: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + }), + write_mask: wgpu::ColorWrites::ALL, + })] +} + #[repr(C)] -#[derive(Clone, Copy, Zeroable, Pod)] +#[derive(Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)] pub struct Vertex { _position: [f32; 2], } -const QUAD_INDICES: [u16; 6] = [0, 1, 2, 0, 2, 3]; +impl Vertex { + fn buffer_layout<'a>() -> wgpu::VertexBufferLayout<'a> { + wgpu::VertexBufferLayout { + array_stride: mem::size_of::<Self>() as u64, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &[wgpu::VertexAttribute { + shader_location: 0, + format: wgpu::VertexFormat::Float32x2, + offset: 0, + }], + } + } +} -const QUAD_VERTS: [Vertex; 4] = [ +const INDICES: [u16; 6] = [0, 1, 2, 0, 2, 3]; + +const VERTICES: [Vertex; 4] = [ Vertex { _position: [0.0, 0.0], }, @@ -298,10 +348,8 @@ const QUAD_VERTS: [Vertex; 4] = [ }, ]; -const INITIAL_INSTANCES: usize = 10_000; - #[repr(C)] -#[derive(Debug, Clone, Copy, Zeroable, Pod)] +#[derive(Debug, Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)] struct Uniforms { transform: [f32; 16], scale: f32, diff --git a/wgpu/src/quad/gradient.rs b/wgpu/src/quad/gradient.rs new file mode 100644 index 00000000..6db37252 --- /dev/null +++ b/wgpu/src/quad/gradient.rs @@ -0,0 +1,165 @@ +use crate::graphics::gradient; +use crate::quad::{self, Quad}; +use crate::Buffer; + +use bytemuck::{Pod, Zeroable}; +use std::ops::Range; + +/// A quad filled with interpolated colors. +#[derive(Clone, Copy, Debug)] +#[repr(C)] +pub struct Gradient { + /// The background gradient data of the quad. + pub gradient: gradient::Packed, + + /// The [`Quad`] data of the [`Gradient`]. + pub quad: Quad, +} + +#[allow(unsafe_code)] +unsafe impl Pod for Gradient {} + +#[allow(unsafe_code)] +unsafe impl Zeroable for Gradient {} + +#[derive(Debug)] +pub struct Layer { + instances: Buffer<Gradient>, + instance_count: usize, +} + +impl Layer { + pub fn new(device: &wgpu::Device) -> Self { + let instances = Buffer::new( + device, + "iced_wgpu.quad.gradient.buffer", + quad::INITIAL_INSTANCES, + wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + ); + + Self { + instances, + instance_count: 0, + } + } + + pub fn prepare( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + instances: &[Gradient], + ) { + let _ = self.instances.resize(device, instances.len()); + let _ = self.instances.write(queue, 0, instances); + + self.instance_count = instances.len(); + } +} + +#[derive(Debug)] +pub struct Pipeline { + pipeline: wgpu::RenderPipeline, +} + +impl Pipeline { + pub fn new( + device: &wgpu::Device, + format: wgpu::TextureFormat, + constants_layout: &wgpu::BindGroupLayout, + ) -> Self { + let layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("iced_wgpu.quad.gradient.pipeline"), + push_constant_ranges: &[], + bind_group_layouts: &[constants_layout], + }); + + let shader = + device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("iced_wgpu.quad.gradient.shader"), + source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( + include_str!("../shader/quad.wgsl"), + )), + }); + + let pipeline = + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("iced_wgpu.quad.gradient.pipeline"), + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "gradient_vs_main", + buffers: &[ + quad::Vertex::buffer_layout(), + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::<Gradient>() + as u64, + step_mode: wgpu::VertexStepMode::Instance, + attributes: &wgpu::vertex_attr_array!( + // Colors 1-2 + 1 => Uint32x4, + // Colors 3-4 + 2 => Uint32x4, + // Colors 5-6 + 3 => Uint32x4, + // Colors 7-8 + 4 => Uint32x4, + // Offsets 1-8 + 5 => Uint32x4, + // Direction + 6 => Float32x4, + // Position & Scale + 7 => Float32x4, + // Border color + 8 => Float32x4, + // Border radius + 9 => Float32x4, + // Border width + 10 => Float32 + ), + }, + ], + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "gradient_fs_main", + targets: &quad::color_target_state(format), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + front_face: wgpu::FrontFace::Cw, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview: None, + }); + + Self { pipeline } + } + + pub fn render<'a>( + &'a self, + render_pass: &mut wgpu::RenderPass<'a>, + constants: &'a wgpu::BindGroup, + layer: &'a Layer, + range: Range<usize>, + ) { + #[cfg(feature = "tracing")] + let _ = tracing::info_span!("Wgpu::Quad::Gradient", "DRAW").entered(); + + render_pass.set_pipeline(&self.pipeline); + render_pass.set_bind_group(0, constants, &[]); + render_pass.set_vertex_buffer(1, layer.instances.slice(..)); + + render_pass.draw_indexed( + 0..quad::INDICES.len() as u32, + 0, + range.start as u32..range.end as u32, + ); + } +} diff --git a/wgpu/src/quad/solid.rs b/wgpu/src/quad/solid.rs new file mode 100644 index 00000000..f8f1e3a5 --- /dev/null +++ b/wgpu/src/quad/solid.rs @@ -0,0 +1,150 @@ +use crate::graphics::color; +use crate::quad::{self, Quad}; +use crate::Buffer; + +use bytemuck::{Pod, Zeroable}; +use std::ops::Range; + +/// A quad filled with a solid color. +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +#[repr(C)] +pub struct Solid { + /// The background color data of the quad. + pub color: color::Packed, + + /// The [`Quad`] data of the [`Solid`]. + pub quad: Quad, +} + +#[derive(Debug)] +pub struct Layer { + instances: Buffer<Solid>, + instance_count: usize, +} + +impl Layer { + pub fn new(device: &wgpu::Device) -> Self { + let instances = Buffer::new( + device, + "iced_wgpu.quad.solid.buffer", + quad::INITIAL_INSTANCES, + wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + ); + + Self { + instances, + instance_count: 0, + } + } + + pub fn prepare( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + instances: &[Solid], + ) { + let _ = self.instances.resize(device, instances.len()); + let _ = self.instances.write(queue, 0, instances); + + self.instance_count = instances.len(); + } +} + +#[derive(Debug)] +pub struct Pipeline { + pipeline: wgpu::RenderPipeline, +} + +impl Pipeline { + pub fn new( + device: &wgpu::Device, + format: wgpu::TextureFormat, + constants_layout: &wgpu::BindGroupLayout, + ) -> Self { + let layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("iced_wgpu.quad.solid.pipeline"), + push_constant_ranges: &[], + bind_group_layouts: &[constants_layout], + }); + + let shader = + device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("iced_wgpu.quad.solid.shader"), + source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( + include_str!("../shader/quad.wgsl"), + )), + }); + + let pipeline = + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("iced_wgpu.quad.solid.pipeline"), + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "solid_vs_main", + buffers: &[ + quad::Vertex::buffer_layout(), + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::<Solid>() as u64, + step_mode: wgpu::VertexStepMode::Instance, + attributes: &wgpu::vertex_attr_array!( + // Color + 1 => Float32x4, + // Position + 2 => Float32x2, + // Size + 3 => Float32x2, + // Border color + 4 => Float32x4, + // Border radius + 5 => Float32x4, + // Border width + 6 => Float32, + ), + }, + ], + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "solid_fs_main", + targets: &quad::color_target_state(format), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + front_face: wgpu::FrontFace::Cw, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview: None, + }); + + Self { pipeline } + } + + pub fn render<'a>( + &'a self, + render_pass: &mut wgpu::RenderPass<'a>, + constants: &'a wgpu::BindGroup, + layer: &'a Layer, + range: Range<usize>, + ) { + #[cfg(feature = "tracing")] + let _ = tracing::info_span!("Wgpu::Quad::Solid", "DRAW").entered(); + + render_pass.set_pipeline(&self.pipeline); + render_pass.set_bind_group(0, constants, &[]); + render_pass.set_vertex_buffer(1, layer.instances.slice(..)); + + render_pass.draw_indexed( + 0..quad::INDICES.len() as u32, + 0, + range.start as u32..range.end as u32, + ); + } +} diff --git a/wgpu/src/shader/gradient.wgsl b/wgpu/src/shader/gradient.wgsl deleted file mode 100644 index 63825aec..00000000 --- a/wgpu/src/shader/gradient.wgsl +++ /dev/null @@ -1,88 +0,0 @@ -struct Uniforms { - transform: mat4x4<f32>, - //xy = start, wz = end - position: vec4<f32>, - //x = start stop, y = end stop, zw = padding - stop_range: vec4<i32>, -} - -struct Stop { - color: vec4<f32>, - offset: f32, -}; - -@group(0) @binding(0) -var<uniform> uniforms: Uniforms; - -@group(0) @binding(1) -var<storage, read> color_stops: array<Stop>; - -struct VertexOutput { - @builtin(position) position: vec4<f32>, - @location(0) raw_position: vec2<f32> -} - -@vertex -fn vs_main(@location(0) input: vec2<f32>) -> VertexOutput { - var output: VertexOutput; - output.position = uniforms.transform * vec4<f32>(input.xy, 0.0, 1.0); - output.raw_position = input; - - return output; -} - -//TODO: rewrite without branching -@fragment -fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> { - let start = uniforms.position.xy; - let end = uniforms.position.zw; - let start_stop = uniforms.stop_range.x; - let end_stop = uniforms.stop_range.y; - - let v1 = end - start; - let v2 = input.raw_position.xy - start; - let unit = normalize(v1); - let offset = dot(unit, v2) / length(v1); - - let min_stop = color_stops[start_stop]; - let max_stop = color_stops[end_stop]; - - var color: vec4<f32>; - - if (offset <= min_stop.offset) { - color = min_stop.color; - } else if (offset >= max_stop.offset) { - color = max_stop.color; - } else { - var min = min_stop; - var max = max_stop; - var min_index = start_stop; - var max_index = end_stop; - - loop { - if (min_index >= max_index - 1) { - break; - } - - let index = min_index + (max_index - min_index) / 2; - - let stop = color_stops[index]; - - if (offset <= stop.offset) { - max = stop; - max_index = index; - } else { - min = stop; - min_index = index; - } - } - - color = mix(min.color, max.color, smoothstep( - min.offset, - max.offset, - offset - )); - } - - return color; -} diff --git a/wgpu/src/shader/quad.wgsl b/wgpu/src/shader/quad.wgsl index cf4f7e4d..fb402158 100644 --- a/wgpu/src/shader/quad.wgsl +++ b/wgpu/src/shader/quad.wgsl @@ -5,17 +5,57 @@ struct Globals { @group(0) @binding(0) var<uniform> globals: Globals; -struct VertexInput { +fn distance_alg( + frag_coord: vec2<f32>, + position: vec2<f32>, + size: vec2<f32>, + radius: f32 +) -> f32 { + var inner_size: vec2<f32> = size - vec2<f32>(radius, radius) * 2.0; + var top_left: vec2<f32> = position + vec2<f32>(radius, radius); + var bottom_right: vec2<f32> = top_left + inner_size; + + var top_left_distance: vec2<f32> = top_left - frag_coord; + var bottom_right_distance: vec2<f32> = frag_coord - bottom_right; + + var dist: vec2<f32> = vec2<f32>( + max(max(top_left_distance.x, bottom_right_distance.x), 0.0), + max(max(top_left_distance.y, bottom_right_distance.y), 0.0) + ); + + return sqrt(dist.x * dist.x + dist.y * dist.y); +} + +// Based on the fragement position and the center of the quad, select one of the 4 radi. +// Order matches CSS border radius attribute: +// radi.x = top-left, radi.y = top-right, radi.z = bottom-right, radi.w = bottom-left +fn select_border_radius(radi: vec4<f32>, position: vec2<f32>, center: vec2<f32>) -> f32 { + var rx = radi.x; + var ry = radi.y; + rx = select(radi.x, radi.y, position.x > center.x); + ry = select(radi.w, radi.z, position.x > center.x); + rx = select(rx, ry, position.y > center.y); + return rx; +} + +fn unpack_u32(color: vec2<u32>) -> vec4<f32> { + let rg: vec2<f32> = unpack2x16float(color.x); + let ba: vec2<f32> = unpack2x16float(color.y); + + return vec4<f32>(rg.y, rg.x, ba.y, ba.x); +} + +struct SolidVertexInput { @location(0) v_pos: vec2<f32>, - @location(1) pos: vec2<f32>, - @location(2) scale: vec2<f32>, - @location(3) color: vec4<f32>, + @location(1) color: vec4<f32>, + @location(2) pos: vec2<f32>, + @location(3) scale: vec2<f32>, @location(4) border_color: vec4<f32>, @location(5) border_radius: vec4<f32>, @location(6) border_width: f32, } -struct VertexOutput { +struct SolidVertexOutput { @builtin(position) position: vec4<f32>, @location(0) color: vec4<f32>, @location(1) border_color: vec4<f32>, @@ -26,8 +66,8 @@ struct VertexOutput { } @vertex -fn vs_main(input: VertexInput) -> VertexOutput { - var out: VertexOutput; +fn solid_vs_main(input: SolidVertexInput) -> SolidVertexOutput { + var out: SolidVertexOutput; var pos: vec2<f32> = input.pos * globals.scale; var scale: vec2<f32> = input.scale * globals.scale; @@ -47,54 +87,20 @@ fn vs_main(input: VertexInput) -> VertexOutput { vec4<f32>(pos - vec2<f32>(0.5, 0.5), 0.0, 1.0) ); + out.position = globals.transform * transform * vec4<f32>(input.v_pos, 0.0, 1.0); out.color = input.color; out.border_color = input.border_color; out.pos = pos; out.scale = scale; out.border_radius = border_radius * globals.scale; out.border_width = input.border_width * globals.scale; - out.position = globals.transform * transform * vec4<f32>(input.v_pos, 0.0, 1.0); return out; } -fn distance_alg( - frag_coord: vec2<f32>, - position: vec2<f32>, - size: vec2<f32>, - radius: f32 -) -> f32 { - var inner_size: vec2<f32> = size - vec2<f32>(radius, radius) * 2.0; - var top_left: vec2<f32> = position + vec2<f32>(radius, radius); - var bottom_right: vec2<f32> = top_left + inner_size; - - var top_left_distance: vec2<f32> = top_left - frag_coord; - var bottom_right_distance: vec2<f32> = frag_coord - bottom_right; - - var dist: vec2<f32> = vec2<f32>( - max(max(top_left_distance.x, bottom_right_distance.x), 0.0), - max(max(top_left_distance.y, bottom_right_distance.y), 0.0) - ); - - return sqrt(dist.x * dist.x + dist.y * dist.y); -} - -// Based on the fragement position and the center of the quad, select one of the 4 radi. -// Order matches CSS border radius attribute: -// radi.x = top-left, radi.y = top-right, radi.z = bottom-right, radi.w = bottom-left -fn select_border_radius(radi: vec4<f32>, position: vec2<f32>, center: vec2<f32>) -> f32 { - var rx = radi.x; - var ry = radi.y; - rx = select(radi.x, radi.y, position.x > center.x); - ry = select(radi.w, radi.z, position.x > center.x); - rx = select(rx, ry, position.y > center.y); - return rx; -} - - @fragment -fn fs_main( - input: VertexOutput +fn solid_fs_main( + input: SolidVertexOutput ) -> @location(0) vec4<f32> { var mixed_color: vec4<f32> = input.color; @@ -138,3 +144,202 @@ fn fs_main( return vec4<f32>(mixed_color.x, mixed_color.y, mixed_color.z, mixed_color.w * radius_alpha); } + +struct GradientVertexInput { + @location(0) v_pos: vec2<f32>, + @location(1) colors_1: vec4<u32>, + @location(2) colors_2: vec4<u32>, + @location(3) colors_3: vec4<u32>, + @location(4) colors_4: vec4<u32>, + @location(5) offsets: vec4<u32>, + @location(6) direction: vec4<f32>, + @location(7) position_and_scale: vec4<f32>, + @location(8) border_color: vec4<f32>, + @location(9) border_radius: vec4<f32>, + @location(10) border_width: f32, +} + +struct GradientVertexOutput { + @builtin(position) position: vec4<f32>, + @location(1) colors_1: vec4<u32>, + @location(2) colors_2: vec4<u32>, + @location(3) colors_3: vec4<u32>, + @location(4) colors_4: vec4<u32>, + @location(5) offsets: vec4<u32>, + @location(6) direction: vec4<f32>, + @location(7) position_and_scale: vec4<f32>, + @location(8) border_color: vec4<f32>, + @location(9) border_radius: vec4<f32>, + @location(10) border_width: f32, +} + +@vertex +fn gradient_vs_main(input: GradientVertexInput) -> GradientVertexOutput { + var out: GradientVertexOutput; + + var pos: vec2<f32> = input.position_and_scale.xy * globals.scale; + var scale: vec2<f32> = input.position_and_scale.zw * globals.scale; + + var min_border_radius = min(input.position_and_scale.z, input.position_and_scale.w) * 0.5; + var border_radius: vec4<f32> = vec4<f32>( + min(input.border_radius.x, min_border_radius), + min(input.border_radius.y, min_border_radius), + min(input.border_radius.z, min_border_radius), + min(input.border_radius.w, min_border_radius) + ); + + var transform: mat4x4<f32> = mat4x4<f32>( + vec4<f32>(scale.x + 1.0, 0.0, 0.0, 0.0), + vec4<f32>(0.0, scale.y + 1.0, 0.0, 0.0), + vec4<f32>(0.0, 0.0, 1.0, 0.0), + vec4<f32>(pos - vec2<f32>(0.5, 0.5), 0.0, 1.0) + ); + + out.position = globals.transform * transform * vec4<f32>(input.v_pos, 0.0, 1.0); + out.colors_1 = input.colors_1; + out.colors_2 = input.colors_2; + out.colors_3 = input.colors_3; + out.colors_4 = input.colors_4; + out.offsets = input.offsets; + out.direction = input.direction * globals.scale; + out.position_and_scale = vec4<f32>(pos, scale); + out.border_color = input.border_color; + out.border_radius = border_radius * globals.scale; + out.border_width = input.border_width * globals.scale; + + return out; +} + +fn random(coords: vec2<f32>) -> f32 { + return fract(sin(dot(coords, vec2(12.9898,78.233))) * 43758.5453); +} + +/// Returns the current interpolated color with a max 8-stop gradient +fn gradient( + raw_position: vec2<f32>, + direction: vec4<f32>, + colors: array<vec4<f32>, 8>, + offsets: array<f32, 8>, + last_index: i32 +) -> vec4<f32> { + let start = direction.xy; + let end = direction.zw; + + let v1 = end - start; + let v2 = raw_position - start; + let unit = normalize(v1); + let coord_offset = dot(unit, v2) / length(v1); + + //need to store these as a var to use dynamic indexing in a loop + //this is already added to wgsl spec but not in wgpu yet + var colors_arr = colors; + var offsets_arr = offsets; + + var color: vec4<f32>; + + let noise_granularity: f32 = 0.3/255.0; + + for (var i: i32 = 0; i < last_index; i++) { + let curr_offset = offsets_arr[i]; + let next_offset = offsets_arr[i+1]; + + if (coord_offset <= offsets_arr[0]) { + color = colors_arr[0]; + } + + if (curr_offset <= coord_offset && coord_offset <= next_offset) { + color = mix(colors_arr[i], colors_arr[i+1], smoothstep( + curr_offset, + next_offset, + coord_offset, + )); + } + + if (coord_offset >= offsets_arr[last_index]) { + color = colors_arr[last_index]; + } + } + + return color + mix(-noise_granularity, noise_granularity, random(raw_position)); +} + +@fragment +fn gradient_fs_main(input: GradientVertexOutput) -> @location(0) vec4<f32> { + let colors = array<vec4<f32>, 8>( + unpack_u32(input.colors_1.xy), + unpack_u32(input.colors_1.zw), + unpack_u32(input.colors_2.xy), + unpack_u32(input.colors_2.zw), + unpack_u32(input.colors_3.xy), + unpack_u32(input.colors_3.zw), + unpack_u32(input.colors_4.xy), + unpack_u32(input.colors_4.zw), + ); + + let offsets_1: vec4<f32> = unpack_u32(input.offsets.xy); + let offsets_2: vec4<f32> = unpack_u32(input.offsets.zw); + + var offsets = array<f32, 8>( + offsets_1.x, + offsets_1.y, + offsets_1.z, + offsets_1.w, + offsets_2.x, + offsets_2.y, + offsets_2.z, + offsets_2.w, + ); + + //TODO could just pass this in to the shader but is probably more performant to just check it here + var last_index = 7; + for (var i: i32 = 0; i <= 7; i++) { + if (offsets[i] > 1.0) { + last_index = i - 1; + break; + } + } + + var mixed_color: vec4<f32> = gradient(input.position.xy, input.direction, colors, offsets, last_index); + + let pos = input.position_and_scale.xy; + let scale = input.position_and_scale.zw; + + var border_radius = select_border_radius( + input.border_radius, + input.position.xy, + (pos + scale * 0.5).xy + ); + + if (input.border_width > 0.0) { + var internal_border: f32 = max(border_radius - input.border_width, 0.0); + + var internal_distance: f32 = distance_alg( + input.position.xy, + pos + vec2<f32>(input.border_width, input.border_width), + scale - vec2<f32>(input.border_width * 2.0, input.border_width * 2.0), + internal_border + ); + + var border_mix: f32 = smoothstep( + max(internal_border - 0.5, 0.0), + internal_border + 0.5, + internal_distance + ); + + mixed_color = mix(mixed_color, input.border_color, vec4<f32>(border_mix, border_mix, border_mix, border_mix)); + } + + var dist: f32 = distance_alg( + input.position.xy, + pos, + scale, + border_radius + ); + + var radius_alpha: f32 = 1.0 - smoothstep( + max(border_radius - 0.5, 0.0), + border_radius + 0.5, + dist); + + return vec4<f32>(mixed_color.x, mixed_color.y, mixed_color.z, mixed_color.w * radius_alpha); +} diff --git a/wgpu/src/shader/solid.wgsl b/wgpu/src/shader/solid.wgsl deleted file mode 100644 index b24402f8..00000000 --- a/wgpu/src/shader/solid.wgsl +++ /dev/null @@ -1,30 +0,0 @@ -struct Globals { - transform: mat4x4<f32>, -} - -@group(0) @binding(0) var<uniform> globals: Globals; - -struct VertexInput { - @location(0) position: vec2<f32>, - @location(1) color: vec4<f32>, -} - -struct VertexOutput { - @builtin(position) position: vec4<f32>, - @location(0) color: vec4<f32>, -} - -@vertex -fn vs_main(input: VertexInput) -> VertexOutput { - var out: VertexOutput; - - out.color = input.color; - out.position = globals.transform * vec4<f32>(input.position, 0.0, 1.0); - - return out; -} - -@fragment -fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> { - return input.color; -} diff --git a/wgpu/src/shader/triangle.wgsl b/wgpu/src/shader/triangle.wgsl new file mode 100644 index 00000000..9f512d14 --- /dev/null +++ b/wgpu/src/shader/triangle.wgsl @@ -0,0 +1,165 @@ +struct Globals { + transform: mat4x4<f32>, +} + +@group(0) @binding(0) var<uniform> globals: Globals; + +fn unpack_u32(color: vec2<u32>) -> vec4<f32> { + let rg: vec2<f32> = unpack2x16float(color.x); + let ba: vec2<f32> = unpack2x16float(color.y); + + return vec4<f32>(rg.y, rg.x, ba.y, ba.x); +} + +struct SolidVertexInput { + @location(0) position: vec2<f32>, + @location(1) color: vec4<f32>, +} + +struct SolidVertexOutput { + @builtin(position) position: vec4<f32>, + @location(0) color: vec4<f32>, +} + +@vertex +fn solid_vs_main(input: SolidVertexInput) -> SolidVertexOutput { + var out: SolidVertexOutput; + + out.color = input.color; + out.position = globals.transform * vec4<f32>(input.position, 0.0, 1.0); + + return out; +} + +@fragment +fn solid_fs_main(input: SolidVertexOutput) -> @location(0) vec4<f32> { + return input.color; +} + +struct GradientVertexInput { + @location(0) v_pos: vec2<f32>, + @location(1) colors_1: vec4<u32>, + @location(2) colors_2: vec4<u32>, + @location(3) colors_3: vec4<u32>, + @location(4) colors_4: vec4<u32>, + @location(5) offsets: vec4<u32>, + @location(6) direction: vec4<f32>, +} + +struct GradientVertexOutput { + @builtin(position) position: vec4<f32>, + @location(0) raw_position: vec2<f32>, + @location(1) colors_1: vec4<u32>, + @location(2) colors_2: vec4<u32>, + @location(3) colors_3: vec4<u32>, + @location(4) colors_4: vec4<u32>, + @location(5) offsets: vec4<u32>, + @location(6) direction: vec4<f32>, +} + +@vertex +fn gradient_vs_main(input: GradientVertexInput) -> GradientVertexOutput { + var output: GradientVertexOutput; + + output.position = globals.transform * vec4<f32>(input.v_pos, 0.0, 1.0); + output.raw_position = input.v_pos; + output.colors_1 = input.colors_1; + output.colors_2 = input.colors_2; + output.colors_3 = input.colors_3; + output.colors_4 = input.colors_4; + output.offsets = input.offsets; + output.direction = input.direction; + + return output; +} + +fn random(coords: vec2<f32>) -> f32 { + return fract(sin(dot(coords, vec2(12.9898,78.233))) * 43758.5453); +} + +/// Returns the current interpolated color with a max 8-stop gradient +fn gradient( + raw_position: vec2<f32>, + direction: vec4<f32>, + colors: array<vec4<f32>, 8>, + offsets: array<f32, 8>, + last_index: i32 +) -> vec4<f32> { + let start = direction.xy; + let end = direction.zw; + + let v1 = end - start; + let v2 = raw_position - start; + let unit = normalize(v1); + let coord_offset = dot(unit, v2) / length(v1); + + //need to store these as a var to use dynamic indexing in a loop + //this is already added to wgsl spec but not in wgpu yet + var colors_arr = colors; + var offsets_arr = offsets; + + var color: vec4<f32>; + + let noise_granularity: f32 = 0.3/255.0; + + for (var i: i32 = 0; i < last_index; i++) { + let curr_offset = offsets_arr[i]; + let next_offset = offsets_arr[i+1]; + + if (coord_offset <= offsets_arr[0]) { + color = colors_arr[0]; + } + + if (curr_offset <= coord_offset && coord_offset <= next_offset) { + color = mix(colors_arr[i], colors_arr[i+1], smoothstep( + curr_offset, + next_offset, + coord_offset, + )); + } + + if (coord_offset >= offsets_arr[last_index]) { + color = colors_arr[last_index]; + } + } + + return color + mix(-noise_granularity, noise_granularity, random(raw_position)); +} + +@fragment +fn gradient_fs_main(input: GradientVertexOutput) -> @location(0) vec4<f32> { + let colors = array<vec4<f32>, 8>( + unpack_u32(input.colors_1.xy), + unpack_u32(input.colors_1.zw), + unpack_u32(input.colors_2.xy), + unpack_u32(input.colors_2.zw), + unpack_u32(input.colors_3.xy), + unpack_u32(input.colors_3.zw), + unpack_u32(input.colors_4.xy), + unpack_u32(input.colors_4.zw), + ); + + let offsets_1: vec4<f32> = unpack_u32(input.offsets.xy); + let offsets_2: vec4<f32> = unpack_u32(input.offsets.zw); + + var offsets = array<f32, 8>( + offsets_1.x, + offsets_1.y, + offsets_1.z, + offsets_1.w, + offsets_2.x, + offsets_2.y, + offsets_2.z, + offsets_2.w, + ); + + var last_index = 7; + for (var i: i32 = 0; i <= 7; i++) { + if (offsets[i] >= 1.0) { + last_index = i; + break; + } + } + + return gradient(input.raw_position, input.direction, colors, offsets, last_index); +} diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 714e0400..c9188bd1 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -2,6 +2,7 @@ use crate::core::alignment; use crate::core::font::{self, Font}; use crate::core::text::{Hit, LineHeight, Shaping}; use crate::core::{Pixels, Point, Rectangle, Size}; +use crate::graphics::color; use crate::layer::Text; use rustc_hash::{FxHashMap, FxHashSet}; @@ -17,8 +18,7 @@ pub struct Pipeline { renderers: Vec<glyphon::TextRenderer>, atlas: glyphon::TextAtlas, prepare_layer: usize, - measurement_cache: RefCell<Cache>, - render_cache: Cache, + cache: RefCell<Cache>, } impl Pipeline { @@ -35,17 +35,27 @@ impl Pipeline { .into_iter(), )), renderers: Vec::new(), - atlas: glyphon::TextAtlas::new(device, queue, format), + atlas: glyphon::TextAtlas::new( + device, + queue, + format, + if color::GAMMA_CORRECTION { + glyphon::ColorMode::Accurate + } else { + glyphon::ColorMode::Web + }, + ), prepare_layer: 0, - measurement_cache: RefCell::new(Cache::new()), - render_cache: Cache::new(), + cache: RefCell::new(Cache::new()), } } pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) { - self.font_system.get_mut().db_mut().load_font_source( + let _ = self.font_system.get_mut().db_mut().load_font_source( glyphon::fontdb::Source::Binary(Arc::new(bytes.into_owned())), ); + + self.cache = RefCell::new(Cache::new()); } pub fn prepare( @@ -68,25 +78,25 @@ impl Pipeline { let font_system = self.font_system.get_mut(); let renderer = &mut self.renderers[self.prepare_layer]; + let cache = self.cache.get_mut(); let keys: Vec<_> = sections .iter() .map(|section| { - let (key, _) = self.render_cache.allocate( + let (key, _) = cache.allocate( font_system, Key { content: section.content, - size: section.size * scale_factor, + size: section.size, line_height: f32::from( section .line_height .to_absolute(Pixels(section.size)), - ) * scale_factor, + ), font: section.font, bounds: Size { - width: (section.bounds.width * scale_factor).ceil(), - height: (section.bounds.height * scale_factor) - .ceil(), + width: section.bounds.width, + height: section.bounds.height, }, shaping: section.shaping, }, @@ -103,22 +113,16 @@ impl Pipeline { .iter() .zip(keys.iter()) .filter_map(|(section, key)| { - let buffer = - self.render_cache.get(key).expect("Get cached buffer"); - - 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 * buffer.metrics().line_height; + let buffer = cache.get(key).expect("Get cached buffer"); let x = section.bounds.x * scale_factor; let y = section.bounds.y * scale_factor; + let (max_width, total_height) = measure(buffer); + + let max_width = max_width * scale_factor; + let total_height = total_height * scale_factor; + let left = match section.horizontal_alignment { alignment::Horizontal::Left => x, alignment::Horizontal::Center => x - max_width / 2.0, @@ -140,14 +144,11 @@ impl Pipeline { let clip_bounds = bounds.intersection(§ion_bounds)?; - // TODO: Subpixel glyph positioning - let left = left.round() as i32; - let top = top.round() as i32; - Some(glyphon::TextArea { buffer, left, top, + scale: scale_factor, bounds: glyphon::TextBounds { left: clip_bounds.x as i32, top: clip_bounds.y as i32, @@ -155,7 +156,8 @@ impl Pipeline { bottom: (clip_bounds.y + clip_bounds.height) as i32, }, default_color: { - let [r, g, b, a] = section.color.into_linear(); + let [r, g, b, a] = + color::pack(section.color).components(); glyphon::Color::rgba( (r * 255.0) as u8, @@ -224,7 +226,7 @@ impl Pipeline { pub fn end_frame(&mut self) { self.atlas.trim(); - self.render_cache.trim(); + self.cache.get_mut().trim(); self.prepare_layer = 0; } @@ -238,7 +240,7 @@ impl Pipeline { bounds: Size, shaping: Shaping, ) -> (f32, f32) { - let mut measurement_cache = self.measurement_cache.borrow_mut(); + let mut measurement_cache = self.cache.borrow_mut(); let line_height = f32::from(line_height.to_absolute(Pixels(size))); @@ -254,14 +256,7 @@ impl Pipeline { }, ); - 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, line_height * total_lines as f32) + measure(paragraph) } pub fn hit_test( @@ -275,7 +270,7 @@ impl Pipeline { point: Point, _nearest_only: bool, ) -> Option<Hit> { - let mut measurement_cache = self.measurement_cache.borrow_mut(); + let mut measurement_cache = self.cache.borrow_mut(); let line_height = f32::from(line_height.to_absolute(Pixels(size))); @@ -295,10 +290,16 @@ impl Pipeline { Some(Hit::CharOffset(cursor.index)) } +} - pub fn trim_measurement_cache(&mut self) { - self.measurement_cache.borrow_mut().trim(); - } +fn measure(buffer: &glyphon::Buffer) -> (f32, f32) { + let (width, total_lines) = buffer + .layout_runs() + .fold((0.0, 0usize), |(width, total_lines), run| { + (run.line_w.max(width), total_lines + 1) + }); + + (width, total_lines as f32 * buffer.metrics().line_height) } fn to_family(family: font::Family) -> glyphon::Family<'static> { diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index eb15a458..3f3635cf 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -1,26 +1,19 @@ //! Draw meshes of triangles. mod msaa; -use crate::buffer::r#static::Buffer; use crate::core::Size; use crate::graphics::{Antialiasing, Transformation}; use crate::layer::mesh::{self, Mesh}; +use crate::Buffer; -#[cfg(not(target_arch = "wasm32"))] -use crate::core::Gradient; - -#[cfg(feature = "tracing")] -use tracing::info_span; +const INITIAL_INDEX_COUNT: usize = 1_000; +const INITIAL_VERTEX_COUNT: usize = 1_000; #[derive(Debug)] pub struct Pipeline { blit: Option<msaa::Blit>, solid: solid::Pipeline, - - /// Gradients are currently not supported on WASM targets due to their need of storage buffers. - #[cfg(not(target_arch = "wasm32"))] gradient: gradient::Pipeline, - layers: Vec<Layer>, prepare_layer: usize, } @@ -30,8 +23,6 @@ struct Layer { index_buffer: Buffer<u32>, index_strides: Vec<u32>, solid: solid::Layer, - - #[cfg(not(target_arch = "wasm32"))] gradient: gradient::Layer, } @@ -39,18 +30,17 @@ impl Layer { fn new( device: &wgpu::Device, solid: &solid::Pipeline, - #[cfg(not(target_arch = "wasm32"))] gradient: &gradient::Pipeline, + gradient: &gradient::Pipeline, ) -> Self { Self { index_buffer: Buffer::new( device, - "iced_wgpu::triangle index buffer", + "iced_wgpu.triangle.index_buffer", + INITIAL_INDEX_COUNT, wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST, ), index_strides: Vec::new(), solid: solid::Layer::new(device, &solid.constants_layout), - - #[cfg(not(target_arch = "wasm32"))] gradient: gradient::Layer::new(device, &gradient.constants_layout), } } @@ -60,7 +50,7 @@ impl Layer { device: &wgpu::Device, queue: &wgpu::Queue, solid: &solid::Pipeline, - #[cfg(not(target_arch = "wasm32"))] gradient: &gradient::Pipeline, + gradient: &gradient::Pipeline, meshes: &[Mesh<'_>], transformation: Transformation, ) { @@ -73,183 +63,102 @@ impl Layer { // the majority of use cases. Therefore we will write GPU data every frame (for now). let _ = self.index_buffer.resize(device, count.indices); let _ = self.solid.vertices.resize(device, count.solid_vertices); - - #[cfg(not(target_arch = "wasm32"))] let _ = self .gradient .vertices .resize(device, count.gradient_vertices); - // Prepare dynamic buffers & data store for writing - self.index_buffer.clear(); + if self.solid.uniforms.resize(device, count.solids) { + self.solid.constants = solid::Layer::bind_group( + device, + &self.solid.uniforms.raw, + &solid.constants_layout, + ); + } + + if self.gradient.uniforms.resize(device, count.gradients) { + self.gradient.constants = gradient::Layer::bind_group( + device, + &self.gradient.uniforms.raw, + &gradient.constants_layout, + ); + } + self.index_strides.clear(); + self.index_buffer.clear(); self.solid.vertices.clear(); self.solid.uniforms.clear(); - - #[cfg(not(target_arch = "wasm32"))] - { - self.gradient.uniforms.clear(); - self.gradient.vertices.clear(); - self.gradient.storage.clear(); - } + self.gradient.vertices.clear(); + self.gradient.uniforms.clear(); let mut solid_vertex_offset = 0; - let mut index_offset = 0; - - #[cfg(not(target_arch = "wasm32"))] + let mut solid_uniform_offset = 0; let mut gradient_vertex_offset = 0; + let mut gradient_uniform_offset = 0; + let mut index_offset = 0; for mesh in meshes { let origin = mesh.origin(); let indices = mesh.indices(); - let transform = - transformation * Transformation::translate(origin.x, origin.y); + let uniforms = Uniforms::new( + transformation * Transformation::translate(origin.x, origin.y), + ); - let new_index_offset = + index_offset += self.index_buffer.write(queue, index_offset, indices); - - index_offset += new_index_offset; self.index_strides.push(indices.len() as u32); - //push uniform data to CPU buffers match mesh { Mesh::Solid { buffers, .. } => { - self.solid.uniforms.push(&solid::Uniforms::new(transform)); - - let written_bytes = self.solid.vertices.write( + solid_vertex_offset += self.solid.vertices.write( queue, solid_vertex_offset, &buffers.vertices, ); - solid_vertex_offset += written_bytes; + solid_uniform_offset += self.solid.uniforms.write( + queue, + solid_uniform_offset, + &[uniforms], + ); } - #[cfg(not(target_arch = "wasm32"))] - Mesh::Gradient { - buffers, gradient, .. - } => { - let written_bytes = self.gradient.vertices.write( + Mesh::Gradient { buffers, .. } => { + gradient_vertex_offset += self.gradient.vertices.write( queue, gradient_vertex_offset, &buffers.vertices, ); - gradient_vertex_offset += written_bytes; - - match gradient { - Gradient::Linear(linear) => { - use glam::{IVec4, Vec4}; - - let start_offset = self.gradient.color_stop_offset; - let end_offset = (linear.color_stops.len() as i32) - + start_offset - - 1; - - self.gradient.uniforms.push(&gradient::Uniforms { - transform: transform.into(), - direction: Vec4::new( - linear.start.x, - linear.start.y, - linear.end.x, - linear.end.y, - ), - stop_range: IVec4::new( - start_offset, - end_offset, - 0, - 0, - ), - }); - - self.gradient.color_stop_offset = end_offset + 1; - - let stops: Vec<gradient::ColorStop> = linear - .color_stops - .iter() - .map(|stop| { - let [r, g, b, a] = stop.color.into_linear(); - - gradient::ColorStop { - offset: stop.offset, - color: Vec4::new(r, g, b, a), - } - }) - .collect(); - - self.gradient - .color_stops_pending_write - .color_stops - .extend(stops); - } - } + gradient_uniform_offset += self.gradient.uniforms.write( + queue, + gradient_uniform_offset, + &[uniforms], + ); } - #[cfg(target_arch = "wasm32")] - Mesh::Gradient { .. } => {} - } - } - - // Write uniform data to GPU - if count.solid_vertices > 0 { - let uniforms_resized = self.solid.uniforms.resize(device); - - if uniforms_resized { - self.solid.constants = solid::Layer::bind_group( - device, - self.solid.uniforms.raw(), - &solid.constants_layout, - ) } - - self.solid.uniforms.write(queue); - } - - #[cfg(not(target_arch = "wasm32"))] - if count.gradient_vertices > 0 { - // First write the pending color stops to the CPU buffer - self.gradient - .storage - .push(&self.gradient.color_stops_pending_write); - - // Resize buffers if needed - let uniforms_resized = self.gradient.uniforms.resize(device); - let storage_resized = self.gradient.storage.resize(device); - - if uniforms_resized || storage_resized { - self.gradient.constants = gradient::Layer::bind_group( - device, - self.gradient.uniforms.raw(), - self.gradient.storage.raw(), - &gradient.constants_layout, - ); - } - - // Write to GPU - self.gradient.uniforms.write(queue); - self.gradient.storage.write(queue); - - // Cleanup - self.gradient.color_stop_offset = 0; - self.gradient.color_stops_pending_write.color_stops.clear(); } } fn render<'a>( &'a self, solid: &'a solid::Pipeline, - #[cfg(not(target_arch = "wasm32"))] gradient: &'a gradient::Pipeline, + gradient: &'a gradient::Pipeline, meshes: &[Mesh<'_>], scale_factor: f32, render_pass: &mut wgpu::RenderPass<'a>, ) { let mut num_solids = 0; - #[cfg(not(target_arch = "wasm32"))] let mut num_gradients = 0; let mut last_is_solid = None; for (index, mesh) in meshes.iter().enumerate() { let clip_bounds = (mesh.clip_bounds() * scale_factor).snap(); + if clip_bounds.width < 1 || clip_bounds.height < 1 { + continue; + } + render_pass.set_scissor_rect( clip_bounds.x, clip_bounds.y, @@ -268,7 +177,8 @@ impl Layer { render_pass.set_bind_group( 0, &self.solid.constants, - &[self.solid.uniforms.offset_at_index(num_solids)], + &[(num_solids * std::mem::size_of::<Uniforms>()) + as u32], ); render_pass.set_vertex_buffer( @@ -278,7 +188,6 @@ impl Layer { num_solids += 1; } - #[cfg(not(target_arch = "wasm32"))] Mesh::Gradient { .. } => { if last_is_solid.unwrap_or(true) { render_pass.set_pipeline(&gradient.pipeline); @@ -289,10 +198,8 @@ impl Layer { render_pass.set_bind_group( 0, &self.gradient.constants, - &[self - .gradient - .uniforms - .offset_at_index(num_gradients)], + &[(num_gradients * std::mem::size_of::<Uniforms>()) + as u32], ); render_pass.set_vertex_buffer( @@ -302,8 +209,6 @@ impl Layer { num_gradients += 1; } - #[cfg(target_arch = "wasm32")] - Mesh::Gradient { .. } => {} }; render_pass.set_index_buffer( @@ -325,10 +230,7 @@ impl Pipeline { Pipeline { blit: antialiasing.map(|a| msaa::Blit::new(device, format, a)), solid: solid::Pipeline::new(device, format, antialiasing), - - #[cfg(not(target_arch = "wasm32"))] gradient: gradient::Pipeline::new(device, format, antialiasing), - layers: Vec::new(), prepare_layer: 0, } @@ -342,15 +244,11 @@ impl Pipeline { transformation: Transformation, ) { #[cfg(feature = "tracing")] - let _ = info_span!("Wgpu::Triangle", "PREPARE").entered(); + let _ = tracing::info_span!("Wgpu::Triangle", "PREPARE").entered(); if self.layers.len() <= self.prepare_layer { - self.layers.push(Layer::new( - device, - &self.solid, - #[cfg(not(target_arch = "wasm32"))] - &self.gradient, - )); + self.layers + .push(Layer::new(device, &self.solid, &self.gradient)); } let layer = &mut self.layers[self.prepare_layer]; @@ -358,7 +256,6 @@ impl Pipeline { device, queue, &self.solid, - #[cfg(not(target_arch = "wasm32"))] &self.gradient, meshes, transformation, @@ -378,9 +275,8 @@ impl Pipeline { scale_factor: f32, ) { #[cfg(feature = "tracing")] - let _ = info_span!("Wgpu::Triangle", "DRAW").entered(); + let _ = tracing::info_span!("Wgpu::Triangle", "DRAW").entered(); - // Configure render pass { let (attachment, resolve_target, load) = if let Some(blit) = &mut self.blit @@ -397,12 +293,9 @@ impl Pipeline { (target, None, wgpu::LoadOp::Load) }; - #[cfg(feature = "tracing")] - let _ = info_span!("Wgpu::Triangle", "BEGIN_RENDER_PASS").enter(); - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("iced_wgpu::triangle render pass"), + label: Some("iced_wgpu.triangle.render_pass"), color_attachments: &[Some( wgpu::RenderPassColorAttachment { view: attachment, @@ -417,7 +310,6 @@ impl Pipeline { layer.render( &self.solid, - #[cfg(not(target_arch = "wasm32"))] &self.gradient, meshes, scale_factor, @@ -463,14 +355,48 @@ fn multisample_state( } } +#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] +#[repr(C)] +pub struct Uniforms { + transform: [f32; 16], + /// Uniform values must be 256-aligned; + /// see: [`wgpu::Limits`] `min_uniform_buffer_offset_alignment`. + _padding: [f32; 48], +} + +impl Uniforms { + pub fn new(transform: Transformation) -> Self { + Self { + transform: transform.into(), + _padding: [0.0; 48], + } + } + + pub fn entry() -> wgpu::BindGroupLayoutEntry { + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: wgpu::BufferSize::new( + std::mem::size_of::<Self>() as u64, + ), + }, + count: None, + } + } + + pub fn min_size() -> Option<wgpu::BufferSize> { + wgpu::BufferSize::new(std::mem::size_of::<Self>() as u64) + } +} + mod solid { - use crate::buffer::dynamic; - use crate::buffer::r#static::Buffer; use crate::graphics::primitive; - use crate::graphics::{Antialiasing, Transformation}; + use crate::graphics::Antialiasing; use crate::triangle; - - use encase::ShaderType; + use crate::Buffer; #[derive(Debug)] pub struct Pipeline { @@ -481,7 +407,7 @@ mod solid { #[derive(Debug)] pub struct Layer { pub vertices: Buffer<primitive::ColoredVertex2D>, - pub uniforms: dynamic::Buffer<Uniforms>, + pub uniforms: Buffer<triangle::Uniforms>, pub constants: wgpu::BindGroup, } @@ -492,17 +418,20 @@ mod solid { ) -> Self { let vertices = Buffer::new( device, - "iced_wgpu::triangle::solid vertex buffer", + "iced_wgpu.triangle.solid.vertex_buffer", + triangle::INITIAL_VERTEX_COUNT, wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, ); - let uniforms = dynamic::Buffer::uniform( + let uniforms = Buffer::new( device, - "iced_wgpu::triangle::solid uniforms", + "iced_wgpu.triangle.solid.uniforms", + 1, + wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, ); let constants = - Self::bind_group(device, uniforms.raw(), constants_layout); + Self::bind_group(device, &uniforms.raw, constants_layout); Self { vertices, @@ -517,7 +446,7 @@ mod solid { layout: &wgpu::BindGroupLayout, ) -> wgpu::BindGroup { device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("iced_wgpu::triangle::solid bind group"), + label: Some("iced_wgpu.triangle.solid.bind_group"), layout, entries: &[wgpu::BindGroupEntry { binding: 0, @@ -525,7 +454,7 @@ mod solid { wgpu::BufferBinding { buffer, offset: 0, - size: Some(Uniforms::min_size()), + size: triangle::Uniforms::min_size(), }, ), }], @@ -533,21 +462,7 @@ mod solid { } } - #[derive(Debug, Clone, Copy, ShaderType)] - pub struct Uniforms { - transform: glam::Mat4, - } - - impl Uniforms { - pub fn new(transform: Transformation) -> Self { - Self { - transform: transform.into(), - } - } - } - impl Pipeline { - /// Creates a new [SolidPipeline] using `solid.wgsl` shader. pub fn new( device: &wgpu::Device, format: wgpu::TextureFormat, @@ -555,23 +470,14 @@ mod solid { ) -> Self { let constants_layout = device.create_bind_group_layout( &wgpu::BindGroupLayoutDescriptor { - label: Some("iced_wgpu::triangle::solid bind group layout"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: true, - min_binding_size: Some(Uniforms::min_size()), - }, - count: None, - }], + label: Some("iced_wgpu.triangle.solid.bind_group_layout"), + entries: &[triangle::Uniforms::entry()], }, ); let layout = device.create_pipeline_layout( &wgpu::PipelineLayoutDescriptor { - label: Some("iced_wgpu::triangle::solid pipeline layout"), + label: Some("iced_wgpu.triangle.solid.pipeline_layout"), bind_group_layouts: &[&constants_layout], push_constant_ranges: &[], }, @@ -579,12 +485,10 @@ mod solid { let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some( - "iced_wgpu triangle solid create shader module", - ), + label: Some("iced_wgpu.triangle.solid.shader"), source: wgpu::ShaderSource::Wgsl( std::borrow::Cow::Borrowed(include_str!( - "shader/solid.wgsl" + "shader/triangle.wgsl" )), ), }); @@ -595,7 +499,7 @@ mod solid { layout: Some(&layout), vertex: wgpu::VertexState { module: &shader, - entry_point: "vs_main", + entry_point: "solid_vs_main", buffers: &[wgpu::VertexBufferLayout { array_stride: std::mem::size_of::< primitive::ColoredVertex2D, @@ -612,7 +516,7 @@ mod solid { }, fragment: Some(wgpu::FragmentState { module: &shader, - entry_point: "fs_main", + entry_point: "solid_fs_main", targets: &[triangle::fragment_target(format)], }), primitive: triangle::primitive_state(), @@ -630,16 +534,10 @@ mod solid { } } -#[cfg(not(target_arch = "wasm32"))] mod gradient { - use crate::buffer::dynamic; - use crate::buffer::r#static::Buffer; - use crate::graphics::Antialiasing; + use crate::graphics::{primitive, Antialiasing}; use crate::triangle; - - use encase::ShaderType; - use glam::{IVec4, Vec4}; - use iced_graphics::primitive; + use crate::Buffer; #[derive(Debug)] pub struct Pipeline { @@ -649,14 +547,9 @@ mod gradient { #[derive(Debug)] pub struct Layer { - pub vertices: Buffer<primitive::Vertex2D>, - pub uniforms: dynamic::Buffer<Uniforms>, - pub storage: dynamic::Buffer<Storage>, + pub vertices: Buffer<primitive::GradientVertex2D>, + pub uniforms: Buffer<triangle::Uniforms>, pub constants: wgpu::BindGroup, - pub color_stop_offset: i32, - //Need to store these and then write them all at once - //or else they will be padded to 256 and cause gaps in the storage buffer - pub color_stops_pending_write: Storage, } impl Layer { @@ -666,94 +559,52 @@ mod gradient { ) -> Self { let vertices = Buffer::new( device, - "iced_wgpu::triangle::gradient vertex buffer", + "iced_wgpu.triangle.gradient.vertex_buffer", + triangle::INITIAL_VERTEX_COUNT, wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, ); - let uniforms = dynamic::Buffer::uniform( + let uniforms = Buffer::new( device, - "iced_wgpu::triangle::gradient uniforms", + "iced_wgpu.triangle.gradient.uniforms", + 1, + wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, ); - // Note: with a WASM target storage buffers are not supported. Will need to use UBOs & static - // sized array (eg like the 32-sized array on OpenGL side right now) to make gradients work - let storage = dynamic::Buffer::storage( - device, - "iced_wgpu::triangle::gradient storage", - ); - - let constants = Self::bind_group( - device, - uniforms.raw(), - storage.raw(), - constants_layout, - ); + let constants = + Self::bind_group(device, &uniforms.raw, constants_layout); Self { vertices, uniforms, - storage, constants, - color_stop_offset: 0, - color_stops_pending_write: Storage { - color_stops: vec![], - }, } } pub fn bind_group( device: &wgpu::Device, uniform_buffer: &wgpu::Buffer, - storage_buffer: &wgpu::Buffer, layout: &wgpu::BindGroupLayout, ) -> wgpu::BindGroup { device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("iced_wgpu::triangle::gradient bind group"), + label: Some("iced_wgpu.triangle.gradient.bind_group"), layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer( - wgpu::BufferBinding { - buffer: uniform_buffer, - offset: 0, - size: Some(Uniforms::min_size()), - }, - ), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: storage_buffer.as_entire_binding(), - }, - ], + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer( + wgpu::BufferBinding { + buffer: uniform_buffer, + offset: 0, + size: triangle::Uniforms::min_size(), + }, + ), + }], }) } } - #[derive(Debug, ShaderType)] - pub struct Uniforms { - pub transform: glam::Mat4, - //xy = start, zw = end - pub direction: Vec4, - //x = start stop, y = end stop, zw = padding - pub stop_range: IVec4, - } - - #[derive(Debug, ShaderType)] - pub struct ColorStop { - pub color: Vec4, - pub offset: f32, - } - - #[derive(Debug, ShaderType)] - pub struct Storage { - #[size(runtime)] - pub color_stops: Vec<ColorStop>, - } - impl Pipeline { - /// Creates a new [GradientPipeline] using `gradient.wgsl` shader. - pub(super) fn new( + pub fn new( device: &wgpu::Device, format: wgpu::TextureFormat, antialiasing: Option<Antialiasing>, @@ -761,40 +612,15 @@ mod gradient { let constants_layout = device.create_bind_group_layout( &wgpu::BindGroupLayoutDescriptor { label: Some( - "iced_wgpu::triangle::gradient bind group layout", + "iced_wgpu.triangle.gradient.bind_group_layout", ), - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: true, - min_binding_size: Some(Uniforms::min_size()), - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Storage { - read_only: true, - }, - has_dynamic_offset: false, - min_binding_size: Some(Storage::min_size()), - }, - count: None, - }, - ], + entries: &[triangle::Uniforms::entry()], }, ); let layout = device.create_pipeline_layout( &wgpu::PipelineLayoutDescriptor { - label: Some( - "iced_wgpu::triangle::gradient pipeline layout", - ), + label: Some("iced_wgpu.triangle.gradient.pipeline_layout"), bind_group_layouts: &[&constants_layout], push_constant_ranges: &[], }, @@ -802,48 +628,56 @@ mod gradient { let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some( - "iced_wgpu::triangle::gradient create shader module", - ), + label: Some("iced_wgpu.triangle.gradient.shader"), source: wgpu::ShaderSource::Wgsl( std::borrow::Cow::Borrowed(include_str!( - "shader/gradient.wgsl" + "shader/triangle.wgsl" )), ), }); - 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, + 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: "gradient_vs_main", + buffers: &[wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::< + primitive::GradientVertex2D, + >() + as u64, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &wgpu::vertex_attr_array!( + // Position + 0 => Float32x2, + // Colors 1-2 + 1 => Uint32x4, + // Colors 3-4 + 2 => Uint32x4, + // Colors 5-6 + 3 => Uint32x4, + // Colors 7-8 + 4 => Uint32x4, + // Offsets + 5 => Uint32x4, + // Direction + 6 => Float32x4 + ), + }], }, - ); + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "gradient_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/triangle/msaa.rs b/wgpu/src/triangle/msaa.rs index 4afbdb32..320b5b12 100644 --- a/wgpu/src/triangle/msaa.rs +++ b/wgpu/src/triangle/msaa.rs @@ -16,15 +16,8 @@ impl Blit { format: wgpu::TextureFormat, antialiasing: graphics::Antialiasing, ) -> Blit { - let sampler = device.create_sampler(&wgpu::SamplerDescriptor { - address_mode_u: wgpu::AddressMode::ClampToEdge, - address_mode_v: wgpu::AddressMode::ClampToEdge, - address_mode_w: wgpu::AddressMode::ClampToEdge, - mag_filter: wgpu::FilterMode::Nearest, - min_filter: wgpu::FilterMode::Nearest, - mipmap_filter: wgpu::FilterMode::Nearest, - ..Default::default() - }); + let sampler = + device.create_sampler(&wgpu::SamplerDescriptor::default()); let constant_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 500458e8..1cfd7b67 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -1,6 +1,7 @@ //! Connect a window with a renderer. -use crate::core::Color; +use crate::core::{Color, Size}; use crate::graphics; +use crate::graphics::color; use crate::graphics::compositor; use crate::graphics::{Error, Primitive, Viewport}; use crate::{Backend, Renderer, Settings}; @@ -69,16 +70,19 @@ impl<Theme> Compositor<Theme> { let format = compatible_surface.as_ref().and_then(|surface| { let capabilities = surface.get_capabilities(&adapter); - capabilities - .formats - .iter() - .copied() - .find(wgpu::TextureFormat::is_srgb) - .or_else(|| { - log::warn!("No sRGB format found!"); + let mut formats = capabilities.formats.iter().copied(); - capabilities.formats.first().copied() - }) + let format = if color::GAMMA_CORRECTION { + formats.find(wgpu::TextureFormat::is_srgb) + } else { + formats.find(|format| !wgpu::TextureFormat::is_srgb(format)) + }; + + format.or_else(|| { + log::warn!("No format found!"); + + capabilities.formats.first().copied() + }) })?; log::info!("Selected format: {:?}", format); @@ -279,4 +283,154 @@ impl<Theme> graphics::Compositor for Compositor<Theme> { ) }) } + + fn screenshot<T: AsRef<str>>( + &mut self, + renderer: &mut Self::Renderer, + _surface: &mut Self::Surface, + viewport: &Viewport, + background_color: Color, + overlay: &[T], + ) -> Vec<u8> { + renderer.with_primitives(|backend, primitives| { + screenshot( + self, + backend, + primitives, + viewport, + background_color, + overlay, + ) + }) + } +} + +/// Renders the current surface to an offscreen buffer. +/// +/// Returns RGBA bytes of the texture data. +pub fn screenshot<Theme, T: AsRef<str>>( + compositor: &Compositor<Theme>, + backend: &mut Backend, + primitives: &[Primitive], + viewport: &Viewport, + background_color: Color, + overlay: &[T], +) -> Vec<u8> { + let mut encoder = compositor.device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { + label: Some("iced_wgpu.offscreen.encoder"), + }, + ); + + let dimensions = BufferDimensions::new(viewport.physical_size()); + + let texture_extent = wgpu::Extent3d { + width: dimensions.width, + height: dimensions.height, + depth_or_array_layers: 1, + }; + + let texture = compositor.device.create_texture(&wgpu::TextureDescriptor { + label: Some("iced_wgpu.offscreen.source_texture"), + size: texture_extent, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: compositor.format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::COPY_SRC + | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + + let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + + backend.present( + &compositor.device, + &compositor.queue, + &mut encoder, + Some(background_color), + &view, + primitives, + viewport, + overlay, + ); + + let texture = crate::color::convert( + &compositor.device, + &mut encoder, + texture, + if color::GAMMA_CORRECTION { + wgpu::TextureFormat::Rgba8UnormSrgb + } else { + wgpu::TextureFormat::Rgba8Unorm + }, + ); + + let output_buffer = + compositor.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("iced_wgpu.offscreen.output_texture_buffer"), + size: (dimensions.padded_bytes_per_row * dimensions.height as usize) + as u64, + usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + encoder.copy_texture_to_buffer( + texture.as_image_copy(), + wgpu::ImageCopyBuffer { + buffer: &output_buffer, + layout: wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: Some(dimensions.padded_bytes_per_row as u32), + rows_per_image: None, + }, + }, + texture_extent, + ); + + let index = compositor.queue.submit(Some(encoder.finish())); + + let slice = output_buffer.slice(..); + slice.map_async(wgpu::MapMode::Read, |_| {}); + + let _ = compositor + .device + .poll(wgpu::Maintain::WaitForSubmissionIndex(index)); + + let mapped_buffer = slice.get_mapped_range(); + + mapped_buffer.chunks(dimensions.padded_bytes_per_row).fold( + vec![], + |mut acc, row| { + acc.extend(&row[..dimensions.unpadded_bytes_per_row]); + acc + }, + ) +} + +#[derive(Clone, Copy, Debug)] +struct BufferDimensions { + width: u32, + height: u32, + unpadded_bytes_per_row: usize, + padded_bytes_per_row: usize, +} + +impl BufferDimensions { + fn new(size: Size<u32>) -> Self { + let unpadded_bytes_per_row = size.width as usize * 4; //slice of buffer per row; always RGBA + let alignment = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize; //256 + let padded_bytes_per_row_padding = + (alignment - unpadded_bytes_per_row % alignment) % alignment; + let padded_bytes_per_row = + unpadded_bytes_per_row + padded_bytes_per_row_padding; + + Self { + width: size.width, + height: size.height, + unpadded_bytes_per_row, + padded_bytes_per_row, + } + } } diff --git a/widget/Cargo.toml b/widget/Cargo.toml index 40e4db37..14aae72e 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -28,7 +28,7 @@ version = "0.8" path = "../style" [dependencies.ouroboros] -version = "0.13" +version = "0.17" optional = true [dependencies.qrcode] diff --git a/widget/src/button.rs b/widget/src/button.rs index 7eee69cb..8ebc9657 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -102,8 +102,17 @@ where /// 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); + pub fn on_press(mut self, on_press: Message) -> Self { + self.on_press = Some(on_press); + self + } + + /// Sets the message that will be produced when the [`Button`] is pressed, + /// if `Some`. + /// + /// If `None`, the [`Button`] will be disabled. + pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self { + self.on_press = on_press; self } @@ -187,7 +196,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -196,7 +205,7 @@ where &mut tree.children[0], event.clone(), layout.children().next().unwrap(), - cursor_position, + cursor, renderer, clipboard, shell, @@ -204,14 +213,9 @@ where return event::Status::Captured; } - update( - event, - layout, - cursor_position, - shell, - &self.on_press, - || tree.state.downcast_mut::<State>(), - ) + update(event, layout, cursor, shell, &self.on_press, || { + tree.state.downcast_mut::<State>() + }) } fn draw( @@ -221,7 +225,7 @@ where theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, ) { let bounds = layout.bounds(); @@ -230,7 +234,7 @@ where let styling = draw( renderer, bounds, - cursor_position, + cursor, self.on_press.is_some(), theme, &self.style, @@ -245,7 +249,7 @@ where text_color: styling.text_color, }, content_layout, - cursor_position, + cursor, &bounds, ); } @@ -254,11 +258,11 @@ where &self, _tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - mouse_interaction(layout, cursor_position, self.on_press.is_some()) + mouse_interaction(layout, cursor, self.on_press.is_some()) } fn overlay<'b>( @@ -305,7 +309,7 @@ impl State { pub fn update<'a, Message: Clone>( event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, on_press: &Option<Message>, state: impl FnOnce() -> &'a mut State, @@ -316,7 +320,7 @@ pub fn update<'a, Message: Clone>( if on_press.is_some() { let bounds = layout.bounds(); - if bounds.contains(cursor_position) { + if cursor.is_over(bounds) { let state = state(); state.is_pressed = true; @@ -335,7 +339,7 @@ pub fn update<'a, Message: Clone>( let bounds = layout.bounds(); - if bounds.contains(cursor_position) { + if cursor.is_over(bounds) { shell.publish(on_press); } @@ -358,7 +362,7 @@ pub fn update<'a, Message: Clone>( pub fn draw<'a, Renderer: crate::core::Renderer>( renderer: &mut Renderer, bounds: Rectangle, - cursor_position: Point, + cursor: mouse::Cursor, is_enabled: bool, style_sheet: &dyn StyleSheet< Style = <Renderer::Theme as StyleSheet>::Style, @@ -369,7 +373,7 @@ pub fn draw<'a, Renderer: crate::core::Renderer>( where Renderer::Theme: StyleSheet, { - let is_mouse_over = bounds.contains(cursor_position); + let is_mouse_over = cursor.is_over(bounds); let styling = if !is_enabled { style_sheet.disabled(style) @@ -395,7 +399,7 @@ where y: bounds.y + styling.shadow_offset.y, ..bounds }, - border_radius: styling.border_radius.into(), + border_radius: styling.border_radius, border_width: 0.0, border_color: Color::TRANSPARENT, }, @@ -406,7 +410,7 @@ where renderer.fill_quad( renderer::Quad { bounds, - border_radius: styling.border_radius.into(), + border_radius: styling.border_radius, border_width: styling.border_width, border_color: styling.border_color, }, @@ -442,10 +446,10 @@ pub fn layout<Renderer>( /// Returns the [`mouse::Interaction`] of a [`Button`]. pub fn mouse_interaction( layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, is_enabled: bool, ) -> mouse::Interaction { - let is_mouse_over = layout.bounds().contains(cursor_position); + let is_mouse_over = cursor.is_over(layout.bounds()); if is_mouse_over && is_enabled { mouse::Interaction::Pointer diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs index 171c4534..96062038 100644 --- a/widget/src/canvas.rs +++ b/widget/src/canvas.rs @@ -1,10 +1,8 @@ //! 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; @@ -17,7 +15,7 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; use crate::core::{Clipboard, Element, Shell, Widget}; -use crate::core::{Length, Point, Rectangle, Size, Vector}; +use crate::core::{Length, Rectangle, Size, Vector}; use crate::graphics::geometry; use std::marker::PhantomData; @@ -28,8 +26,9 @@ use std::marker::PhantomData; /// 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_widget::canvas::{self, Canvas, Fill, Frame, Geometry, Path, Program}; /// # use iced_widget::core::{Color, Rectangle}; +/// # use iced_widget::core::mouse; /// # use iced_widget::style::Theme; /// # /// # pub type Renderer = iced_widget::renderer::Renderer<Theme>; @@ -43,7 +42,7 @@ use std::marker::PhantomData; /// impl Program<()> for Circle { /// type State = (); /// -/// fn draw(&self, _state: &(), renderer: &Renderer, _theme: &Theme, bounds: Rectangle, _cursor: Cursor) -> Vec<Geometry>{ +/// fn draw(&self, _state: &(), renderer: &Renderer, _theme: &Theme, bounds: Rectangle, _cursor: mouse::Cursor) -> Vec<Geometry>{ /// // We prepare a new `Frame` /// let mut frame = Frame::new(renderer, bounds.size()); /// @@ -144,7 +143,7 @@ where tree: &mut Tree, event: core::Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _renderer: &Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -160,8 +159,6 @@ where _ => None, }; - let cursor = Cursor::from_window_position(cursor_position); - if let Some(canvas_event) = canvas_event { let state = tree.state.downcast_mut::<P::State>(); @@ -182,12 +179,11 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { let bounds = layout.bounds(); - let cursor = Cursor::from_window_position(cursor_position); let state = tree.state.downcast_ref::<P::State>(); self.program.mouse_interaction(state, bounds, cursor) @@ -200,7 +196,7 @@ where theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, ) { let bounds = layout.bounds(); @@ -209,7 +205,6 @@ where return; } - let cursor = Cursor::from_window_position(cursor_position); let state = tree.state.downcast_ref::<P::State>(); renderer.with_translation( diff --git a/widget/src/canvas/cursor.rs b/widget/src/canvas/cursor.rs deleted file mode 100644 index 5a65e9a7..00000000 --- a/widget/src/canvas/cursor.rs +++ /dev/null @@ -1,64 +0,0 @@ -use crate::core::{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<Point> { - 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<Point> { - 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<Point> { - 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/widget/src/canvas/program.rs b/widget/src/canvas/program.rs index efb33c56..929ee285 100644 --- a/widget/src/canvas/program.rs +++ b/widget/src/canvas/program.rs @@ -1,8 +1,7 @@ use crate::canvas::event::{self, Event}; use crate::canvas::mouse; -use crate::canvas::Cursor; use crate::core::Rectangle; -use crate::graphics::geometry; +use crate::graphics::geometry::{self, Geometry}; /// The state and logic of a [`Canvas`]. /// @@ -33,7 +32,7 @@ where _state: &mut Self::State, _event: Event, _bounds: Rectangle, - _cursor: Cursor, + _cursor: mouse::Cursor, ) -> (event::Status, Option<Message>) { (event::Status::Ignored, None) } @@ -51,8 +50,8 @@ where renderer: &Renderer, theme: &Renderer::Theme, bounds: Rectangle, - cursor: Cursor, - ) -> Vec<Renderer::Geometry>; + cursor: mouse::Cursor, + ) -> Vec<Geometry>; /// Returns the current mouse interaction of the [`Program`]. /// @@ -64,7 +63,7 @@ where &self, _state: &Self::State, _bounds: Rectangle, - _cursor: Cursor, + _cursor: mouse::Cursor, ) -> mouse::Interaction { mouse::Interaction::default() } @@ -82,7 +81,7 @@ where state: &mut Self::State, event: Event, bounds: Rectangle, - cursor: Cursor, + cursor: mouse::Cursor, ) -> (event::Status, Option<Message>) { T::update(self, state, event, bounds, cursor) } @@ -93,8 +92,8 @@ where renderer: &Renderer, theme: &Renderer::Theme, bounds: Rectangle, - cursor: Cursor, - ) -> Vec<Renderer::Geometry> { + cursor: mouse::Cursor, + ) -> Vec<Geometry> { T::draw(self, state, renderer, theme, bounds, cursor) } @@ -102,7 +101,7 @@ where &self, state: &Self::State, bounds: Rectangle, - cursor: Cursor, + cursor: mouse::Cursor, ) -> mouse::Interaction { T::mouse_interaction(self, state, bounds, cursor) } diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index 7d43bb4a..aa0bff42 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -8,8 +8,8 @@ use crate::core::text; use crate::core::touch; use crate::core::widget::Tree; use crate::core::{ - Alignment, Clipboard, Element, Layout, Length, Pixels, Point, Rectangle, - Shell, Widget, + Alignment, Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, + Widget, }; use crate::{Row, Text}; @@ -204,7 +204,7 @@ where _tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _renderer: &Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -212,7 +212,7 @@ where match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { - let mouse_over = layout.bounds().contains(cursor_position); + let mouse_over = cursor.is_over(layout.bounds()); if mouse_over { shell.publish((self.on_toggle)(!self.is_checked)); @@ -230,11 +230,11 @@ where &self, _tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - if layout.bounds().contains(cursor_position) { + if cursor.is_over(layout.bounds()) { mouse::Interaction::Pointer } else { mouse::Interaction::default() @@ -248,11 +248,10 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, ) { - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); + let is_mouse_over = cursor.is_over(layout.bounds()); let mut children = layout.children(); @@ -269,7 +268,7 @@ where renderer.fill_quad( renderer::Quad { bounds, - border_radius: custom_style.border_radius.into(), + border_radius: custom_style.border_radius, border_width: custom_style.border_width, border_color: custom_style.border_color, }, diff --git a/widget/src/column.rs b/widget/src/column.rs index 8f363ec6..d92d794b 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -6,8 +6,8 @@ use crate::core::overlay; use crate::core::renderer; use crate::core::widget::{Operation, Tree}; use crate::core::{ - Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Point, - Rectangle, Shell, Widget, + Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, + Shell, Widget, }; /// A container that distributes its contents vertically. @@ -166,7 +166,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -180,7 +180,7 @@ where state, event.clone(), layout, - cursor_position, + cursor, renderer, clipboard, shell, @@ -193,7 +193,7 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { @@ -203,11 +203,7 @@ where .zip(layout.children()) .map(|((child, state), layout)| { child.as_widget().mouse_interaction( - state, - layout, - cursor_position, - viewport, - renderer, + state, layout, cursor, viewport, renderer, ) }) .max() @@ -221,7 +217,7 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, ) { for ((child, state), layout) in self @@ -230,15 +226,9 @@ where .zip(&tree.children) .zip(layout.children()) { - child.as_widget().draw( - state, - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ); + child + .as_widget() + .draw(state, renderer, theme, style, layout, cursor, viewport); } } diff --git a/widget/src/container.rs b/widget/src/container.rs index 9d932772..da9a31d6 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -196,7 +196,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -205,7 +205,7 @@ where &mut tree.children[0], event, layout.children().next().unwrap(), - cursor_position, + cursor, renderer, clipboard, shell, @@ -216,14 +216,14 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { self.content.as_widget().mouse_interaction( &tree.children[0], layout.children().next().unwrap(), - cursor_position, + cursor, viewport, renderer, ) @@ -236,7 +236,7 @@ where theme: &Renderer::Theme, renderer_style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, ) { let style = theme.appearance(&self.style); @@ -253,7 +253,7 @@ where .unwrap_or(renderer_style.text_color), }, layout.children().next().unwrap(), - cursor_position, + cursor, viewport, ); } @@ -332,7 +332,7 @@ pub fn draw_background<Renderer>( renderer.fill_quad( renderer::Quad { bounds, - border_radius: appearance.border_radius.into(), + border_radius: appearance.border_radius, border_width: appearance.border_width, border_color: appearance.border_color, }, diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 336ac4ee..3f5136f8 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -310,7 +310,6 @@ where /// /// [`Image`]: widget::Image #[cfg(feature = "image")] -#[cfg_attr(docsrs, doc(cfg(feature = "image")))] pub fn image<Handle>(handle: impl Into<Handle>) -> crate::Image<Handle> { crate::Image::new(handle.into()) } @@ -320,7 +319,6 @@ pub fn image<Handle>(handle: impl Into<Handle>) -> crate::Image<Handle> { /// [`Svg`]: widget::Svg /// [`Handle`]: widget::svg::Handle #[cfg(feature = "svg")] -#[cfg_attr(docsrs, doc(cfg(feature = "svg")))] pub fn svg<Renderer>( handle: impl Into<core::svg::Handle>, ) -> crate::Svg<Renderer> @@ -333,7 +331,6 @@ where /// Creates a new [`Canvas`]. #[cfg(feature = "canvas")] -#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] pub fn canvas<P, Message, Renderer>( program: P, ) -> crate::Canvas<P, Message, Renderer> diff --git a/widget/src/image.rs b/widget/src/image.rs index abcb6ef2..66bf2156 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -4,10 +4,11 @@ pub use viewer::Viewer; use crate::core::image; use crate::core::layout; +use crate::core::mouse; use crate::core::renderer; use crate::core::widget::Tree; use crate::core::{ - ContentFit, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget, + ContentFit, Element, Layout, Length, Rectangle, Size, Vector, Widget, }; use std::hash::Hash; @@ -186,7 +187,7 @@ where _theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, - _cursor_position: Point, + _cursor: mouse::Cursor, _viewport: &Rectangle, ) { draw(renderer, layout, &self.handle, self.content_fit) diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 0d60d818..8040d6bd 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -144,18 +144,19 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, 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 => - { + Event::Mouse(mouse::Event::WheelScrolled { delta }) => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored; + }; + match delta { mouse::ScrollDelta::Lines { y, .. } | mouse::ScrollDelta::Pixels { y, .. } => { @@ -205,9 +206,11 @@ where event::Status::Captured } - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - if is_mouse_over => - { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored; + }; + let state = tree.state.downcast_mut::<State>(); state.cursor_grabbed_at = Some(cursor_position); @@ -277,13 +280,13 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { let state = tree.state.downcast_ref::<State>(); let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); + let is_mouse_over = cursor.is_over(bounds); if state.is_cursor_grabbed() { mouse::Interaction::Grabbing @@ -301,7 +304,7 @@ where _theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, - _cursor_position: Point, + _cursor: mouse::Cursor, _viewport: &Rectangle, ) { let state = tree.state.downcast_ref::<State>(); diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index b08ed8cb..da287f06 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -20,12 +20,14 @@ use crate::core::Element; use crate::core::{ self, Clipboard, Hasher, Length, Point, Rectangle, Shell, Size, }; +use crate::runtime::overlay::Nested; use ouroboros::self_referencing; use std::cell::RefCell; use std::hash::{Hash, Hasher as H}; use std::rc::Rc; +/// A widget that only rebuilds its contents when necessary. #[allow(missing_debug_implementations)] pub struct Lazy<'a, Message, Renderer, Dependency, View> { dependency: Dependency, @@ -41,6 +43,8 @@ where Dependency: Hash + 'a, View: Into<Element<'static, Message, Renderer>>, { + /// Creates a new [`Lazy`] widget with the given data `Dependency` and a + /// closure that can turn this data into a widget tree. pub fn new( dependency: Dependency, view: impl Fn(&Dependency) -> View + 'a, @@ -178,7 +182,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -188,7 +192,7 @@ where &mut tree.children[0], event, layout, - cursor_position, + cursor, renderer, clipboard, shell, @@ -200,7 +204,7 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { @@ -208,7 +212,7 @@ where element.as_widget().mouse_interaction( &tree.children[0], layout, - cursor_position, + cursor, viewport, renderer, ) @@ -222,7 +226,7 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, ) { self.with_element(|element| { @@ -232,7 +236,7 @@ where theme, style, layout, - cursor_position, + cursor, viewport, ) }) @@ -257,14 +261,17 @@ where .unwrap(), tree: &mut tree.children[0], overlay_builder: |element, tree| { - element.as_widget_mut().overlay(tree, layout, renderer) + element + .as_widget_mut() + .overlay(tree, layout, renderer) + .map(|overlay| RefCell::new(Nested::new(overlay))) }, } .build(), )); - let has_overlay = overlay - .with_overlay_maybe(|overlay| overlay::Element::position(overlay)); + let has_overlay = + overlay.with_overlay_maybe(|overlay| overlay.position()); has_overlay .map(|position| overlay::Element::new(position, Box::new(overlay))) @@ -272,18 +279,14 @@ where } #[self_referencing] -struct Inner<'a, Message, Renderer> -where - Message: 'a, - Renderer: 'a, -{ +struct Inner<'a, Message: 'a, Renderer: 'a> { cell: Rc<RefCell<Option<Element<'static, Message, Renderer>>>>, element: Element<'static, Message, Renderer>, tree: &'a mut Tree, #[borrows(mut element, mut tree)] - #[covariant] - overlay: Option<overlay::Element<'this, Message, Renderer>>, + #[not_covariant] + overlay: Option<RefCell<Nested<'this, Message, Renderer>>>, } struct Overlay<'a, Message, Renderer>(Option<Inner<'a, Message, Renderer>>); @@ -298,19 +301,20 @@ impl<'a, Message, Renderer> Drop for Overlay<'a, Message, Renderer> { impl<'a, Message, Renderer> Overlay<'a, Message, Renderer> { fn with_overlay_maybe<T>( &self, - f: impl FnOnce(&overlay::Element<'_, Message, Renderer>) -> T, + f: impl FnOnce(&mut Nested<'_, Message, Renderer>) -> T, ) -> Option<T> { - self.0.as_ref().unwrap().borrow_overlay().as_ref().map(f) + self.0.as_ref().unwrap().with_overlay(|overlay| { + overlay.as_ref().map(|nested| (f)(&mut nested.borrow_mut())) + }) } fn with_overlay_mut_maybe<T>( &mut self, - f: impl FnOnce(&mut overlay::Element<'_, Message, Renderer>) -> T, + f: impl FnOnce(&mut Nested<'_, Message, Renderer>) -> T, ) -> Option<T> { - self.0 - .as_mut() - .unwrap() - .with_overlay_mut(|overlay| overlay.as_mut().map(f)) + self.0.as_mut().unwrap().with_overlay_mut(|overlay| { + overlay.as_mut().map(|nested| (f)(nested.get_mut())) + }) } } @@ -326,9 +330,7 @@ where position: Point, ) -> layout::Node { self.with_overlay_maybe(|overlay| { - let translation = position - overlay.position(); - - overlay.layout(renderer, bounds, translation) + overlay.layout(renderer, bounds, position) }) .unwrap_or_default() } @@ -339,27 +341,22 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, ) { let _ = self.with_overlay_maybe(|overlay| { - overlay.draw(renderer, theme, style, layout, cursor_position); + overlay.draw(renderer, theme, style, layout, cursor); }); } fn mouse_interaction( &self, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { self.with_overlay_maybe(|overlay| { - overlay.mouse_interaction( - layout, - cursor_position, - viewport, - renderer, - ) + overlay.mouse_interaction(layout, cursor, viewport, renderer) }) .unwrap_or_default() } @@ -368,27 +365,25 @@ where &mut self, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, 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, - ) + overlay.on_event(event, layout, cursor, renderer, clipboard, shell) }) .unwrap_or(event::Status::Ignored) } - fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { + fn is_over( + &self, + layout: Layout<'_>, + renderer: &Renderer, + cursor_position: Point, + ) -> bool { self.with_overlay_maybe(|overlay| { - overlay.is_over(layout, cursor_position) + overlay.is_over(layout, renderer, cursor_position) }) .unwrap_or_default() } diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index 49ae68af..f955d9dd 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -9,6 +9,7 @@ use crate::core::widget::tree::{self, Tree}; use crate::core::{ self, Clipboard, Element, Length, Point, Rectangle, Shell, Size, Widget, }; +use crate::runtime::overlay::Nested; use ouroboros::self_referencing; use std::cell::RefCell; @@ -265,7 +266,7 @@ where tree: &mut Tree, event: core::Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -279,7 +280,7 @@ where &mut t.borrow_mut().as_mut().unwrap().children[0], event, layout, - cursor_position, + cursor, renderer, clipboard, &mut local_shell, @@ -397,7 +398,7 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, ) { let tree = tree.state.downcast_ref::<Rc<RefCell<Option<Tree>>>>(); @@ -408,7 +409,7 @@ where theme, style, layout, - cursor_position, + cursor, viewport, ); }); @@ -418,7 +419,7 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { @@ -427,7 +428,7 @@ where element.as_widget().mouse_interaction( &tree.borrow().as_ref().unwrap().children[0], layout, - cursor_position, + cursor, viewport, renderer, ) @@ -455,11 +456,18 @@ where 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, - ) + element + .as_mut() + .unwrap() + .as_widget_mut() + .overlay( + &mut tree.children[0], + layout, + renderer, + ) + .map(|overlay| { + RefCell::new(Nested::new(overlay)) + }) }, ) }, @@ -468,7 +476,7 @@ where )); let has_overlay = overlay.0.as_ref().unwrap().with_overlay(|overlay| { - overlay.as_ref().map(overlay::Element::position) + overlay.as_ref().map(|nested| nested.borrow().position()) }); has_overlay.map(|position| { @@ -503,8 +511,8 @@ struct Inner<'a, 'b, Message, Renderer, Event, S> { types: PhantomData<(Message, Event, S)>, #[borrows(mut instance, mut tree)] - #[covariant] - overlay: Option<overlay::Element<'this, Event, Renderer>>, + #[not_covariant] + overlay: Option<RefCell<Nested<'this, Event, Renderer>>>, } struct OverlayInstance<'a, 'b, Message, Renderer, Event, S> { @@ -516,7 +524,7 @@ impl<'a, 'b, Message, Renderer, Event, S> { fn with_overlay_maybe<T>( &self, - f: impl FnOnce(&overlay::Element<'_, Event, Renderer>) -> T, + f: impl FnOnce(&mut Nested<'_, Event, Renderer>) -> T, ) -> Option<T> { self.overlay .as_ref() @@ -524,14 +532,14 @@ impl<'a, 'b, Message, Renderer, Event, S> .0 .as_ref() .unwrap() - .borrow_overlay() - .as_ref() - .map(f) + .with_overlay(|overlay| { + overlay.as_ref().map(|nested| (f)(&mut nested.borrow_mut())) + }) } fn with_overlay_mut_maybe<T>( &mut self, - f: impl FnOnce(&mut overlay::Element<'_, Event, Renderer>) -> T, + f: impl FnOnce(&mut Nested<'_, Event, Renderer>) -> T, ) -> Option<T> { self.overlay .as_mut() @@ -539,7 +547,9 @@ impl<'a, 'b, Message, Renderer, Event, S> .0 .as_mut() .unwrap() - .with_overlay_mut(|overlay| overlay.as_mut().map(f)) + .with_overlay_mut(|overlay| { + overlay.as_mut().map(|nested| (f)(nested.get_mut())) + }) } } @@ -556,9 +566,7 @@ where position: Point, ) -> layout::Node { self.with_overlay_maybe(|overlay| { - let translation = position - overlay.position(); - - overlay.layout(renderer, bounds, translation) + overlay.layout(renderer, bounds, position) }) .unwrap_or_default() } @@ -569,27 +577,22 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, ) { let _ = self.with_overlay_maybe(|overlay| { - overlay.draw(renderer, theme, style, layout, cursor_position); + overlay.draw(renderer, theme, style, layout, cursor); }); } fn mouse_interaction( &self, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { self.with_overlay_maybe(|overlay| { - overlay.mouse_interaction( - layout, - cursor_position, - viewport, - renderer, - ) + overlay.mouse_interaction(layout, cursor, viewport, renderer) }) .unwrap_or_default() } @@ -598,7 +601,7 @@ where &mut self, event: core::Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -611,7 +614,7 @@ where overlay.on_event( event, layout, - cursor_position, + cursor, renderer, clipboard, &mut local_shell, @@ -660,9 +663,14 @@ where event_status } - fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { + fn is_over( + &self, + layout: Layout<'_>, + renderer: &Renderer, + cursor_position: Point, + ) -> bool { self.with_overlay_maybe(|overlay| { - overlay.is_over(layout, cursor_position) + overlay.is_over(layout, renderer, cursor_position) }) .unwrap_or_default() } diff --git a/widget/src/lazy/helpers.rs b/widget/src/lazy/helpers.rs index be60bb78..8ca9cb86 100644 --- a/widget/src/lazy/helpers.rs +++ b/widget/src/lazy/helpers.rs @@ -4,6 +4,8 @@ use crate::lazy::{Lazy, Responsive}; use std::hash::Hash; +/// Creates a new [`Lazy`] widget with the given data `Dependency` and a +/// closure that can turn this data into a widget tree. pub fn lazy<'a, Message, Renderer, Dependency, View>( dependency: Dependency, view: impl Fn(&Dependency) -> View + 'a, @@ -29,6 +31,12 @@ where component::view(component) } +/// 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 responsive<'a, Message, Renderer>( f: impl Fn(Size) -> Element<'a, Message, Renderer> + 'a, ) -> Responsive<'a, Message, Renderer> diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index b41d978b..07300857 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -9,6 +9,7 @@ use crate::core::{ self, Clipboard, Element, Length, Point, Rectangle, Shell, Size, Widget, }; use crate::horizontal_space; +use crate::runtime::overlay::Nested; use ouroboros::self_referencing; use std::cell::{RefCell, RefMut}; @@ -81,6 +82,7 @@ where self.element = view(new_size); self.size = new_size; + self.layout = None; tree.diff(&self.element); } @@ -176,7 +178,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -197,7 +199,7 @@ where tree, event, layout, - cursor_position, + cursor, renderer, clipboard, &mut local_shell, @@ -221,7 +223,7 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, ) { let state = tree.state.downcast_ref::<State>(); @@ -234,13 +236,7 @@ where &self.view, |tree, renderer, layout, element| { element.as_widget().draw( - tree, - renderer, - theme, - style, - layout, - cursor_position, - viewport, + tree, renderer, theme, style, layout, cursor, viewport, ) }, ) @@ -250,7 +246,7 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { @@ -263,13 +259,9 @@ where layout, &self.view, |tree, renderer, layout, element| { - element.as_widget().mouse_interaction( - tree, - layout, - cursor_position, - viewport, - renderer, - ) + element + .as_widget() + .mouse_interaction(tree, layout, cursor, viewport, renderer) }, ) } @@ -307,13 +299,13 @@ where element .as_widget_mut() .overlay(tree, content_layout, renderer) + .map(|overlay| RefCell::new(Nested::new(overlay))) }, } .build(); - let has_overlay = overlay.with_overlay(|overlay| { - overlay.as_ref().map(overlay::Element::position) - }); + let has_overlay = + overlay.with_overlay_maybe(|overlay| overlay.position()); has_overlay .map(|position| overlay::Element::new(position, Box::new(overlay))) @@ -338,23 +330,27 @@ struct Overlay<'a, 'b, Message, Renderer> { types: PhantomData<Message>, #[borrows(mut content, mut tree)] - #[covariant] - overlay: Option<overlay::Element<'this, Message, Renderer>>, + #[not_covariant] + overlay: Option<RefCell<Nested<'this, Message, Renderer>>>, } impl<'a, 'b, Message, Renderer> Overlay<'a, 'b, Message, Renderer> { fn with_overlay_maybe<T>( &self, - f: impl FnOnce(&overlay::Element<'_, Message, Renderer>) -> T, + f: impl FnOnce(&mut Nested<'_, Message, Renderer>) -> T, ) -> Option<T> { - self.borrow_overlay().as_ref().map(f) + self.with_overlay(|overlay| { + overlay.as_ref().map(|nested| (f)(&mut nested.borrow_mut())) + }) } fn with_overlay_mut_maybe<T>( &mut self, - f: impl FnOnce(&mut overlay::Element<'_, Message, Renderer>) -> T, + f: impl FnOnce(&mut Nested<'_, Message, Renderer>) -> T, ) -> Option<T> { - self.with_overlay_mut(|overlay| overlay.as_mut().map(f)) + self.with_overlay_mut(|overlay| { + overlay.as_mut().map(|nested| (f)(nested.get_mut())) + }) } } @@ -370,9 +366,7 @@ where position: Point, ) -> layout::Node { self.with_overlay_maybe(|overlay| { - let translation = position - overlay.position(); - - overlay.layout(renderer, bounds, translation) + overlay.layout(renderer, bounds, position) }) .unwrap_or_default() } @@ -383,27 +377,22 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, ) { let _ = self.with_overlay_maybe(|overlay| { - overlay.draw(renderer, theme, style, layout, cursor_position); + overlay.draw(renderer, theme, style, layout, cursor); }); } fn mouse_interaction( &self, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { self.with_overlay_maybe(|overlay| { - overlay.mouse_interaction( - layout, - cursor_position, - viewport, - renderer, - ) + overlay.mouse_interaction(layout, cursor, viewport, renderer) }) .unwrap_or_default() } @@ -412,27 +401,25 @@ where &mut self, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, 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, - ) + overlay.on_event(event, layout, cursor, renderer, clipboard, shell) }) .unwrap_or(event::Status::Ignored) } - fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { + fn is_over( + &self, + layout: Layout<'_>, + renderer: &Renderer, + cursor_position: Point, + ) -> bool { self.with_overlay_maybe(|overlay| { - overlay.is_over(layout, cursor_position) + overlay.is_over(layout, renderer, cursor_position) }) .unwrap_or_default() } diff --git a/widget/src/lib.rs b/widget/src/lib.rs index 904f62ad..9da13f9b 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -4,7 +4,7 @@ )] #![deny( missing_debug_implementations, - //missing_docs, + missing_docs, unused_results, clippy::extra_unused_lifetimes, clippy::from_over_into, @@ -14,6 +14,7 @@ )] #![forbid(unsafe_code, rust_2018_idioms)] #![allow(clippy::inherent_to_string, clippy::type_complexity)] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] pub use iced_renderer as renderer; pub use iced_renderer::graphics; pub use iced_runtime as runtime; diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index 0232c494..da7dc88f 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -8,7 +8,7 @@ use crate::core::renderer; use crate::core::touch; use crate::core::widget::{tree, Operation, Tree}; use crate::core::{ - Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Widget, + Clipboard, Element, Layout, Length, Rectangle, Shell, Widget, }; /// Emit messages on mouse events. @@ -146,7 +146,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -155,7 +155,7 @@ where &mut tree.children[0], event.clone(), layout, - cursor_position, + cursor, renderer, clipboard, shell, @@ -163,21 +163,21 @@ where return event::Status::Captured; } - update(self, &event, layout, cursor_position, shell) + update(self, &event, layout, cursor, shell) } fn mouse_interaction( &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { self.content.as_widget().mouse_interaction( &tree.children[0], layout, - cursor_position, + cursor, viewport, renderer, ) @@ -190,7 +190,7 @@ where theme: &Renderer::Theme, renderer_style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, ) { self.content.as_widget().draw( @@ -199,7 +199,7 @@ where theme, renderer_style, layout, - cursor_position, + cursor, viewport, ); } @@ -237,10 +237,10 @@ fn update<Message: Clone, Renderer>( widget: &mut MouseArea<'_, Message, Renderer>, event: &Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, ) -> event::Status { - if !layout.bounds().contains(cursor_position) { + if !cursor.is_over(layout.bounds()) { return event::Status::Ignored; } diff --git a/widget/src/overlay.rs b/widget/src/overlay.rs index b9a0e3e0..bc0ed744 100644 --- a/widget/src/overlay.rs +++ b/widget/src/overlay.rs @@ -1 +1,2 @@ +//! Display interactive elements on top of other widgets. pub mod menu; diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index 0acc6f79..ccf4dfb5 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -19,7 +19,7 @@ pub use iced_style::menu::{Appearance, StyleSheet}; /// A list of selectable options. #[allow(missing_debug_implementations)] -pub struct Menu<'a, T, Renderer = crate::Renderer> +pub struct Menu<'a, T, Message, Renderer = crate::Renderer> where Renderer: text::Renderer, Renderer::Theme: StyleSheet, @@ -27,7 +27,7 @@ where state: &'a mut State, options: &'a [T], hovered_option: &'a mut Option<usize>, - last_selection: &'a mut Option<T>, + on_selected: Box<dyn FnMut(T) -> Message + 'a>, width: f32, padding: Padding, text_size: Option<f32>, @@ -37,9 +37,10 @@ where style: <Renderer::Theme as StyleSheet>::Style, } -impl<'a, T, Renderer> Menu<'a, T, Renderer> +impl<'a, T, Message, Renderer> Menu<'a, T, Message, Renderer> where T: ToString + Clone, + Message: 'a, Renderer: text::Renderer + 'a, Renderer::Theme: StyleSheet + container::StyleSheet + scrollable::StyleSheet, @@ -50,13 +51,13 @@ where state: &'a mut State, options: &'a [T], hovered_option: &'a mut Option<usize>, - last_selection: &'a mut Option<T>, + on_selected: impl FnMut(T) -> Message + 'a, ) -> Self { Menu { state, options, hovered_option, - last_selection, + on_selected: Box::new(on_selected), width: 0.0, padding: Padding::ZERO, text_size: None, @@ -121,7 +122,7 @@ where /// 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<Message: 'a>( + pub fn overlay( self, position: Point, target_height: f32, @@ -174,7 +175,10 @@ where Renderer::Theme: StyleSheet + container::StyleSheet + scrollable::StyleSheet, { - pub fn new<T>(menu: Menu<'a, T, Renderer>, target_height: f32) -> Self + pub fn new<T>( + menu: Menu<'a, T, Message, Renderer>, + target_height: f32, + ) -> Self where T: Clone + ToString, { @@ -182,7 +186,7 @@ where state, options, hovered_option, - last_selection, + on_selected, width, padding, font, @@ -195,7 +199,7 @@ where let container = Container::new(Scrollable::new(List { options, hovered_option, - last_selection, + on_selected, font, text_size, text_line_height, @@ -259,36 +263,25 @@ where &mut self, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, 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, + self.state, event, layout, cursor, renderer, clipboard, shell, ) } fn mouse_interaction( &self, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - self.container.mouse_interaction( - self.state, - layout, - cursor_position, - viewport, - renderer, - ) + self.container + .mouse_interaction(self.state, layout, cursor, viewport, renderer) } fn draw( @@ -297,7 +290,7 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, ) { let appearance = theme.appearance(&self.style); let bounds = layout.bounds(); @@ -307,31 +300,24 @@ where bounds, border_color: appearance.border_color, border_width: appearance.border_width, - border_radius: appearance.border_radius.into(), + border_radius: appearance.border_radius, }, appearance.background, ); - self.container.draw( - self.state, - renderer, - theme, - style, - layout, - cursor_position, - &bounds, - ); + self.container + .draw(self.state, renderer, theme, style, layout, cursor, &bounds); } } -struct List<'a, T, Renderer> +struct List<'a, T, Message, Renderer> where Renderer: text::Renderer, Renderer::Theme: StyleSheet, { options: &'a [T], hovered_option: &'a mut Option<usize>, - last_selection: &'a mut Option<T>, + on_selected: Box<dyn FnMut(T) -> Message + 'a>, padding: Padding, text_size: Option<f32>, text_line_height: text::LineHeight, @@ -341,7 +327,7 @@ where } impl<'a, T, Message, Renderer> Widget<Message, Renderer> - for List<'a, T, Renderer> + for List<'a, T, Message, Renderer> where T: Clone + ToString, Renderer: text::Renderer, @@ -387,27 +373,26 @@ where _state: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, _clipboard: &mut dyn Clipboard, - _shell: &mut Shell<'_, Message>, + 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 cursor.is_over(layout.bounds()) { if let Some(index) = *self.hovered_option { if let Some(option) = self.options.get(index) { - *self.last_selection = Some(option.clone()); + shell.publish((self.on_selected)(option.clone())); + return event::Status::Captured; } } } } Event::Mouse(mouse::Event::CursorMoved { .. }) => { - let bounds = layout.bounds(); - - if bounds.contains(cursor_position) { + if let Some(cursor_position) = + cursor.position_in(layout.bounds()) + { let text_size = self .text_size .unwrap_or_else(|| renderer.default_size()); @@ -416,16 +401,14 @@ where self.text_line_height.to_absolute(Pixels(text_size)), ) + self.padding.vertical(); - *self.hovered_option = Some( - ((cursor_position.y - bounds.y) / option_height) - as usize, - ); + *self.hovered_option = + Some((cursor_position.y / option_height) as usize); } } Event::Touch(touch::Event::FingerPressed { .. }) => { - let bounds = layout.bounds(); - - if bounds.contains(cursor_position) { + if let Some(cursor_position) = + cursor.position_in(layout.bounds()) + { let text_size = self .text_size .unwrap_or_else(|| renderer.default_size()); @@ -434,14 +417,13 @@ where self.text_line_height.to_absolute(Pixels(text_size)), ) + self.padding.vertical(); - *self.hovered_option = Some( - ((cursor_position.y - bounds.y) / option_height) - as usize, - ); + *self.hovered_option = + Some((cursor_position.y / option_height) as usize); if let Some(index) = *self.hovered_option { if let Some(option) = self.options.get(index) { - *self.last_selection = Some(option.clone()); + shell.publish((self.on_selected)(option.clone())); + return event::Status::Captured; } } } @@ -456,11 +438,11 @@ where &self, _state: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - let is_mouse_over = layout.bounds().contains(cursor_position); + let is_mouse_over = cursor.is_over(layout.bounds()); if is_mouse_over { mouse::Interaction::Pointer @@ -476,7 +458,7 @@ where theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, - _cursor_position: Point, + _cursor: mouse::Cursor, viewport: &Rectangle, ) { let appearance = theme.appearance(&self.style); @@ -515,7 +497,7 @@ where }, border_color: Color::TRANSPARENT, border_width: 0.0, - border_radius: appearance.border_radius.into(), + border_radius: appearance.border_radius, }, appearance.selected_background, ); @@ -545,7 +527,7 @@ where } } -impl<'a, T, Message, Renderer> From<List<'a, T, Renderer>> +impl<'a, T, Message, Renderer> From<List<'a, T, Message, Renderer>> for Element<'a, Message, Renderer> where T: ToString + Clone, @@ -553,7 +535,7 @@ where Renderer: 'a + text::Renderer, Renderer::Theme: StyleSheet, { - fn from(list: List<'a, T, Renderer>) -> Self { + fn from(list: List<'a, T, Message, Renderer>) -> Self { Element::new(list) } } diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 67145e8e..040d6bb3 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -30,7 +30,7 @@ pub use split::Split; pub use state::State; pub use title_bar::TitleBar; -pub use crate::style::pane_grid::{Line, StyleSheet}; +pub use crate::style::pane_grid::{Appearance, Line, StyleSheet}; use crate::container; use crate::core::event::{self, Event}; @@ -313,7 +313,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -331,7 +331,7 @@ where self.contents.layout(), &event, layout, - cursor_position, + cursor, shell, self.spacing, self.contents.iter(), @@ -353,7 +353,7 @@ where tree, event.clone(), layout, - cursor_position, + cursor, renderer, clipboard, shell, @@ -367,7 +367,7 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { @@ -375,7 +375,7 @@ where tree.state.downcast_ref(), self.contents.layout(), layout, - cursor_position, + cursor, self.spacing, self.on_resize.as_ref().map(|(leeway, _)| *leeway), ) @@ -388,7 +388,7 @@ where content.mouse_interaction( tree, layout, - cursor_position, + cursor, viewport, renderer, self.drag_enabled(), @@ -406,14 +406,14 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, ) { draw( tree.state.downcast_ref(), self.contents.layout(), layout, - cursor_position, + cursor, renderer, theme, style, @@ -425,20 +425,9 @@ where .iter() .zip(&tree.children) .map(|((pane, content), tree)| (pane, (content, tree))), - |(content, tree), - renderer, - style, - layout, - cursor_position, - rectangle| { + |(content, tree), renderer, style, layout, cursor, rectangle| { content.draw( - tree, - renderer, - theme, - style, - layout, - cursor_position, - rectangle, + tree, renderer, theme, style, layout, cursor, rectangle, ); }, ) @@ -520,7 +509,7 @@ pub fn update<'a, Message, T: Draggable>( node: &Node, event: &Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, spacing: f32, contents: impl Iterator<Item = (Pane, T)>, @@ -535,7 +524,7 @@ pub fn update<'a, Message, T: Draggable>( | Event::Touch(touch::Event::FingerPressed { .. }) => { let bounds = layout.bounds(); - if bounds.contains(cursor_position) { + if let Some(cursor_position) = cursor.position_over(bounds) { event_status = event::Status::Captured; match on_resize { @@ -592,15 +581,24 @@ pub fn update<'a, Message, T: Draggable>( | 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 dropped_region = + cursor.position().and_then(|cursor_position| { + contents + .zip(layout.children()) + .filter_map(|(target, layout)| { + layout_region(layout, cursor_position) + .map(|region| (target, region)) + }) + .next() }); - let event = match dropped_region.next() { - Some(((target, _), _)) if pane != target => { - DragEvent::Dropped { pane, target } + let event = match dropped_region { + Some(((target, _), region)) if pane != target => { + DragEvent::Dropped { + pane, + target, + region, + } } _ => DragEvent::Canceled { pane }, }; @@ -629,24 +627,32 @@ pub fn update<'a, Message, T: Draggable>( ); 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; + if let Some(cursor_position) = cursor.position() { + 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; + } } } } @@ -657,6 +663,28 @@ pub fn update<'a, Message, T: Draggable>( event_status } +fn layout_region(layout: Layout<'_>, cursor_position: Point) -> Option<Region> { + let bounds = layout.bounds(); + + if !bounds.contains(cursor_position) { + return None; + } + + let region = if cursor_position.x < (bounds.x + bounds.width / 3.0) { + Region::Left + } else if cursor_position.x > (bounds.x + 2.0 * bounds.width / 3.0) { + Region::Right + } else if cursor_position.y < (bounds.y + bounds.height / 3.0) { + Region::Top + } else if cursor_position.y > (bounds.y + 2.0 * bounds.height / 3.0) { + Region::Bottom + } else { + Region::Center + }; + + Some(region) +} + fn click_pane<'a, Message, T>( action: &mut state::Action, layout: Layout<'_>, @@ -697,7 +725,7 @@ pub fn mouse_interaction( action: &state::Action, node: &Node, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, spacing: f32, resize_leeway: Option<f32>, ) -> Option<mouse::Interaction> { @@ -708,6 +736,7 @@ pub fn mouse_interaction( let resize_axis = action.picked_split().map(|(_, axis)| axis).or_else(|| { resize_leeway.and_then(|leeway| { + let cursor_position = cursor.position()?; let bounds = layout.bounds(); let splits = node.split_regions(spacing, bounds.size()); @@ -737,7 +766,7 @@ pub fn draw<Renderer, T>( action: &state::Action, node: &Node, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &mut Renderer, theme: &Renderer::Theme, default_style: &renderer::Style, @@ -751,7 +780,7 @@ pub fn draw<Renderer, T>( &mut Renderer, &renderer::Style, Layout<'_>, - Point, + mouse::Cursor, &Rectangle, ), ) where @@ -775,6 +804,7 @@ pub fn draw<Renderer, T>( }) .or_else(|| match resize_leeway { Some(leeway) => { + let cursor_position = cursor.position()?; let bounds = layout.bounds(); let relative_cursor = Point::new( @@ -795,12 +825,10 @@ pub fn draw<Renderer, T>( 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) + let pane_cursor = if picked_pane.is_some() { + mouse::Cursor::Unavailable } else { - cursor_position + cursor }; let mut render_picked_pane = None; @@ -810,13 +838,45 @@ pub fn draw<Renderer, T>( Some((dragging, origin)) if id == dragging => { render_picked_pane = Some((pane, origin, layout)); } + Some((dragging, _)) if id != dragging => { + draw_pane( + pane, + renderer, + default_style, + layout, + pane_cursor, + viewport, + ); + + if picked_pane.is_some() { + if let Some(region) = + cursor.position().and_then(|cursor_position| { + layout_region(layout, cursor_position) + }) + { + let bounds = layout_region_bounds(layout, region); + let hovered_region_style = theme.hovered_region(style); + + renderer.fill_quad( + renderer::Quad { + bounds, + border_radius: hovered_region_style + .border_radius, + border_width: hovered_region_style.border_width, + border_color: hovered_region_style.border_color, + }, + theme.hovered_region(style).background, + ); + } + } + } _ => { draw_pane( pane, renderer, default_style, layout, - pane_cursor_position, + pane_cursor, viewport, ); } @@ -825,25 +885,27 @@ pub fn draw<Renderer, T>( // 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(cursor_position) = cursor.position() { + 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, + viewport, + ); + }); + }, + ); + } + } if let Some((axis, split_region, is_picked)) = picked_split { let highlight = if is_picked { @@ -884,6 +946,32 @@ pub fn draw<Renderer, T>( } } +fn layout_region_bounds(layout: Layout<'_>, region: Region) -> Rectangle { + let bounds = layout.bounds(); + + match region { + Region::Center => bounds, + Region::Top => Rectangle { + height: bounds.height / 2.0, + ..bounds + }, + Region::Left => Rectangle { + width: bounds.width / 2.0, + ..bounds + }, + Region::Right => Rectangle { + x: bounds.x + bounds.width / 2.0, + width: bounds.width / 2.0, + ..bounds + }, + Region::Bottom => Rectangle { + y: bounds.y + bounds.height / 2.0, + height: bounds.height / 2.0, + ..bounds + }, + } +} + /// An event produced during a drag and drop interaction of a [`PaneGrid`]. #[derive(Debug, Clone, Copy)] pub enum DragEvent { @@ -900,6 +988,9 @@ pub enum DragEvent { /// The [`Pane`] where the picked one was dropped on. target: Pane, + + /// The [`Region`] of the target [`Pane`] where the picked one was dropped on. + region: Region, }, /// A [`Pane`] was picked and then dropped outside of other [`Pane`] @@ -910,6 +1001,22 @@ pub enum DragEvent { }, } +/// The region of a [`Pane`]. +#[derive(Debug, Clone, Copy, Default)] +pub enum Region { + /// Center region. + #[default] + Center, + /// Top region. + Top, + /// Left region. + Left, + /// Right region. + Right, + /// Bottom region. + Bottom, +} + /// An event produced during a resize interaction of a [`PaneGrid`]. #[derive(Debug, Clone, Copy)] pub struct ResizeEvent { diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs index 035ef05b..c28ae6e3 100644 --- a/widget/src/pane_grid/content.rs +++ b/widget/src/pane_grid/content.rs @@ -95,7 +95,7 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, ) { use container::StyleSheet; @@ -113,7 +113,7 @@ where let title_bar_layout = children.next().unwrap(); let body_layout = children.next().unwrap(); - let show_controls = bounds.contains(cursor_position); + let show_controls = cursor.is_over(bounds); self.body.as_widget().draw( &tree.children[0], @@ -121,7 +121,7 @@ where theme, style, body_layout, - cursor_position, + cursor, viewport, ); @@ -131,7 +131,7 @@ where theme, style, title_bar_layout, - cursor_position, + cursor, viewport, show_controls, ); @@ -142,7 +142,7 @@ where theme, style, layout, - cursor_position, + cursor, viewport, ); } @@ -218,7 +218,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -233,7 +233,7 @@ where &mut tree.children[1], event.clone(), children.next().unwrap(), - cursor_position, + cursor, renderer, clipboard, shell, @@ -251,7 +251,7 @@ where &mut tree.children[0], event, body_layout, - cursor_position, + cursor, renderer, clipboard, shell, @@ -265,42 +265,48 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, 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, - ); + 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 = cursor + .position() + .map(|cursor_position| { + title_bar + .is_over_pick_area(title_bar_layout, cursor_position) + }) + .unwrap_or_default(); + + if is_over_pick_area && drag_enabled { + return mouse::Interaction::Grab; + } - (children.next().unwrap(), mouse_interaction) - } else { - (layout, mouse::Interaction::default()) - }; + let mouse_interaction = title_bar.mouse_interaction( + &tree.children[1], + title_bar_layout, + cursor, + 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, + cursor, viewport, renderer, ) diff --git a/widget/src/pane_grid/draggable.rs b/widget/src/pane_grid/draggable.rs index a9274dad..9d31feb5 100644 --- a/widget/src/pane_grid/draggable.rs +++ b/widget/src/pane_grid/draggable.rs @@ -4,9 +4,5 @@ use crate::core::{Layout, Point}; 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; + fn can_be_dragged_at(&self, layout: Layout<'_>, cursor: Point) -> bool; } diff --git a/widget/src/pane_grid/state.rs b/widget/src/pane_grid/state.rs index a6e2ec7f..1f034ca3 100644 --- a/widget/src/pane_grid/state.rs +++ b/widget/src/pane_grid/state.rs @@ -2,7 +2,9 @@ //! //! [`PaneGrid`]: crate::widget::PaneGrid use crate::core::{Point, Size}; -use crate::pane_grid::{Axis, Configuration, Direction, Node, Pane, Split}; +use crate::pane_grid::{ + Axis, Configuration, Direction, Node, Pane, Region, Split, +}; use std::collections::HashMap; @@ -165,6 +167,43 @@ impl<T> State<T> { Some((new_pane, new_split)) } + /// Split a target [`Pane`] with a given [`Pane`] on a given [`Region`]. + /// + /// Panes will be swapped by default for [`Region::Center`]. + pub fn split_with(&mut self, target: &Pane, pane: &Pane, region: Region) { + match region { + Region::Center => self.swap(pane, target), + Region::Top => { + self.split_and_swap(Axis::Horizontal, target, pane, true) + } + Region::Bottom => { + self.split_and_swap(Axis::Horizontal, target, pane, false) + } + Region::Left => { + self.split_and_swap(Axis::Vertical, target, pane, true) + } + Region::Right => { + self.split_and_swap(Axis::Vertical, target, pane, false) + } + } + } + + fn split_and_swap( + &mut self, + axis: Axis, + target: &Pane, + pane: &Pane, + swap: bool, + ) { + if let Some((state, _)) = self.close(pane) { + if let Some((new_pane, _)) = self.split(axis, target, state) { + if swap { + self.swap(target, &new_pane); + } + } + } + } + /// Swaps the position of the provided panes in the [`State`]. /// /// If you want to swap panes on drag and drop in your [`PaneGrid`], you diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs index 2129937b..2fe79f80 100644 --- a/widget/src/pane_grid/title_bar.rs +++ b/widget/src/pane_grid/title_bar.rs @@ -122,7 +122,7 @@ where theme: &Renderer::Theme, inherited_style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, show_controls: bool, ) { @@ -158,7 +158,7 @@ where theme, &inherited_style, controls_layout, - cursor_position, + cursor, viewport, ); } @@ -171,7 +171,7 @@ where theme, &inherited_style, title_layout, - cursor_position, + cursor, viewport, ); } @@ -300,7 +300,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -324,7 +324,7 @@ where &mut tree.children[1], event.clone(), controls_layout, - cursor_position, + cursor, renderer, clipboard, shell, @@ -338,7 +338,7 @@ where &mut tree.children[0], event, title_layout, - cursor_position, + cursor, renderer, clipboard, shell, @@ -354,7 +354,7 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { @@ -367,7 +367,7 @@ where let title_interaction = self.content.as_widget().mouse_interaction( &tree.children[0], title_layout, - cursor_position, + cursor, viewport, renderer, ); @@ -377,7 +377,7 @@ where let controls_interaction = controls.as_widget().mouse_interaction( &tree.children[1], controls_layout, - cursor_position, + cursor, viewport, renderer, ); diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index 8c445dda..832aae6b 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -11,8 +11,8 @@ use crate::core::text::{self, Text}; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Clipboard, Element, Layout, Length, Padding, Pixels, Point, Rectangle, - Shell, Size, Widget, + Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, Shell, + Size, Widget, }; use crate::overlay::menu::{self, Menu}; use crate::scrollable; @@ -157,11 +157,11 @@ where From<<Renderer::Theme as StyleSheet>::Style>, { fn tag(&self) -> tree::Tag { - tree::Tag::of::<State<T>>() + tree::Tag::of::<State>() } fn state(&self) -> tree::State { - tree::State::new(State::<T>::new()) + tree::State::new(State::new()) } fn width(&self) -> Length { @@ -196,7 +196,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _renderer: &Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -204,12 +204,12 @@ where update( event, layout, - cursor_position, + cursor, shell, self.on_selected.as_ref(), self.selected.as_ref(), &self.options, - || tree.state.downcast_mut::<State<T>>(), + || tree.state.downcast_mut::<State>(), ) } @@ -217,11 +217,11 @@ where &self, _tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - mouse_interaction(layout, cursor_position) + mouse_interaction(layout, cursor) } fn draw( @@ -231,7 +231,7 @@ where theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, ) { let font = self.font.unwrap_or_else(|| renderer.default_font()); @@ -239,7 +239,7 @@ where renderer, theme, layout, - cursor_position, + cursor, self.padding, self.text_size, self.text_line_height, @@ -249,7 +249,7 @@ where self.selected.as_ref(), &self.handle, &self.style, - || tree.state.downcast_ref::<State<T>>(), + || tree.state.downcast_ref::<State>(), ) } @@ -259,7 +259,7 @@ where layout: Layout<'_>, renderer: &Renderer, ) -> Option<overlay::Element<'b, Message, Renderer>> { - let state = tree.state.downcast_mut::<State<T>>(); + let state = tree.state.downcast_mut::<State>(); overlay( layout, @@ -269,6 +269,7 @@ where self.text_shaping, self.font.unwrap_or_else(|| renderer.default_font()), &self.options, + &self.on_selected, self.style.clone(), ) } @@ -295,15 +296,14 @@ where /// The local state of a [`PickList`]. #[derive(Debug)] -pub struct State<T> { +pub struct State { menu: menu::State, keyboard_modifiers: keyboard::Modifiers, is_open: bool, hovered_option: Option<usize>, - last_selection: Option<T>, } -impl<T> State<T> { +impl State { /// Creates a new [`State`] for a [`PickList`]. pub fn new() -> Self { Self { @@ -311,12 +311,11 @@ impl<T> State<T> { keyboard_modifiers: keyboard::Modifiers::default(), is_open: bool::default(), hovered_option: Option::default(), - last_selection: Option::default(), } } } -impl<T> Default for State<T> { +impl Default for State { fn default() -> Self { Self::new() } @@ -431,12 +430,12 @@ where pub fn update<'a, T, Message>( event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, on_selected: &dyn Fn(T) -> Message, selected: Option<&T>, options: &[T], - state: impl FnOnce() -> &'a mut State<T>, + state: impl FnOnce() -> &'a mut State, ) -> event::Status where T: PartialEq + Clone + 'a, @@ -446,13 +445,13 @@ where | Event::Touch(touch::Event::FingerPressed { .. }) => { let state = state(); - let event_status = if state.is_open { + 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) { + } else if cursor.is_over(layout.bounds()) { state.is_open = true; state.hovered_option = options.iter().position(|option| Some(option) == selected); @@ -460,16 +459,6 @@ where 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 { @@ -478,7 +467,7 @@ where let state = state(); if state.keyboard_modifiers.command() - && layout.bounds().contains(cursor_position) + && cursor.is_over(layout.bounds()) && !state.is_open { fn find_next<'a, T: PartialEq>( @@ -529,10 +518,10 @@ where /// Returns the current [`mouse::Interaction`] of a [`PickList`]. pub fn mouse_interaction( layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, ) -> mouse::Interaction { let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); + let is_mouse_over = cursor.is_over(bounds); if is_mouse_over { mouse::Interaction::Pointer @@ -544,12 +533,13 @@ pub fn mouse_interaction( /// Returns the current overlay of a [`PickList`]. pub fn overlay<'a, T, Message, Renderer>( layout: Layout<'_>, - state: &'a mut State<T>, + state: &'a mut State, padding: Padding, text_size: Option<f32>, text_shaping: text::Shaping, font: Renderer::Font, options: &'a [T], + on_selected: &'a dyn Fn(T) -> Message, style: <Renderer::Theme as StyleSheet>::Style, ) -> Option<overlay::Element<'a, Message, Renderer>> where @@ -570,7 +560,11 @@ where &mut state.menu, options, &mut state.hovered_option, - &mut state.last_selection, + |option| { + state.is_open = false; + + (on_selected)(option) + }, ) .width(bounds.width) .padding(padding) @@ -593,7 +587,7 @@ pub fn draw<'a, T, Renderer>( renderer: &mut Renderer, theme: &Renderer::Theme, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, padding: Padding, text_size: Option<f32>, text_line_height: text::LineHeight, @@ -603,14 +597,14 @@ pub fn draw<'a, T, Renderer>( selected: Option<&T>, handle: &Handle<Renderer::Font>, style: &<Renderer::Theme as StyleSheet>::Style, - state: impl FnOnce() -> &'a State<T>, + 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_mouse_over = cursor.is_over(bounds); let is_selected = selected.is_some(); let style = if is_mouse_over { @@ -624,7 +618,7 @@ pub fn draw<'a, T, Renderer>( bounds, border_color: style.border_color, border_width: style.border_width, - border_radius: style.border_radius.into(), + border_radius: style.border_radius, }, style.background, ); diff --git a/widget/src/progress_bar.rs b/widget/src/progress_bar.rs index ef0d87d5..37c6bc72 100644 --- a/widget/src/progress_bar.rs +++ b/widget/src/progress_bar.rs @@ -1,10 +1,9 @@ //! Provide progress feedback to your users. use crate::core::layout; +use crate::core::mouse; use crate::core::renderer; use crate::core::widget::Tree; -use crate::core::{ - Color, Element, Layout, Length, Point, Rectangle, Size, Widget, -}; +use crate::core::{Color, Element, Layout, Length, Rectangle, Size, Widget}; use std::ops::RangeInclusive; @@ -115,7 +114,7 @@ where theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, - _cursor_position: Point, + _cursor: mouse::Cursor, _viewport: &Rectangle, ) { let bounds = layout.bounds(); @@ -133,7 +132,7 @@ where renderer.fill_quad( renderer::Quad { bounds: Rectangle { ..bounds }, - border_radius: style.border_radius.into(), + border_radius: style.border_radius, border_width: 0.0, border_color: Color::TRANSPARENT, }, @@ -147,7 +146,7 @@ where width: active_progress_width, ..bounds }, - border_radius: style.border_radius.into(), + border_radius: style.border_radius, border_width: 0.0, border_color: Color::TRANSPARENT, }, diff --git a/widget/src/qr_code.rs b/widget/src/qr_code.rs index 7709125f..06be93c0 100644 --- a/widget/src/qr_code.rs +++ b/widget/src/qr_code.rs @@ -1,6 +1,7 @@ //! Encode and display information in a QR code. use crate::canvas; use crate::core::layout; +use crate::core::mouse; use crate::core::renderer::{self, Renderer as _}; use crate::core::widget::Tree; use crate::core::{ @@ -74,7 +75,7 @@ impl<'a, Message, Theme> Widget<Message, Renderer<Theme>> for QRCode<'a> { _theme: &Theme, _style: &renderer::Style, layout: Layout<'_>, - _cursor_position: Point, + _cursor: mouse::Cursor, _viewport: &Rectangle, ) { let bounds = layout.bounds(); diff --git a/widget/src/radio.rs b/widget/src/radio.rs index 9dad1e22..5b883147 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -8,8 +8,8 @@ use crate::core::text; use crate::core::touch; use crate::core::widget::Tree; use crate::core::{ - Alignment, Clipboard, Color, Element, Layout, Length, Pixels, Point, - Rectangle, Shell, Widget, + Alignment, Clipboard, Color, Element, Layout, Length, Pixels, Rectangle, + Shell, Widget, }; use crate::{Row, Text}; @@ -229,7 +229,7 @@ where _state: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _renderer: &Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -237,7 +237,7 @@ where match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { - if layout.bounds().contains(cursor_position) { + if cursor.is_over(layout.bounds()) { shell.publish(self.on_click.clone()); return event::Status::Captured; @@ -253,11 +253,11 @@ where &self, _state: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - if layout.bounds().contains(cursor_position) { + if cursor.is_over(layout.bounds()) { mouse::Interaction::Pointer } else { mouse::Interaction::default() @@ -271,11 +271,10 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, ) { - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); + let is_mouse_over = cursor.is_over(layout.bounds()); let mut children = layout.children(); diff --git a/widget/src/row.rs b/widget/src/row.rs index 3ce363f8..1db22416 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -6,8 +6,8 @@ use crate::core::overlay; use crate::core::renderer; use crate::core::widget::{Operation, Tree}; use crate::core::{ - Alignment, Clipboard, Element, Length, Padding, Pixels, Point, Rectangle, - Shell, Widget, + Alignment, Clipboard, Element, Length, Padding, Pixels, Rectangle, Shell, + Widget, }; /// A container that distributes its contents horizontally. @@ -155,7 +155,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -169,7 +169,7 @@ where state, event.clone(), layout, - cursor_position, + cursor, renderer, clipboard, shell, @@ -182,7 +182,7 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { @@ -192,11 +192,7 @@ where .zip(layout.children()) .map(|((child, state), layout)| { child.as_widget().mouse_interaction( - state, - layout, - cursor_position, - viewport, - renderer, + state, layout, cursor, viewport, renderer, ) }) .max() @@ -210,7 +206,7 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, ) { for ((child, state), layout) in self @@ -219,15 +215,9 @@ where .zip(&tree.children) .zip(layout.children()) { - child.as_widget().draw( - state, - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ); + child + .as_widget() + .draw(state, renderer, theme, style, layout, cursor, viewport); } } diff --git a/widget/src/rule.rs b/widget/src/rule.rs index 3749d7ce..d703e6ae 100644 --- a/widget/src/rule.rs +++ b/widget/src/rule.rs @@ -1,9 +1,10 @@ //! Display a horizontal or vertical rule for dividing content. use crate::core::layout; +use crate::core::mouse; use crate::core::renderer; use crate::core::widget::Tree; use crate::core::{ - Color, Element, Layout, Length, Pixels, Point, Rectangle, Size, Widget, + Color, Element, Layout, Length, Pixels, Rectangle, Size, Widget, }; pub use crate::style::rule::{Appearance, FillMode, StyleSheet}; @@ -86,7 +87,7 @@ where theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, - _cursor_position: Point, + _cursor: mouse::Cursor, _viewport: &Rectangle, ) { let bounds = layout.bounds(); @@ -125,7 +126,7 @@ where renderer.fill_quad( renderer::Quad { bounds, - border_radius: style.radius.into(), + border_radius: style.radius, border_width: 0.0, border_color: Color::TRANSPARENT, }, diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index fd51a6a8..010befac 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -128,9 +128,8 @@ impl Properties { } /// Sets the scrollbar width of the [`Scrollable`] . - /// Silently enforces a minimum width of 1. pub fn width(mut self, width: impl Into<Pixels>) -> Self { - self.width = width.into().0.max(1.0); + self.width = width.into().0.max(0.0); self } @@ -141,9 +140,8 @@ impl Properties { } /// Sets the scroller width of the [`Scrollable`] . - /// Silently enforces a minimum width of 1. pub fn scroller_width(mut self, scroller_width: impl Into<Pixels>) -> Self { - self.scroller_width = scroller_width.into().0.max(1.0); + self.scroller_width = scroller_width.into().0.max(0.0); self } } @@ -224,7 +222,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -233,18 +231,18 @@ where tree.state.downcast_mut::<State>(), event, layout, - cursor_position, + cursor, clipboard, shell, &self.vertical, self.horizontal.as_ref(), &self.on_scroll, - |event, layout, cursor_position, clipboard, shell| { + |event, layout, cursor, clipboard, shell| { self.content.as_widget_mut().on_event( &mut tree.children[0], event, layout, - cursor_position, + cursor, renderer, clipboard, shell, @@ -260,7 +258,7 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, ) { draw( @@ -268,18 +266,18 @@ where renderer, theme, layout, - cursor_position, + cursor, &self.vertical, self.horizontal.as_ref(), &self.style, - |renderer, layout, cursor_position, viewport| { + |renderer, layout, cursor, viewport| { self.content.as_widget().draw( &tree.children[0], renderer, theme, style, layout, - cursor_position, + cursor, viewport, ) }, @@ -290,21 +288,21 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { mouse_interaction( tree.state.downcast_ref::<State>(), layout, - cursor_position, + cursor, &self.vertical, self.horizontal.as_ref(), - |layout, cursor_position, viewport| { + |layout, cursor, viewport| { self.content.as_widget().mouse_interaction( &tree.children[0], layout, - cursor_position, + cursor, viewport, renderer, ) @@ -430,7 +428,7 @@ pub fn update<Message>( state: &mut State, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, vertical: &Properties, @@ -439,13 +437,13 @@ pub fn update<Message>( update_content: impl FnOnce( Event, Layout<'_>, - Point, + mouse::Cursor, &mut dyn Clipboard, &mut Shell<'_, Message>, ) -> event::Status, ) -> event::Status { let bounds = layout.bounds(); - let mouse_over_scrollable = bounds.contains(cursor_position); + let cursor_over_scrollable = cursor.position_over(bounds); let content = layout.children().next().unwrap(); let content_bounds = content.bounds(); @@ -454,28 +452,21 @@ pub fn update<Message>( Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = - scrollbars.is_mouse_over(cursor_position); + scrollbars.is_mouse_over(cursor); 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<Point>` so we can encode - // cursor availability. - // This will probably happen naturally once we add multi-window - // support. - Point::new(-1.0, -1.0) + let cursor = match cursor_over_scrollable { + Some(cursor_position) + if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) => + { + mouse::Cursor::Available( + cursor_position + state.offset(bounds, content_bounds), + ) + } + _ => mouse::Cursor::Unavailable, }; - update_content( - event.clone(), - content, - cursor_position, - clipboard, - shell, - ) + update_content(event.clone(), content, cursor, clipboard, shell) }; if let event::Status::Captured = event_status { @@ -489,76 +480,79 @@ pub fn update<Message>( 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) - }; + match event { + Event::Mouse(mouse::Event::WheelScrolled { delta }) => { + if cursor_over_scrollable.is_none() { + return event::Status::Ignored; + } - movement * 60.0 - } - mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y), - }; + 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) + }; - state.scroll(delta, bounds, content_bounds); + movement * 60.0 + } + mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y), + }; - notify_on_scroll( - state, - on_scroll, - bounds, - content_bounds, - shell, - ); + 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 { .. } => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored + }; + + state.scroll_area_touched_at = Some(cursor_position); + } + touch::Event::FingerMoved { .. } => { + if let Some(scroll_box_touched_at) = + state.scroll_area_touched_at + { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored + }; + + 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); - 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; + + notify_on_scroll( + state, + on_scroll, + bounds, + content_bounds, + shell, + ); } } - - return event::Status::Captured; + 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 { @@ -573,6 +567,10 @@ pub fn update<Message>( Event::Mouse(mouse::Event::CursorMoved { .. }) | Event::Touch(touch::Event::FingerMoved { .. }) => { if let Some(scrollbar) = scrollbars.y { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored + }; + state.scroll_y_to( scrollbar.scroll_percentage_y( scroller_grabbed_at, @@ -599,6 +597,10 @@ pub fn update<Message>( match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored + }; + if let (Some(scroller_grabbed_at), Some(scrollbar)) = (scrollbars.grab_y_scroller(cursor_position), scrollbars.y) { @@ -639,6 +641,10 @@ pub fn update<Message>( } Event::Mouse(mouse::Event::CursorMoved { .. }) | Event::Touch(touch::Event::FingerMoved { .. }) => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored + }; + if let Some(scrollbar) = scrollbars.x { state.scroll_x_to( scrollbar.scroll_percentage_x( @@ -666,6 +672,10 @@ pub fn update<Message>( match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored + }; + if let (Some(scroller_grabbed_at), Some(scrollbar)) = (scrollbars.grab_x_scroller(cursor_position), scrollbars.x) { @@ -702,17 +712,17 @@ pub fn update<Message>( pub fn mouse_interaction( state: &State, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, vertical: &Properties, horizontal: Option<&Properties>, content_interaction: impl FnOnce( Layout<'_>, - Point, + mouse::Cursor, &Rectangle, ) -> mouse::Interaction, ) -> mouse::Interaction { let bounds = layout.bounds(); - let mouse_over_scrollable = bounds.contains(cursor_position); + let cursor_over_scrollable = cursor.position_over(bounds); let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); @@ -721,7 +731,7 @@ pub fn mouse_interaction( Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = - scrollbars.is_mouse_over(cursor_position); + scrollbars.is_mouse_over(cursor); if (mouse_over_x_scrollbar || mouse_over_y_scrollbar) || state.scrollers_grabbed() @@ -730,17 +740,18 @@ pub fn mouse_interaction( } 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) + let cursor = match cursor_over_scrollable { + Some(cursor_position) + if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) => + { + mouse::Cursor::Available(cursor_position + offset) + } + _ => mouse::Cursor::Unavailable, }; content_interaction( content_layout, - cursor_position, + cursor, &Rectangle { y: bounds.y + offset.y, x: bounds.x + offset.x, @@ -756,11 +767,11 @@ pub fn draw<Renderer>( renderer: &mut Renderer, theme: &Renderer::Theme, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, vertical: &Properties, horizontal: Option<&Properties>, style: &<Renderer::Theme as StyleSheet>::Style, - draw_content: impl FnOnce(&mut Renderer, Layout<'_>, Point, &Rectangle), + draw_content: impl FnOnce(&mut Renderer, Layout<'_>, mouse::Cursor, &Rectangle), ) where Renderer: crate::core::Renderer, Renderer::Theme: StyleSheet, @@ -772,18 +783,19 @@ pub fn draw<Renderer>( let scrollbars = Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); - let mouse_over_scrollable = bounds.contains(cursor_position); + let cursor_over_scrollable = cursor.position_over(bounds); let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = - scrollbars.is_mouse_over(cursor_position); + scrollbars.is_mouse_over(cursor); 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) + let cursor = match cursor_over_scrollable { + Some(cursor_position) + if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) => + { + mouse::Cursor::Available(cursor_position + offset) + } + _ => mouse::Cursor::Unavailable, }; // Draw inner content @@ -795,7 +807,7 @@ pub fn draw<Renderer>( draw_content( renderer, content_layout, - cursor_position, + cursor, &Rectangle { y: bounds.y + offset.y, x: bounds.x + offset.x, @@ -811,14 +823,16 @@ pub fn draw<Renderer>( style: Scrollbar, scrollbar: &internals::Scrollbar| { //track - if style.background.is_some() - || (style.border_color != Color::TRANSPARENT - && style.border_width > 0.0) + if scrollbar.bounds.width > 0.0 + && scrollbar.bounds.height > 0.0 + && (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_radius: style.border_radius, border_width: style.border_width, border_color: style.border_color, }, @@ -829,14 +843,16 @@ pub fn draw<Renderer>( } //thumb - if style.scroller.color != Color::TRANSPARENT - || (style.scroller.border_color != Color::TRANSPARENT - && style.scroller.border_width > 0.0) + if scrollbar.scroller.bounds.width > 0.0 + && scrollbar.scroller.bounds.height > 0.0 + && (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_radius: style.scroller.border_radius, border_width: style.scroller.border_width, border_color: style.scroller.border_color, }, @@ -856,7 +872,7 @@ pub fn draw<Renderer>( if let Some(scrollbar) = scrollbars.y { let style = if state.y_scroller_grabbed_at.is_some() { theme.dragging(style) - } else if mouse_over_scrollable { + } else if cursor_over_scrollable.is_some() { theme.hovered(style, mouse_over_y_scrollbar) } else { theme.active(style) @@ -869,7 +885,7 @@ pub fn draw<Renderer>( if let Some(scrollbar) = scrollbars.x { let style = if state.x_scroller_grabbed_at.is_some() { theme.dragging_horizontal(style) - } else if mouse_over_scrollable { + } else if cursor_over_scrollable.is_some() { theme.hovered_horizontal(style, mouse_over_x_scrollbar) } else { theme.active_horizontal(style) @@ -883,7 +899,7 @@ pub fn draw<Renderer>( draw_content( renderer, content_layout, - cursor_position, + cursor, &Rectangle { x: bounds.x + offset.x, y: bounds.y + offset.y, @@ -1281,17 +1297,21 @@ impl Scrollbars { } } - 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 is_mouse_over(&self, cursor: mouse::Cursor) -> (bool, bool) { + if let Some(cursor_position) = cursor.position() { + ( + 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), + ) + } else { + (false, false) + } } fn grab_y_scroller(&self, cursor_position: Point) -> Option<f32> { diff --git a/widget/src/slider.rs b/widget/src/slider.rs index 18a49665..3ea4391b 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -183,7 +183,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _renderer: &Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -191,7 +191,7 @@ where update( event, layout, - cursor_position, + cursor, shell, tree.state.downcast_mut::<State>(), &mut self.value, @@ -209,13 +209,13 @@ where theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, ) { draw( renderer, layout, - cursor_position, + cursor, tree.state.downcast_ref::<State>(), self.value, &self.range, @@ -228,15 +228,11 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - mouse_interaction( - layout, - cursor_position, - tree.state.downcast_ref::<State>(), - ) + mouse_interaction(layout, cursor, tree.state.downcast_ref::<State>()) } } @@ -260,7 +256,7 @@ where pub fn update<Message, T>( event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, state: &mut State, value: &mut T, @@ -275,7 +271,7 @@ where { let is_dragging = state.is_dragging; - let mut change = || { + let mut change = |cursor_position: Point| { let bounds = layout.bounds(); let new_value = if cursor_position.x <= bounds.x { *range.start() @@ -309,8 +305,9 @@ where match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { - if layout.bounds().contains(cursor_position) { - change(); + if let Some(cursor_position) = cursor.position_over(layout.bounds()) + { + change(cursor_position); state.is_dragging = true; return event::Status::Captured; @@ -331,7 +328,7 @@ where Event::Mouse(mouse::Event::CursorMoved { .. }) | Event::Touch(touch::Event::FingerMoved { .. }) => { if is_dragging { - change(); + let _ = cursor.position().map(change); return event::Status::Captured; } @@ -346,7 +343,7 @@ where pub fn draw<T, R>( renderer: &mut R, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, state: &State, value: T, range: &RangeInclusive<T>, @@ -358,7 +355,7 @@ pub fn draw<T, R>( R::Theme: StyleSheet, { let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); + let is_mouse_over = cursor.is_over(bounds); let style = if state.is_dragging { style_sheet.dragging(style) @@ -368,16 +365,16 @@ pub fn draw<T, R>( style_sheet.active(style) }; - let (handle_width, handle_height, handle_border_radius) = match style - .handle - .shape - { - HandleShape::Circle { radius } => (radius * 2.0, radius * 2.0, radius), - HandleShape::Rectangle { - width, - border_radius, - } => (f32::from(width), bounds.height, border_radius), - }; + let (handle_width, handle_height, handle_border_radius) = + match style.handle.shape { + HandleShape::Circle { radius } => { + (radius * 2.0, radius * 2.0, radius.into()) + } + HandleShape::Rectangle { + width, + border_radius, + } => (f32::from(width), bounds.height, border_radius), + }; let value = value.into() as f32; let (range_start, range_end) = { @@ -403,7 +400,7 @@ pub fn draw<T, R>( width: offset + handle_width / 2.0, height: style.rail.width, }, - border_radius: Default::default(), + border_radius: style.rail.border_radius, border_width: 0.0, border_color: Color::TRANSPARENT, }, @@ -418,7 +415,7 @@ pub fn draw<T, R>( width: bounds.width - offset - handle_width / 2.0, height: style.rail.width, }, - border_radius: Default::default(), + border_radius: style.rail.border_radius, border_width: 0.0, border_color: Color::TRANSPARENT, }, @@ -433,7 +430,7 @@ pub fn draw<T, R>( width: handle_width, height: handle_height, }, - border_radius: handle_border_radius.into(), + border_radius: handle_border_radius, border_width: style.handle.border_width, border_color: style.handle.border_color, }, @@ -444,11 +441,11 @@ pub fn draw<T, R>( /// Computes the current [`mouse::Interaction`] of a [`Slider`]. pub fn mouse_interaction( layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, state: &State, ) -> mouse::Interaction { let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); + let is_mouse_over = cursor.is_over(bounds); if state.is_dragging { mouse::Interaction::Grabbing diff --git a/widget/src/space.rs b/widget/src/space.rs index e1e09d5a..9a5385e8 100644 --- a/widget/src/space.rs +++ b/widget/src/space.rs @@ -1,9 +1,10 @@ //! Distribute content vertically. use crate::core; use crate::core::layout; +use crate::core::mouse; use crate::core::renderer; use crate::core::widget::Tree; -use crate::core::{Element, Layout, Length, Point, Rectangle, Size, Widget}; +use crate::core::{Element, Layout, Length, Rectangle, Size, Widget}; /// An amount of empty space. /// @@ -69,7 +70,7 @@ where _theme: &Renderer::Theme, _style: &renderer::Style, _layout: Layout<'_>, - _cursor_position: Point, + _cursor: mouse::Cursor, _viewport: &Rectangle, ) { } diff --git a/widget/src/svg.rs b/widget/src/svg.rs index 89017fcf..1ccc5d62 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -1,10 +1,11 @@ //! Display vector graphics in your application. use crate::core::layout; +use crate::core::mouse; use crate::core::renderer; use crate::core::svg; use crate::core::widget::Tree; use crate::core::{ - ContentFit, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget, + ContentFit, Element, Layout, Length, Rectangle, Size, Vector, Widget, }; use std::path::PathBuf; @@ -143,7 +144,7 @@ where theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, - _cursor_position: Point, + _cursor: mouse::Cursor, _viewport: &Rectangle, ) { let Size { width, height } = renderer.dimensions(&self.handle); diff --git a/widget/src/text.rs b/widget/src/text.rs index 04c31edc..ce4f44bd 100644 --- a/widget/src/text.rs +++ b/widget/src/text.rs @@ -1,4 +1,6 @@ +//! Draw and interact with text. pub use crate::core::widget::text::*; +/// A paragraph. pub type Text<'a, Renderer = crate::Renderer> = crate::core::widget::Text<'a, Renderer>; diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index bbc07dac..272263f9 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -207,14 +207,14 @@ where renderer: &mut Renderer, theme: &Renderer::Theme, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, value: Option<&Value>, ) { draw( renderer, theme, layout, - cursor_position, + cursor, tree.state.downcast_ref::<State>(), value.unwrap_or(&self.value), &self.placeholder, @@ -298,7 +298,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -306,7 +306,7 @@ where update( event, layout, - cursor_position, + cursor, renderer, clipboard, shell, @@ -329,14 +329,14 @@ where theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, ) { draw( renderer, theme, layout, - cursor_position, + cursor, tree.state.downcast_ref::<State>(), &self.value, &self.placeholder, @@ -354,11 +354,11 @@ where &self, _state: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - mouse_interaction(layout, cursor_position, self.on_input.is_none()) + mouse_interaction(layout, cursor, self.on_input.is_none()) } } @@ -528,7 +528,7 @@ where pub fn update<'a, Message, Renderer>( event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -550,10 +550,14 @@ where Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { let state = state(); - let is_clicked = - layout.bounds().contains(cursor_position) && on_input.is_some(); - state.is_focused = if is_clicked { + let click_position = if on_input.is_some() { + cursor.position_over(layout.bounds()) + } else { + None + }; + + state.is_focused = if click_position.is_some() { state.is_focused.or_else(|| { let now = Instant::now(); @@ -566,7 +570,7 @@ where None }; - if is_clicked { + if let Some(cursor_position) = click_position { let text_layout = layout.children().next().unwrap(); let target = cursor_position.x - text_layout.bounds().x; @@ -944,7 +948,7 @@ pub fn draw<Renderer>( renderer: &mut Renderer, theme: &Renderer::Theme, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, state: &State, value: &Value, placeholder: &str, @@ -967,7 +971,7 @@ pub fn draw<Renderer>( let mut children_layout = layout.children(); let text_bounds = children_layout.next().unwrap().bounds(); - let is_mouse_over = bounds.contains(cursor_position); + let is_mouse_over = cursor.is_over(bounds); let appearance = if is_disabled { theme.disabled(style) @@ -982,7 +986,7 @@ pub fn draw<Renderer>( renderer.fill_quad( renderer::Quad { bounds, - border_radius: appearance.border_radius.into(), + border_radius: appearance.border_radius, border_width: appearance.border_width, border_color: appearance.border_color, }, @@ -1154,10 +1158,10 @@ pub fn draw<Renderer>( /// Computes the current [`mouse::Interaction`] of the [`TextInput`]. pub fn mouse_interaction( layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, is_disabled: bool, ) -> mouse::Interaction { - if layout.bounds().contains(cursor_position) { + if cursor.is_over(layout.bounds()) { if is_disabled { mouse::Interaction::NotAllowed } else { diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index b1ba65c9..8b51f2d0 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -7,8 +7,8 @@ use crate::core::renderer; use crate::core::text; use crate::core::widget::Tree; use crate::core::{ - Alignment, Clipboard, Element, Event, Layout, Length, Pixels, Point, - Rectangle, Shell, Widget, + Alignment, Clipboard, Element, Event, Layout, Length, Pixels, Rectangle, + Shell, Widget, }; use crate::{Row, Text}; @@ -202,14 +202,14 @@ where _state: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _renderer: &Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> event::Status { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { - let mouse_over = layout.bounds().contains(cursor_position); + let mouse_over = cursor.is_over(layout.bounds()); if mouse_over { shell.publish((self.on_toggle)(!self.is_toggled)); @@ -227,11 +227,11 @@ where &self, _state: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - if layout.bounds().contains(cursor_position) { + if cursor.is_over(layout.bounds()) { mouse::Interaction::Pointer } else { mouse::Interaction::default() @@ -245,7 +245,7 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, ) { /// Makes sure that the border radius of the toggler looks good at every size. @@ -278,7 +278,7 @@ where let toggler_layout = children.next().unwrap(); let bounds = toggler_layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); + let is_mouse_over = cursor.is_over(layout.bounds()); let style = if is_mouse_over { theme.hovered(&self.style, self.is_toggled) diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs index 084650d1..d425de01 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -9,7 +9,7 @@ use crate::core::renderer; use crate::core::text; use crate::core::widget::Tree; use crate::core::{ - Clipboard, Element, Length, Padding, Pixels, Point, Rectangle, Shell, Size, + Clipboard, Element, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget, }; use crate::Text; @@ -136,7 +136,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -145,7 +145,7 @@ where &mut tree.children[0], event, layout, - cursor_position, + cursor, renderer, clipboard, shell, @@ -156,14 +156,14 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { self.content.as_widget().mouse_interaction( &tree.children[0], layout, - cursor_position, + cursor, viewport, renderer, ) @@ -176,7 +176,7 @@ where theme: &Renderer::Theme, inherited_style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, ) { self.content.as_widget().draw( @@ -185,7 +185,7 @@ where theme, inherited_style, layout, - cursor_position, + cursor, viewport, ); @@ -196,7 +196,7 @@ where theme, inherited_style, layout, - cursor_position, + cursor, viewport, self.position, self.gap, @@ -206,7 +206,7 @@ where |renderer, limits| { Widget::<(), Renderer>::layout(tooltip, renderer, limits) }, - |renderer, defaults, layout, cursor_position, viewport| { + |renderer, defaults, layout, viewport| { Widget::<(), Renderer>::draw( tooltip, &Tree::empty(), @@ -214,7 +214,7 @@ where theme, defaults, layout, - cursor_position, + cursor, viewport, ); }, @@ -270,7 +270,7 @@ pub fn draw<Renderer>( theme: &Renderer::Theme, inherited_style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, position: Position, gap: f32, @@ -278,13 +278,7 @@ pub fn draw<Renderer>( snap_within_viewport: bool, style: &<Renderer::Theme as container::StyleSheet>::Style, layout_text: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, - draw_text: impl FnOnce( - &mut Renderer, - &renderer::Style, - Layout<'_>, - Point, - &Rectangle, - ), + draw_text: impl FnOnce(&mut Renderer, &renderer::Style, Layout<'_>, &Rectangle), ) where Renderer: core::Renderer, Renderer::Theme: container::StyleSheet, @@ -293,7 +287,7 @@ pub fn draw<Renderer>( let bounds = layout.bounds(); - if bounds.contains(cursor_position) { + if let Some(cursor_position) = cursor.position_over(bounds) { let style = theme.appearance(style); let defaults = renderer::Style { @@ -380,7 +374,6 @@ pub fn draw<Renderer>( ), &text_layout, ), - cursor_position, viewport, ) }); diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index 2635611d..91f2b466 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -180,7 +180,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _renderer: &Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -188,7 +188,7 @@ where update( event, layout, - cursor_position, + cursor, shell, tree.state.downcast_mut::<State>(), &mut self.value, @@ -206,13 +206,13 @@ where theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, ) { draw( renderer, layout, - cursor_position, + cursor, tree.state.downcast_ref::<State>(), self.value, &self.range, @@ -225,15 +225,11 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - mouse_interaction( - layout, - cursor_position, - tree.state.downcast_ref::<State>(), - ) + mouse_interaction(layout, cursor, tree.state.downcast_ref::<State>()) } } @@ -257,7 +253,7 @@ where pub fn update<Message, T>( event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, state: &mut State, value: &mut T, @@ -272,8 +268,9 @@ where { let is_dragging = state.is_dragging; - let mut change = || { + let mut change = |cursor_position: Point| { let bounds = layout.bounds(); + let new_value = if cursor_position.y >= bounds.y + bounds.height { *range.start() } else if cursor_position.y <= bounds.y { @@ -307,8 +304,9 @@ where match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { - if layout.bounds().contains(cursor_position) { - change(); + if let Some(cursor_position) = cursor.position_over(layout.bounds()) + { + change(cursor_position); state.is_dragging = true; return event::Status::Captured; @@ -329,7 +327,7 @@ where Event::Mouse(mouse::Event::CursorMoved { .. }) | Event::Touch(touch::Event::FingerMoved { .. }) => { if is_dragging { - change(); + let _ = cursor.position().map(change); return event::Status::Captured; } @@ -344,7 +342,7 @@ where pub fn draw<T, R>( renderer: &mut R, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, state: &State, value: T, range: &RangeInclusive<T>, @@ -356,7 +354,7 @@ pub fn draw<T, R>( R::Theme: StyleSheet, { let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); + let is_mouse_over = cursor.is_over(bounds); let style = if state.is_dragging { style_sheet.dragging(style) @@ -366,16 +364,16 @@ pub fn draw<T, R>( style_sheet.active(style) }; - let (handle_width, handle_height, handle_border_radius) = match style - .handle - .shape - { - HandleShape::Circle { radius } => (radius * 2.0, radius * 2.0, radius), - HandleShape::Rectangle { - width, - border_radius, - } => (f32::from(width), bounds.width, border_radius), - }; + let (handle_width, handle_height, handle_border_radius) = + match style.handle.shape { + HandleShape::Circle { radius } => { + (radius * 2.0, radius * 2.0, radius.into()) + } + HandleShape::Rectangle { + width, + border_radius, + } => (f32::from(width), bounds.width, border_radius), + }; let value = value.into() as f32; let (range_start, range_end) = { @@ -401,7 +399,7 @@ pub fn draw<T, R>( width: style.rail.width, height: offset + handle_width / 2.0, }, - border_radius: Default::default(), + border_radius: style.rail.border_radius, border_width: 0.0, border_color: Color::TRANSPARENT, }, @@ -416,7 +414,7 @@ pub fn draw<T, R>( width: style.rail.width, height: bounds.height - offset - handle_width / 2.0, }, - border_radius: Default::default(), + border_radius: style.rail.border_radius, border_width: 0.0, border_color: Color::TRANSPARENT, }, @@ -431,7 +429,7 @@ pub fn draw<T, R>( width: handle_height, height: handle_width, }, - border_radius: handle_border_radius.into(), + border_radius: handle_border_radius, border_width: style.handle.border_width, border_color: style.handle.border_color, }, @@ -442,11 +440,11 @@ pub fn draw<T, R>( /// Computes the current [`mouse::Interaction`] of a [`VerticalSlider`]. pub fn mouse_interaction( layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, state: &State, ) -> mouse::Interaction { let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); + let is_mouse_over = cursor.is_over(bounds); if state.is_dragging { mouse::Interaction::Grabbing diff --git a/winit/Cargo.toml b/winit/Cargo.toml index 58e13b3e..de7c1c62 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -23,14 +23,15 @@ wayland-dlopen = ["winit/wayland-dlopen"] wayland-csd-adwaita = ["winit/wayland-csd-adwaita"] [dependencies] -window_clipboard = "0.2" +window_clipboard = "0.3" log = "0.4" thiserror = "1.0" +raw-window-handle = "0.5" [dependencies.winit] -version = "0.27" +version = "0.28" git = "https://github.com/iced-rs/winit.git" -rev = "940457522e9fb9f5dac228b0ecfafe0138b4048c" +rev = "c52db2045d0a2f1b8d9923870de1d4ab1994146e" default-features = false [dependencies.iced_runtime] diff --git a/winit/src/application.rs b/winit/src/application.rs index 3d7c6e5d..6e7b94ef 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -308,6 +308,8 @@ async fn run_instance<A, E, C>( run_command( &application, + &mut compositor, + &mut surface, &mut cache, &state, &mut renderer, @@ -318,7 +320,6 @@ async fn run_instance<A, E, C>( &mut proxy, &mut debug, &window, - || compositor.fetch_information(), ); runtime.track(application.subscription().into_recipes()); @@ -356,7 +357,7 @@ async fn run_instance<A, E, C>( let (interface_state, statuses) = user_interface.update( &events, - state.cursor_position(), + state.cursor(), &mut renderer, &mut clipboard, &mut messages, @@ -382,6 +383,8 @@ async fn run_instance<A, E, C>( // Update application update( &mut application, + &mut compositor, + &mut surface, &mut cache, &state, &mut renderer, @@ -392,7 +395,6 @@ async fn run_instance<A, E, C>( &mut debug, &mut messages, &window, - || compositor.fetch_information(), ); // Update window @@ -422,7 +424,7 @@ async fn run_instance<A, E, C>( let (interface_state, _) = user_interface.update( &[redraw_event.clone()], - state.cursor_position(), + state.cursor(), &mut renderer, &mut clipboard, &mut messages, @@ -435,7 +437,7 @@ async fn run_instance<A, E, C>( &renderer::Style { text_color: state.text_color(), }, - state.cursor_position(), + state.cursor(), ); debug.draw_finished(); @@ -508,7 +510,7 @@ async fn run_instance<A, E, C>( &renderer::Style { text_color: state.text_color(), }, - state.cursor_position(), + state.cursor(), ); if new_mouse_interaction != mouse_interaction { @@ -645,8 +647,10 @@ where /// Updates an [`Application`] by feeding it the provided messages, spawning any /// resulting [`Command`], and tracking its [`Subscription`]. -pub fn update<A: Application, E: Executor>( +pub fn update<A: Application, C, E: Executor>( application: &mut A, + compositor: &mut C, + surface: &mut C::Surface, cache: &mut user_interface::Cache, state: &State<A>, renderer: &mut A::Renderer, @@ -657,8 +661,8 @@ pub fn update<A: Application, E: Executor>( debug: &mut Debug, messages: &mut Vec<A::Message>, window: &winit::window::Window, - graphics_info: impl FnOnce() -> compositor::Information + Copy, ) where + C: Compositor<Renderer = A::Renderer> + 'static, <A::Renderer as core::Renderer>::Theme: StyleSheet, { for message in messages.drain(..) { @@ -676,6 +680,8 @@ pub fn update<A: Application, E: Executor>( run_command( application, + compositor, + surface, cache, state, renderer, @@ -686,7 +692,6 @@ pub fn update<A: Application, E: Executor>( proxy, debug, window, - graphics_info, ); } @@ -695,8 +700,10 @@ pub fn update<A: Application, E: Executor>( } /// Runs the actions of a [`Command`]. -pub fn run_command<A, E>( +pub fn run_command<A, C, E>( application: &A, + compositor: &mut C, + surface: &mut C::Surface, cache: &mut user_interface::Cache, state: &State<A>, renderer: &mut A::Renderer, @@ -707,10 +714,10 @@ pub fn run_command<A, E>( proxy: &mut winit::event_loop::EventLoopProxy<A::Message>, debug: &mut Debug, window: &winit::window::Window, - _graphics_info: impl FnOnce() -> compositor::Information + Copy, ) where A: Application, E: Executor, + C: Compositor<Renderer = A::Renderer> + 'static, <A::Renderer as core::Renderer>::Theme: StyleSheet, { use crate::runtime::command; @@ -794,20 +801,36 @@ pub fn run_command<A, E>( window::Action::GainFocus => { window.focus_window(); } - window::Action::ChangeAlwaysOnTop(on_top) => { - window.set_always_on_top(on_top); + window::Action::ChangeLevel(level) => { + window.set_window_level(conversion::window_level(level)); } window::Action::FetchId(tag) => { proxy .send_event(tag(window.id().into())) .expect("Send message to event loop"); } + window::Action::Screenshot(tag) => { + let bytes = compositor.screenshot( + renderer, + surface, + state.viewport(), + state.background_color(), + &debug.overlay(), + ); + + proxy + .send_event(tag(window::Screenshot::new( + bytes, + state.physical_size(), + ))) + .expect("Send message to event loop.") + } }, command::Action::System(action) => match action { system::Action::QueryInformation(_tag) => { #[cfg(feature = "system")] { - let graphics_info = _graphics_info(); + let graphics_info = compositor.fetch_information(); let proxy = proxy.clone(); let _ = std::thread::spawn(move || { diff --git a/winit/src/application/state.rs b/winit/src/application/state.rs index c37ccca6..967f43f2 100644 --- a/winit/src/application/state.rs +++ b/winit/src/application/state.rs @@ -1,7 +1,8 @@ use crate::application::{self, StyleSheet as _}; use crate::conversion; use crate::core; -use crate::core::{Color, Point, Size}; +use crate::core::mouse; +use crate::core::{Color, Size}; use crate::graphics::Viewport; use crate::runtime::Debug; use crate::Application; @@ -20,7 +21,7 @@ where scale_factor: f64, viewport: Viewport, viewport_version: usize, - cursor_position: winit::dpi::PhysicalPosition<f64>, + cursor_position: Option<winit::dpi::PhysicalPosition<f64>>, modifiers: winit::event::ModifiersState, theme: <A::Renderer as core::Renderer>::Theme, appearance: application::Appearance, @@ -52,8 +53,7 @@ where scale_factor, viewport, viewport_version: 0, - // TODO: Encode cursor availability in the type-system - cursor_position: winit::dpi::PhysicalPosition::new(-1.0, -1.0), + cursor_position: None, modifiers: winit::event::ModifiersState::default(), theme, appearance, @@ -89,11 +89,16 @@ where } /// Returns the current cursor position of the [`State`]. - pub fn cursor_position(&self) -> Point { - conversion::cursor_position( - self.cursor_position, - self.viewport.scale_factor(), - ) + pub fn cursor(&self) -> mouse::Cursor { + self.cursor_position + .map(|cursor_position| { + conversion::cursor_position( + cursor_position, + self.viewport.scale_factor(), + ) + }) + .map(mouse::Cursor::Available) + .unwrap_or(mouse::Cursor::Unavailable) } /// Returns the current keyboard modifiers of the [`State`]. @@ -153,12 +158,10 @@ where | WindowEvent::Touch(Touch { location: position, .. }) => { - self.cursor_position = *position; + self.cursor_position = Some(*position); } WindowEvent::CursorLeft { .. } => { - // TODO: Encode cursor availability in the type-system - self.cursor_position = - winit::dpi::PhysicalPosition::new(-1.0, -1.0); + self.cursor_position = None; } WindowEvent::ModifiersChanged(new_modifiers) => { self.modifiers = *new_modifiers; diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index a9262184..dcae7074 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -140,6 +140,19 @@ pub fn window_event( } } +/// Converts a [`window::Level`] to a [`winit`] window level. +/// +/// [`winit`]: https://github.com/rust-windowing/winit +pub fn window_level(level: window::Level) -> winit::window::WindowLevel { + match level { + window::Level::Normal => winit::window::WindowLevel::Normal, + window::Level::AlwaysOnBottom => { + winit::window::WindowLevel::AlwaysOnBottom + } + window::Level::AlwaysOnTop => winit::window::WindowLevel::AlwaysOnTop, + } +} + /// Converts a [`Position`] to a [`winit`] logical position for a given monitor. /// /// [`winit`]: https://github.com/rust-windowing/winit @@ -250,9 +263,7 @@ pub fn mouse_button(mouse_button: winit::event::MouseButton) -> mouse::Button { winit::event::MouseButton::Left => mouse::Button::Left, winit::event::MouseButton::Right => mouse::Button::Right, winit::event::MouseButton::Middle => mouse::Button::Middle, - winit::event::MouseButton::Other(other) => { - mouse::Button::Other(other as u8) - } + winit::event::MouseButton::Other(other) => mouse::Button::Other(other), } } diff --git a/winit/src/lib.rs b/winit/src/lib.rs index 9f6bcebb..4776ea2c 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -25,11 +25,12 @@ clippy::from_over_into, clippy::needless_borrow, clippy::new_without_default, - clippy::useless_conversion + clippy::useless_conversion, + unsafe_code )] -#![forbid(rust_2018_idioms, unsafe_code)] +#![forbid(rust_2018_idioms)] #![allow(clippy::inherent_to_string, clippy::type_complexity)] -#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] pub use iced_graphics as graphics; pub use iced_runtime as runtime; pub use iced_runtime::core; diff --git a/winit/src/settings.rs b/winit/src/settings.rs index be0ab329..40b3d487 100644 --- a/winit/src/settings.rs +++ b/winit/src/settings.rs @@ -22,7 +22,7 @@ mod platform; pub use platform::PlatformSpecific; use crate::conversion; -use crate::core::window::Icon; +use crate::core::window::{Icon, Level}; use crate::Position; use winit::monitor::MonitorHandle; @@ -81,8 +81,8 @@ pub struct Window { /// Whether the window should be transparent. pub transparent: bool, - /// Whether the window will always be on top of other windows. - pub always_on_top: bool, + /// The window [`Level`]. + pub level: Level, /// The window icon, which is also usually used in the taskbar pub icon: Option<Icon>, @@ -102,7 +102,7 @@ impl fmt::Debug for Window { .field("resizable", &self.resizable) .field("decorations", &self.decorations) .field("transparent", &self.transparent) - .field("always_on_top", &self.always_on_top) + .field("level", &self.level) .field("icon", &self.icon.is_some()) .field("platform_specific", &self.platform_specific) .finish() @@ -128,7 +128,7 @@ impl Window { .with_decorations(self.decorations) .with_transparent(self.transparent) .with_window_icon(self.icon.and_then(conversion::icon)) - .with_always_on_top(self.always_on_top) + .with_window_level(conversion::window_level(self.level)) .with_visible(self.visible); if let Some(position) = conversion::position( @@ -157,7 +157,9 @@ impl Window { target_os = "openbsd" ))] { - use ::winit::platform::unix::WindowBuilderExtUnix; + // `with_name` is available on both `WindowBuilderExtWayland` and `WindowBuilderExtX11` and they do + // exactly the same thing. We arbitrarily choose `WindowBuilderExtWayland` here. + use ::winit::platform::wayland::WindowBuilderExtWayland; if let Some(id) = _id { window_builder = window_builder.with_name(id.clone(), id); @@ -167,11 +169,11 @@ impl Window { #[cfg(target_os = "windows")] { use winit::platform::windows::WindowBuilderExtWindows; - - if let Some(parent) = self.platform_specific.parent { - window_builder = window_builder.with_parent_window(parent); + #[allow(unsafe_code)] + unsafe { + window_builder = window_builder + .with_parent_window(self.platform_specific.parent); } - window_builder = window_builder .with_drag_and_drop(self.platform_specific.drag_and_drop); } @@ -205,7 +207,7 @@ impl Default for Window { resizable: true, decorations: true, transparent: false, - always_on_top: false, + level: Level::default(), icon: None, platform_specific: Default::default(), } diff --git a/winit/src/settings/windows.rs b/winit/src/settings/windows.rs index ff03a9c5..45d753bd 100644 --- a/winit/src/settings/windows.rs +++ b/winit/src/settings/windows.rs @@ -1,10 +1,11 @@ //! Platform specific settings for Windows. +use raw_window_handle::RawWindowHandle; /// The platform specific window settings of an application. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct PlatformSpecific { /// Parent window - pub parent: Option<winit::platform::windows::HWND>, + pub parent: Option<RawWindowHandle>, /// Drag and drop support pub drag_and_drop: bool, |