diff options
215 files changed, 8903 insertions, 3484 deletions
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 520d8da9..9e73d3d3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,3 +29,5 @@ jobs: run: cargo check --package iced --target wasm32-unknown-unknown - name: Check compilation of `tour` example run: cargo build --package tour --target wasm32-unknown-unknown + - name: Check compilation of `pokedex` example + run: cargo build --package pokedex --target wasm32-unknown-unknown diff --git a/CHANGELOG.md b/CHANGELOG.md index 59471abc..ee650337 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- `"system_font"` feature gates reading system fonts. [#370] + +[#370]: https://github.com/hecrj/iced/pull/370 ## [0.1.1] - 2020-04-15 ### Added @@ -12,18 +12,31 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"] categories = ["gui"] [features] +default = ["wgpu", "default_system_font"] +# Enables the `iced_wgpu` renderer +wgpu = ["iced_wgpu"] # Enables the `Image` widget image = ["iced_wgpu/image"] # Enables the `Svg` widget svg = ["iced_wgpu/svg"] # Enables the `Canvas` widget canvas = ["iced_wgpu/canvas"] +# Enables using system fonts. +default_system_font = ["iced_wgpu/default_system_font"] +# Enables the `iced_glow` renderer. Overrides `iced_wgpu` +glow = ["iced_glow", "iced_glutin"] +# Enables the `Canvas` widget for `iced_glow` +glow_canvas = ["iced_glow/canvas"] +# Enables using system fonts for `iced_glow`. +glow_default_system_font = ["iced_glow/default_system_font"] # Enables a debug view in native platforms (press F12) debug = ["iced_winit/debug"] # Enables `tokio` as the `executor::Default` on native platforms tokio = ["iced_futures/tokio"] # Enables `async-std` as the `executor::Default` on native platforms async-std = ["iced_futures/async-std"] +# Enables advanced color conversion via `palette` +palette = ["iced_core/palette"] [badges] maintenance = { status = "actively-developed" } @@ -32,6 +45,9 @@ maintenance = { status = "actively-developed" } members = [ "core", "futures", + "graphics", + "glow", + "glutin", "native", "style", "web", @@ -39,10 +55,12 @@ members = [ "winit", "examples/bezier_tool", "examples/clock", + "examples/color_palette", "examples/counter", "examples/custom_widget", "examples/download_progress", "examples/events", + "examples/game_of_life", "examples/geometry", "examples/integration", "examples/pane_grid", @@ -57,11 +75,14 @@ members = [ ] [dependencies] +iced_core = { version = "0.2", path = "core" } iced_futures = { version = "0.1", path = "futures" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] iced_winit = { version = "0.1", path = "winit" } -iced_wgpu = { version = "0.2", path = "wgpu" } +iced_glutin = { version = "0.1", path = "glutin", optional = true } +iced_wgpu = { version = "0.2", path = "wgpu", optional = true } +iced_glow = { version = "0.1", path = "glow", optional = true} [target.'cfg(target_arch = "wasm32")'.dependencies] iced_web = { version = "0.2", path = "web" } @@ -13,7 +13,7 @@ Once a step is completed, it is collapsed and added to this list: * [x] Scrollables / Clippables ([#24]) * [x] Text input widget ([#25]) * [x] TodoMVC example ([#26]) - * [x] Async actions ([#27]) + * [x] Async actions ([#28]) * [x] Custom layout engine ([#52]) * [x] Event subscriptions ([#122]) * [x] Custom styling ([#146]) diff --git a/core/Cargo.toml b/core/Cargo.toml index 837f6aae..b52bf315 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -8,3 +8,7 @@ license = "MIT" repository = "https://github.com/hecrj/iced" [dependencies] + +[dependencies.palette] +version = "0.5.0" +optional = true diff --git a/core/src/color.rs b/core/src/color.rs index db509b88..a4c3d87c 100644 --- a/core/src/color.rs +++ b/core/src/color.rs @@ -1,10 +1,16 @@ +#[cfg(feature = "palette")] +use palette::rgb::{Srgb, Srgba}; + /// A color in the sRGB color space. -#[derive(Debug, Clone, Copy, PartialEq)] -#[allow(missing_docs)] +#[derive(Debug, Clone, Copy, PartialEq, Default)] pub struct Color { + /// Red component, 0.0 - 1.0 pub r: f32, + /// Green component, 0.0 - 1.0 pub g: f32, + /// Blue component, 0.0 - 1.0 pub b: f32, + /// Transparency, 0.0 - 1.0 pub a: f32, } @@ -33,11 +39,45 @@ impl Color { a: 0.0, }; + /// Creates a new [`Color`]. + /// + /// In debug mode, it will panic if the values are not in the correct + /// range: 0.0 - 1.0 + /// + /// [`Color`]: struct.Color.html + pub fn new(r: f32, g: f32, b: f32, a: f32) -> Color { + debug_assert!( + (0.0..=1.0).contains(&r), + "Red component must be on [0, 1]" + ); + debug_assert!( + (0.0..=1.0).contains(&g), + "Green component must be on [0, 1]" + ); + debug_assert!( + (0.0..=1.0).contains(&b), + "Blue component must be on [0, 1]" + ); + debug_assert!( + (0.0..=1.0).contains(&a), + "Alpha component must be on [0, 1]" + ); + + Color { r, g, b, a } + } + /// Creates a [`Color`] from its RGB components. /// /// [`Color`]: struct.Color.html pub const fn from_rgb(r: f32, g: f32, b: f32) -> Color { - Color { r, g, b, a: 1.0 } + Color::from_rgba(r, g, b, 1.0f32) + } + + /// Creates a [`Color`] from its RGBA components. + /// + /// [`Color`]: struct.Color.html + pub const fn from_rgba(r: f32, g: f32, b: f32, a: f32) -> Color { + Color { r, g, b, a } } /// Creates a [`Color`] from its RGB8 components. @@ -80,16 +120,114 @@ impl Color { self.a, ] } + + /// Inverts the [`Color`] in-place. + /// + /// [`Color`]: struct.Color.html + pub fn invert(&mut self) { + self.r = 1.0f32 - self.r; + self.b = 1.0f32 - self.g; + self.g = 1.0f32 - self.b; + } + + /// Returns the inverted [`Color`]. + /// + /// [`Color`]: struct.Color.html + pub fn inverse(self) -> Color { + Color::new(1.0f32 - self.r, 1.0f32 - self.g, 1.0f32 - self.b, self.a) + } } impl From<[f32; 3]> for Color { fn from([r, g, b]: [f32; 3]) -> Self { - Color { r, g, b, a: 1.0 } + Color::new(r, g, b, 1.0) } } impl From<[f32; 4]> for Color { fn from([r, g, b, a]: [f32; 4]) -> Self { - Color { r, g, b, a } + Color::new(r, g, b, a) + } +} + +#[cfg(feature = "palette")] +/// Converts from palette's `Srgba` type to a [`Color`]. +/// +/// [`Color`]: struct.Color.html +impl From<Srgba> for Color { + fn from(srgba: Srgba) -> Self { + Color::new(srgba.red, srgba.green, srgba.blue, srgba.alpha) + } +} + +#[cfg(feature = "palette")] +/// Converts from [`Color`] to palette's `Srgba` type. +/// +/// [`Color`]: struct.Color.html +impl From<Color> for Srgba { + fn from(c: Color) -> Self { + Srgba::new(c.r, c.g, c.b, c.a) + } +} + +#[cfg(feature = "palette")] +/// Converts from palette's `Srgb` type to a [`Color`]. +/// +/// [`Color`]: struct.Color.html +impl From<Srgb> for Color { + fn from(srgb: Srgb) -> Self { + Color::new(srgb.red, srgb.green, srgb.blue, 1.0) + } +} + +#[cfg(feature = "palette")] +/// Converts from [`Color`] to palette's `Srgb` type. +/// +/// [`Color`]: struct.Color.html +/// [`Srgb`]: ../palette/rgb/type.Srgb.html +impl From<Color> for Srgb { + fn from(c: Color) -> Self { + Srgb::new(c.r, c.g, c.b) + } +} + +#[cfg(feature = "palette")] +#[cfg(test)] +mod tests { + use super::*; + use palette::Blend; + + #[test] + fn srgba_traits() { + let c = Color::from_rgb(0.5, 0.4, 0.3); + // Round-trip conversion to the palette:Srgba type + let s: Srgba = c.into(); + let r: Color = s.into(); + assert_eq!(c, r); + } + + #[test] + fn color_manipulation() { + let c1 = Color::from_rgb(0.5, 0.4, 0.3); + let c2 = Color::from_rgb(0.2, 0.5, 0.3); + + // Convert to linear color for manipulation + let l1 = Srgba::from(c1).into_linear(); + let l2 = Srgba::from(c2).into_linear(); + + // Take the lighter of each of the RGB 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 + } + ); } } diff --git a/core/src/font.rs b/core/src/font.rs index be49c825..3f9ad2b5 100644 --- a/core/src/font.rs +++ b/core/src/font.rs @@ -16,3 +16,9 @@ pub enum Font { bytes: &'static [u8], }, } + +impl Default for Font { + fn default() -> Font { + Font::Default + } +} diff --git a/core/src/keyboard.rs b/core/src/keyboard.rs index d98b2989..b26bdb3d 100644 --- a/core/src/keyboard.rs +++ b/core/src/keyboard.rs @@ -1,6 +1,8 @@ //! Reuse basic keyboard types. +mod event; mod key_code; mod modifiers_state; +pub use event::Event; pub use key_code::KeyCode; pub use modifiers_state::ModifiersState; diff --git a/native/src/input/keyboard/event.rs b/core/src/keyboard/event.rs index 862f30c4..bc8437a8 100644 --- a/native/src/input/keyboard/event.rs +++ b/core/src/keyboard/event.rs @@ -1,5 +1,4 @@ use super::{KeyCode, ModifiersState}; -use crate::input::ButtonState; /// A keyboard event. /// @@ -9,11 +8,17 @@ use crate::input::ButtonState; /// [open an issue]: https://github.com/hecrj/iced/issues #[derive(Debug, Clone, Copy, PartialEq)] pub enum Event { - /// A keyboard key was pressed or released. - Input { - /// The state of the key - state: ButtonState, + /// A keyboard key was pressed. + KeyPressed { + /// The key identifier + key_code: KeyCode, + + /// The state of the modifier keys + modifiers: ModifiersState, + }, + /// A keyboard key was released. + KeyReleased { /// The key identifier key_code: KeyCode, diff --git a/core/src/lib.rs b/core/src/lib.rs index c2887a0b..6b9e612e 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -15,6 +15,7 @@ #![forbid(unsafe_code)] #![forbid(rust_2018_idioms)] pub mod keyboard; +pub mod mouse; mod align; mod background; diff --git a/native/src/input/mouse.rs b/core/src/mouse.rs index 7198b233..25ce6ac3 100644 --- a/native/src/input/mouse.rs +++ b/core/src/mouse.rs @@ -1,9 +1,8 @@ -//! Build mouse events. +//! Reuse basic mouse types. mod button; mod event; - -pub mod click; +mod interaction; pub use button::Button; -pub use click::Click; pub use event::{Event, ScrollDelta}; +pub use interaction::Interaction; diff --git a/native/src/input/mouse/button.rs b/core/src/mouse/button.rs index aeb8a55d..aeb8a55d 100644 --- a/native/src/input/mouse/button.rs +++ b/core/src/mouse/button.rs diff --git a/native/src/input/mouse/event.rs b/core/src/mouse/event.rs index aafc4fe3..2f07b207 100644 --- a/native/src/input/mouse/event.rs +++ b/core/src/mouse/event.rs @@ -1,5 +1,4 @@ use super::Button; -use crate::input::ButtonState; /// A mouse event. /// @@ -24,14 +23,11 @@ pub enum Event { y: f32, }, - /// A mouse button was pressed or released. - Input { - /// The state of the button - state: ButtonState, + /// A mouse button was pressed. + ButtonPressed(Button), - /// The button identifier - button: Button, - }, + /// A mouse button was released. + ButtonReleased(Button), /// The mouse wheel was scrolled. WheelScrolled { diff --git a/core/src/mouse/interaction.rs b/core/src/mouse/interaction.rs new file mode 100644 index 00000000..664147a7 --- /dev/null +++ b/core/src/mouse/interaction.rs @@ -0,0 +1,20 @@ +/// The interaction of a mouse cursor. +#[derive(Debug, Eq, PartialEq, Clone, Copy, PartialOrd, Ord)] +#[allow(missing_docs)] +pub enum Interaction { + Idle, + Pointer, + Grab, + Text, + Crosshair, + Working, + Grabbing, + ResizingHorizontally, + ResizingVertically, +} + +impl Default for Interaction { + fn default() -> Interaction { + Interaction::Idle + } +} diff --git a/core/src/point.rs b/core/src/point.rs index 2b5ad154..3714aa2f 100644 --- a/core/src/point.rs +++ b/core/src/point.rs @@ -1,7 +1,7 @@ use crate::Vector; /// A 2D point. -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Default)] pub struct Point { /// The X coordinate. pub x: f32, @@ -67,3 +67,11 @@ impl std::ops::Sub<Vector> for Point { } } } + +impl std::ops::Sub<Point> for Point { + type Output = Vector; + + fn sub(self, point: Point) -> Vector { + Vector::new(self.x - point.x, self.y - point.y) + } +} diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index aead6e9a..ce80c661 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -1,4 +1,4 @@ -use crate::Point; +use crate::{Point, Size, Vector}; /// A rectangle. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] @@ -17,6 +17,35 @@ pub struct Rectangle<T = f32> { } impl Rectangle<f32> { + /// Creates a new [`Rectangle`] with its top-left corner in the given + /// [`Point`] and with the provided [`Size`]. + /// + /// [`Rectangle`]: struct.Rectangle.html + /// [`Point`]: struct.Point.html + /// [`Size`]: struct.Size.html + pub fn new(top_left: Point, size: Size) -> Self { + Self { + x: top_left.x, + y: top_left.y, + width: size.width, + height: size.height, + } + } + + /// Creates a new [`Rectangle`] with its top-left corner at the origin + /// and with the provided [`Size`]. + /// + /// [`Rectangle`]: struct.Rectangle.html + /// [`Size`]: struct.Size.html + pub fn with_size(size: Size) -> Self { + Self { + x: 0.0, + y: 0.0, + width: size.width, + height: size.height, + } + } + /// Returns the [`Point`] at the center of the [`Rectangle`]. /// /// [`Point`]: struct.Point.html @@ -43,6 +72,21 @@ impl Rectangle<f32> { self.y + self.height / 2.0 } + /// Returns the position of the top left corner of the [`Rectangle`]. + /// + /// [`Rectangle`]: struct.Rectangle.html + pub fn position(&self) -> Point { + Point::new(self.x, self.y) + } + + /// Returns the [`Size`] of the [`Rectangle`]. + /// + /// [`Size`]: struct.Size.html + /// [`Rectangle`]: struct.Rectangle.html + pub fn size(&self) -> Size { + Size::new(self.width, self.height) + } + /// Returns true if the given [`Point`] is contained in the [`Rectangle`]. /// /// [`Point`]: struct.Point.html @@ -81,17 +125,29 @@ impl Rectangle<f32> { None } } + + /// Snaps the [`Rectangle`] to __unsigned__ integer coordinates. + /// + /// [`Rectangle`]: struct.Rectangle.html + pub fn snap(self) -> Rectangle<u32> { + Rectangle { + x: self.x as u32, + y: self.y as u32, + width: self.width.ceil() as u32, + height: self.height.ceil() as u32, + } + } } -impl std::ops::Mul<f32> for Rectangle<u32> { +impl std::ops::Mul<f32> for Rectangle<f32> { type Output = Self; fn mul(self, scale: f32) -> Self { Self { - x: (self.x as f32 * scale).round() as u32, - y: (self.y as f32 * scale).round() as u32, - width: (self.width as f32 * scale).round() as u32, - height: (self.height as f32 * scale).round() as u32, + x: self.x as f32 * scale, + y: self.y as f32 * scale, + width: self.width * scale, + height: self.height * scale, } } } @@ -107,13 +163,17 @@ impl From<Rectangle<u32>> for Rectangle<f32> { } } -impl From<Rectangle<f32>> for Rectangle<u32> { - fn from(rectangle: Rectangle<f32>) -> Rectangle<u32> { +impl<T> std::ops::Add<Vector<T>> for Rectangle<T> +where + T: std::ops::Add<Output = T>, +{ + type Output = Rectangle<T>; + + fn add(self, translation: Vector<T>) -> Self { Rectangle { - x: rectangle.x as u32, - y: rectangle.y as u32, - width: rectangle.width.ceil() as u32, - height: rectangle.height.ceil() as u32, + x: self.x + translation.x, + y: self.y + translation.y, + ..self } } } diff --git a/core/src/size.rs b/core/src/size.rs index 4276f05f..aceb5311 100644 --- a/core/src/size.rs +++ b/core/src/size.rs @@ -2,11 +2,20 @@ use std::f32; /// An amount of space in 2 dimensions. #[derive(Debug, Clone, Copy, PartialEq)] -pub struct Size { +pub struct Size<T = f32> { /// The width. - pub width: f32, + pub width: T, /// The height. - pub height: f32, + pub height: T, +} + +impl<T> Size<T> { + /// Creates a new [`Size`] with the given width and height. + /// + /// [`Size`]: struct.Size.html + pub const fn new(width: T, height: T) -> Self { + Size { width, height } + } } impl Size { @@ -15,17 +24,15 @@ impl Size { /// [`Size`]: struct.Size.html pub const ZERO: Size = Size::new(0., 0.); - /// A [`Size`] with infinite width and height. + /// A [`Size`] with a width and height of 1 unit. /// /// [`Size`]: struct.Size.html - pub const INFINITY: Size = Size::new(f32::INFINITY, f32::INFINITY); + pub const UNIT: Size = Size::new(1., 1.); - /// Creates a new [`Size`] with the given width and height. + /// A [`Size`] with infinite width and height. /// /// [`Size`]: struct.Size.html - pub const fn new(width: f32, height: f32) -> Self { - Size { width, height } - } + pub const INFINITY: Size = Size::new(f32::INFINITY, f32::INFINITY); /// Increments the [`Size`] to account for the given padding. /// diff --git a/core/src/vector.rs b/core/src/vector.rs index a75053a0..def3f8c0 100644 --- a/core/src/vector.rs +++ b/core/src/vector.rs @@ -43,6 +43,17 @@ where } } +impl<T> std::ops::Mul<T> for Vector<T> +where + T: std::ops::Mul<Output = T> + Copy, +{ + type Output = Self; + + fn mul(self, scale: T) -> Self { + Self::new(self.x * scale, self.y * scale) + } +} + impl<T> Default for Vector<T> where T: Default, diff --git a/examples/README.md b/examples/README.md index 5aea51eb..8e1b781f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -50,6 +50,27 @@ We have not yet implemented a `LocalStorage` version of the auto-save feature. T [TodoMVC]: http://todomvc.com/ +## [Game of Life](game_of_life) +An interactive version of the [Game of Life], invented by [John Horton Conway]. + +It runs a simulation in a background thread while allowing interaction with a `Canvas` that displays an infinite grid with zooming, panning, and drawing support. + +The relevant code is located in the __[`main`](game_of_life/src/main.rs)__ file. + +<div align="center"> + <a href="https://gfycat.com/briefaccurateaardvark"> + <img src="https://thumbs.gfycat.com/BriefAccurateAardvark-size_restricted.gif"> + </a> +</div> + +You can run it with `cargo run`: +``` +cargo run --package game_of_life +``` + +[Game of Life]: https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life +[John Horton Conway]: https://en.wikipedia.org/wiki/John_Horton_Conway + ## [Styling](styling) An example showcasing custom styling with a light and dark theme. @@ -69,8 +90,9 @@ cargo run --package styling ## Extras A bunch of simpler examples exist: -- [`bezier_tool`](bezier_tool), a Paint-like tool for drawing Bézier curves using [`lyon`]. +- [`bezier_tool`](bezier_tool), a Paint-like tool for drawing Bézier curves using the `Canvas` widget. - [`clock`](clock), an application that uses the `Canvas` widget to draw a clock and its hands to display the current time. +- [`color_palette`](color_palette), a color palette generator based on a user-defined root color. - [`counter`](counter), the classic counter example explained in the [`README`](../README.md). - [`custom_widget`](custom_widget), a demonstration of how to build a custom widget that draws a circle. - [`download_progress`](download_progress), a basic application that asynchronously downloads a dummy file of 100 MB and tracks the download progress. diff --git a/examples/bezier_tool/Cargo.toml b/examples/bezier_tool/Cargo.toml index b13a0aa5..a88975a7 100644 --- a/examples/bezier_tool/Cargo.toml +++ b/examples/bezier_tool/Cargo.toml @@ -6,7 +6,4 @@ edition = "2018" publish = false [dependencies] -iced = { path = "../.." } -iced_native = { path = "../../native" } -iced_wgpu = { path = "../../wgpu" } -lyon = "0.15" +iced = { path = "../..", features = ["canvas"] } diff --git a/examples/bezier_tool/README.md b/examples/bezier_tool/README.md index 933f2120..ebbb12cc 100644 --- a/examples/bezier_tool/README.md +++ b/examples/bezier_tool/README.md @@ -1,6 +1,6 @@ ## Bézier tool -A Paint-like tool for drawing Bézier curves using [`lyon`]. +A Paint-like tool for drawing Bézier curves using the `Canvas` widget. The __[`main`]__ file contains all the code of the example. @@ -16,4 +16,3 @@ cargo run --package bezier_tool ``` [`main`]: src/main.rs -[`lyon`]: https://github.com/nical/lyon diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index fcb7733c..fe41e1b2 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -1,291 +1,6 @@ -//! This example showcases a simple native custom widget that renders arbitrary -//! path with `lyon`. -mod bezier { - // For now, to implement a custom native widget you will need to add - // `iced_native` and `iced_wgpu` to your dependencies. - // - // Then, you simply need to define your widget type and implement the - // `iced_native::Widget` trait with the `iced_wgpu::Renderer`. - // - // Of course, you can choose to make the implementation renderer-agnostic, - // if you wish to, by creating your own `Renderer` trait, which could be - // implemented by `iced_wgpu` and other renderers. - use iced_native::{ - input, layout, Clipboard, Color, Element, Event, Font, Hasher, - HorizontalAlignment, Layout, Length, MouseCursor, Point, Rectangle, - Size, Vector, VerticalAlignment, Widget, - }; - use iced_wgpu::{ - triangle::{Mesh2D, Vertex2D}, - Defaults, Primitive, Renderer, - }; - use lyon::tessellation::{ - basic_shapes, BuffersBuilder, StrokeAttributes, StrokeOptions, - StrokeTessellator, VertexBuffers, - }; - - pub struct Bezier<'a, Message> { - state: &'a mut State, - curves: &'a [Curve], - // [from, to, ctrl] - on_click: Box<dyn Fn(Curve) -> Message>, - } - - #[derive(Debug, Clone, Copy)] - pub struct Curve { - from: Point, - to: Point, - control: Point, - } - - #[derive(Default)] - pub struct State { - pending: Option<Pending>, - } - - enum Pending { - One { from: Point }, - Two { from: Point, to: Point }, - } - - impl<'a, Message> Bezier<'a, Message> { - pub fn new<F>( - state: &'a mut State, - curves: &'a [Curve], - on_click: F, - ) -> Self - where - F: 'static + Fn(Curve) -> Message, - { - Self { - state, - curves, - on_click: Box::new(on_click), - } - } - } - - impl<'a, Message> Widget<Message, Renderer> for Bezier<'a, Message> { - fn width(&self) -> Length { - Length::Fill - } - - fn height(&self) -> Length { - Length::Fill - } - - fn layout( - &self, - _renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let size = limits - .height(Length::Fill) - .width(Length::Fill) - .resolve(Size::ZERO); - layout::Node::new(size) - } - - fn draw( - &self, - _renderer: &mut Renderer, - defaults: &Defaults, - layout: Layout<'_>, - cursor_position: Point, - ) -> (Primitive, MouseCursor) { - let mut buffer: VertexBuffers<Vertex2D, u32> = VertexBuffers::new(); - let mut path_builder = lyon::path::Path::builder(); - - let bounds = layout.bounds(); - - // Draw rectangle border with lyon. - basic_shapes::stroke_rectangle( - &lyon::math::Rect::new( - lyon::math::Point::new(0.5, 0.5), - lyon::math::Size::new( - bounds.width - 1.0, - bounds.height - 1.0, - ), - ), - &StrokeOptions::default().with_line_width(1.0), - &mut BuffersBuilder::new( - &mut buffer, - |pos: lyon::math::Point, _: StrokeAttributes| Vertex2D { - position: pos.to_array(), - color: [0.0, 0.0, 0.0, 1.0], - }, - ), - ) - .unwrap(); - - for curve in self.curves { - path_builder.move_to(lyon::math::Point::new( - curve.from.x, - curve.from.y, - )); - - path_builder.quadratic_bezier_to( - lyon::math::Point::new(curve.control.x, curve.control.y), - lyon::math::Point::new(curve.to.x, curve.to.y), - ); - } - - match self.state.pending { - None => {} - Some(Pending::One { from }) => { - path_builder - .move_to(lyon::math::Point::new(from.x, from.y)); - path_builder.line_to(lyon::math::Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - )); - } - Some(Pending::Two { from, to }) => { - path_builder - .move_to(lyon::math::Point::new(from.x, from.y)); - path_builder.quadratic_bezier_to( - lyon::math::Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ), - lyon::math::Point::new(to.x, to.y), - ); - } - } - - let mut tessellator = StrokeTessellator::new(); - - // Draw strokes with lyon. - tessellator - .tessellate( - &path_builder.build(), - &StrokeOptions::default().with_line_width(3.0), - &mut BuffersBuilder::new( - &mut buffer, - |pos: lyon::math::Point, _: StrokeAttributes| { - Vertex2D { - position: pos.to_array(), - color: [0.0, 0.0, 0.0, 1.0], - } - }, - ), - ) - .unwrap(); - - let mesh = Primitive::Mesh2D { - origin: Point::new(bounds.x, bounds.y), - buffers: Mesh2D { - vertices: buffer.vertices, - indices: buffer.indices, - }, - }; - - ( - Primitive::Clip { - bounds, - offset: Vector::new(0, 0), - content: Box::new( - if self.curves.is_empty() - && self.state.pending.is_none() - { - let instructions = Primitive::Text { - bounds: Rectangle { - x: bounds.center_x(), - y: bounds.center_y(), - ..bounds - }, - color: Color { - a: defaults.text.color.a * 0.7, - ..defaults.text.color - }, - content: String::from( - "Click to create bezier curves!", - ), - font: Font::Default, - size: 30.0, - horizontal_alignment: - HorizontalAlignment::Center, - vertical_alignment: VerticalAlignment::Center, - }; - - Primitive::Group { - primitives: vec![mesh, instructions], - } - } else { - mesh - }, - ), - }, - MouseCursor::OutOfBounds, - ) - } - - fn hash_layout(&self, _state: &mut Hasher) {} - - fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - messages: &mut Vec<Message>, - _renderer: &Renderer, - _clipboard: Option<&dyn Clipboard>, - ) { - let bounds = layout.bounds(); - - if bounds.contains(cursor_position) { - match event { - Event::Mouse(input::mouse::Event::Input { - state: input::ButtonState::Pressed, - .. - }) => { - let new_point = Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ); - - match self.state.pending { - None => { - self.state.pending = - Some(Pending::One { from: new_point }); - } - Some(Pending::One { from }) => { - self.state.pending = Some(Pending::Two { - from, - to: new_point, - }); - } - Some(Pending::Two { from, to }) => { - self.state.pending = None; - - messages.push((self.on_click)(Curve { - from, - to, - control: new_point, - })); - } - } - } - _ => {} - } - } - } - } - - impl<'a, Message> Into<Element<'a, Message, Renderer>> for Bezier<'a, Message> - where - Message: 'static, - { - fn into(self) -> Element<'a, Message, Renderer> { - Element::new(self) - } - } -} - -use bezier::Bezier; +//! This example showcases an interactive `Canvas` for drawing Bézier curves. use iced::{ - button, Align, Button, Column, Container, Element, Length, Sandbox, - Settings, Text, + button, Align, Button, Column, Element, Length, Sandbox, Settings, Text, }; pub fn main() { @@ -323,6 +38,7 @@ impl Sandbox for Example { match message { Message::AddCurve(curve) => { self.curves.push(curve); + self.bezier.request_redraw(); } Message::Clear => { self.bezier = bezier::State::default(); @@ -332,7 +48,7 @@ impl Sandbox for Example { } fn view(&mut self) -> Element<Message> { - let content = Column::new() + Column::new() .padding(20) .spacing(20) .align_items(Align::Center) @@ -341,22 +57,177 @@ impl Sandbox for Example { .width(Length::Shrink) .size(50), ) - .push(Bezier::new( - &mut self.bezier, - self.curves.as_slice(), - Message::AddCurve, - )) + .push(self.bezier.view(&self.curves).map(Message::AddCurve)) .push( Button::new(&mut self.button_state, Text::new("Clear")) .padding(8) .on_press(Message::Clear), - ); + ) + .into() + } +} + +mod bezier { + use iced::{ + canvas::{self, Canvas, Cursor, Event, Frame, Geometry, Path, Stroke}, + mouse, Element, Length, Point, Rectangle, + }; + + #[derive(Default)] + pub struct State { + pending: Option<Pending>, + cache: canvas::Cache, + } - Container::new(content) + impl State { + pub fn view<'a>( + &'a mut self, + curves: &'a [Curve], + ) -> Element<'a, Curve> { + Canvas::new(Bezier { + state: self, + curves, + }) .width(Length::Fill) .height(Length::Fill) - .center_x() - .center_y() .into() + } + + pub fn request_redraw(&mut self) { + self.cache.clear() + } + } + + struct Bezier<'a> { + state: &'a mut State, + curves: &'a [Curve], + } + + impl<'a> canvas::Program<Curve> for Bezier<'a> { + fn update( + &mut self, + event: Event, + bounds: Rectangle, + cursor: Cursor, + ) -> Option<Curve> { + let cursor_position = cursor.position_in(&bounds)?; + + match event { + Event::Mouse(mouse_event) => match mouse_event { + mouse::Event::ButtonPressed(mouse::Button::Left) => { + match self.state.pending { + None => { + self.state.pending = Some(Pending::One { + from: cursor_position, + }); + None + } + Some(Pending::One { from }) => { + self.state.pending = Some(Pending::Two { + from, + to: cursor_position, + }); + + None + } + Some(Pending::Two { from, to }) => { + self.state.pending = None; + + Some(Curve { + from, + to, + control: cursor_position, + }) + } + } + } + _ => None, + }, + } + } + + fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec<Geometry> { + let content = + self.state.cache.draw(bounds.size(), |frame: &mut Frame| { + Curve::draw_all(self.curves, frame); + + frame.stroke( + &Path::rectangle(Point::ORIGIN, frame.size()), + Stroke::default(), + ); + }); + + if let Some(pending) = &self.state.pending { + let pending_curve = pending.draw(bounds, cursor); + + vec![content, pending_curve] + } else { + vec![content] + } + } + + fn mouse_interaction( + &self, + bounds: Rectangle, + cursor: Cursor, + ) -> mouse::Interaction { + if cursor.is_over(&bounds) { + mouse::Interaction::Crosshair + } else { + mouse::Interaction::default() + } + } + } + + #[derive(Debug, Clone, Copy)] + pub struct Curve { + from: Point, + to: Point, + control: Point, + } + + impl Curve { + fn draw_all(curves: &[Curve], frame: &mut Frame) { + let curves = Path::new(|p| { + for curve in curves { + p.move_to(curve.from); + p.quadratic_curve_to(curve.control, curve.to); + } + }); + + frame.stroke(&curves, Stroke::default().with_width(2.0)); + } + } + + #[derive(Debug, Clone, Copy)] + enum Pending { + One { from: Point }, + Two { from: Point, to: Point }, + } + + impl Pending { + fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Geometry { + let mut frame = Frame::new(bounds.size()); + + if let Some(cursor_position) = cursor.position_in(&bounds) { + match *self { + Pending::One { from } => { + let line = Path::line(from, cursor_position); + frame.stroke(&line, Stroke::default().with_width(2.0)); + } + Pending::Two { from, to } => { + let curve = Curve { + from, + to, + control: cursor_position, + }; + + Curve::draw_all(&[curve], &mut frame); + } + }; + } + + frame.into_geometry() + } } } diff --git a/examples/clock/Cargo.toml b/examples/clock/Cargo.toml index 308cbfbb..c6e32379 100644 --- a/examples/clock/Cargo.toml +++ b/examples/clock/Cargo.toml @@ -5,11 +5,6 @@ authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2018" publish = false -[features] -canvas = [] - [dependencies] -iced = { path = "../..", features = ["canvas", "async-std", "debug"] } -iced_native = { path = "../../native" } +iced = { path = "../..", features = ["canvas", "tokio", "debug"] } chrono = "0.4" -async-std = { version = "1.0", features = ["unstable"] } diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index 827379fa..9c583c78 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -1,6 +1,7 @@ use iced::{ - canvas, executor, Application, Canvas, Color, Command, Container, Element, - Length, Point, Settings, Subscription, Vector, + canvas::{self, Cache, Canvas, Cursor, Geometry, LineCap, Path, Stroke}, + executor, time, Application, Color, Command, Container, Element, Length, + Point, Rectangle, Settings, Subscription, Vector, }; pub fn main() { @@ -11,8 +12,8 @@ pub fn main() { } struct Clock { - now: LocalTime, - clock: canvas::layer::Cache<LocalTime>, + now: chrono::DateTime<chrono::Local>, + clock: Cache, } #[derive(Debug, Clone, Copy)] @@ -28,7 +29,7 @@ impl Application for Clock { fn new(_flags: ()) -> (Self, Command<Message>) { ( Clock { - now: chrono::Local::now().into(), + now: chrono::Local::now(), clock: Default::default(), }, Command::none(), @@ -42,7 +43,7 @@ impl Application for Clock { fn update(&mut self, message: Message) -> Command<Message> { match message { Message::Tick(local_time) => { - let now = local_time.into(); + let now = local_time; if now != self.now { self.now = now; @@ -55,14 +56,14 @@ impl Application for Clock { } fn subscription(&self) -> Subscription<Message> { - time::every(std::time::Duration::from_millis(500)).map(Message::Tick) + time::every(std::time::Duration::from_millis(500)) + .map(|_| Message::Tick(chrono::Local::now())) } fn view(&mut self) -> Element<Message> { - let canvas = Canvas::new() + let canvas = Canvas::new(self) .width(Length::Units(400)) - .height(Length::Units(400)) - .push(self.clock.with(&self.now)); + .height(Length::Units(400)); Container::new(canvas) .width(Length::Fill) @@ -74,69 +75,54 @@ impl Application for Clock { } } -#[derive(Debug, PartialEq, Eq)] -struct LocalTime { - hour: u32, - minute: u32, - second: u32, -} - -impl From<chrono::DateTime<chrono::Local>> for LocalTime { - fn from(date_time: chrono::DateTime<chrono::Local>) -> LocalTime { +impl canvas::Program<Message> for Clock { + fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec<Geometry> { use chrono::Timelike; - LocalTime { - hour: date_time.hour(), - minute: date_time.minute(), - second: date_time.second(), - } - } -} + let clock = self.clock.draw(bounds.size(), |frame| { + let center = frame.center(); + let radius = frame.width().min(frame.height()) / 2.0; -impl canvas::Drawable for LocalTime { - fn draw(&self, frame: &mut canvas::Frame) { - use canvas::Path; + let background = Path::circle(center, radius); + frame.fill(&background, Color::from_rgb8(0x12, 0x93, 0xD8)); - let center = frame.center(); - let radius = frame.width().min(frame.height()) / 2.0; + let short_hand = + Path::line(Point::ORIGIN, Point::new(0.0, -0.5 * radius)); - let clock = Path::circle(center, radius); - frame.fill(&clock, Color::from_rgb8(0x12, 0x93, 0xD8)); + let long_hand = + Path::line(Point::ORIGIN, Point::new(0.0, -0.8 * radius)); - let short_hand = - Path::line(Point::ORIGIN, Point::new(0.0, -0.5 * radius)); + let thin_stroke = Stroke { + width: radius / 100.0, + color: Color::WHITE, + line_cap: LineCap::Round, + ..Stroke::default() + }; - let long_hand = - Path::line(Point::ORIGIN, Point::new(0.0, -0.8 * radius)); + let wide_stroke = Stroke { + width: thin_stroke.width * 3.0, + ..thin_stroke + }; - let thin_stroke = canvas::Stroke { - width: radius / 100.0, - color: Color::WHITE, - line_cap: canvas::LineCap::Round, - ..canvas::Stroke::default() - }; + frame.translate(Vector::new(center.x, center.y)); - let wide_stroke = canvas::Stroke { - width: thin_stroke.width * 3.0, - ..thin_stroke - }; + frame.with_save(|frame| { + frame.rotate(hand_rotation(self.now.hour(), 12)); + frame.stroke(&short_hand, wide_stroke); + }); - frame.translate(Vector::new(center.x, center.y)); + frame.with_save(|frame| { + frame.rotate(hand_rotation(self.now.minute(), 60)); + frame.stroke(&long_hand, wide_stroke); + }); - frame.with_save(|frame| { - frame.rotate(hand_rotation(self.hour, 12)); - frame.stroke(&short_hand, wide_stroke); + frame.with_save(|frame| { + frame.rotate(hand_rotation(self.now.second(), 60)); + frame.stroke(&long_hand, thin_stroke); + }) }); - frame.with_save(|frame| { - frame.rotate(hand_rotation(self.minute, 60)); - frame.stroke(&long_hand, wide_stroke); - }); - - frame.with_save(|frame| { - frame.rotate(hand_rotation(self.second, 60)); - frame.stroke(&long_hand, thin_stroke); - }); + vec![clock] } } @@ -145,40 +131,3 @@ fn hand_rotation(n: u32, total: u32) -> f32 { 2.0 * std::f32::consts::PI * turns } - -mod time { - use iced::futures; - - pub fn every( - duration: std::time::Duration, - ) -> iced::Subscription<chrono::DateTime<chrono::Local>> { - iced::Subscription::from_recipe(Every(duration)) - } - - struct Every(std::time::Duration); - - impl<H, I> iced_native::subscription::Recipe<H, I> for Every - where - H: std::hash::Hasher, - { - type Output = chrono::DateTime<chrono::Local>; - - fn hash(&self, state: &mut H) { - use std::hash::Hash; - - std::any::TypeId::of::<Self>().hash(state); - self.0.hash(state); - } - - fn stream( - self: Box<Self>, - _input: futures::stream::BoxStream<'static, I>, - ) -> futures::stream::BoxStream<'static, Self::Output> { - use futures::stream::StreamExt; - - async_std::stream::interval(self.0) - .map(|_| chrono::Local::now()) - .boxed() - } - } -} diff --git a/examples/color_palette/Cargo.toml b/examples/color_palette/Cargo.toml new file mode 100644 index 00000000..00f33e20 --- /dev/null +++ b/examples/color_palette/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "color_palette" +version = "0.1.0" +authors = ["Clark Moody <clark@clarkmoody.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../..", features = ["canvas", "palette"] } +palette = "0.5.0" diff --git a/examples/color_palette/README.md b/examples/color_palette/README.md new file mode 100644 index 00000000..e70188f8 --- /dev/null +++ b/examples/color_palette/README.md @@ -0,0 +1,15 @@ +## Color palette + +A color palette generator, based on a user-defined root color. + +<div align="center"> + <a href="https://gfycat.com/dirtylonebighornsheep"> + <img src="https://github.com/hecrj/iced/raw/1a8d253611d3796b0a32b2f096bb54565a5292e0/examples/color_palette/screenshot.png"> + </a> +</div> + +You can run it with `cargo run`: + +``` +cargo run --package color_palette +``` diff --git a/examples/color_palette/screenshot.png b/examples/color_palette/screenshot.png Binary files differnew file mode 100644 index 00000000..aa4772e0 --- /dev/null +++ b/examples/color_palette/screenshot.png diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs new file mode 100644 index 00000000..3186deff --- /dev/null +++ b/examples/color_palette/src/main.rs @@ -0,0 +1,462 @@ +use iced::canvas::{self, Cursor, Frame, Geometry, Path}; +use iced::{ + slider, Align, Canvas, Color, Column, Element, HorizontalAlignment, Length, + Point, Rectangle, Row, Sandbox, Settings, Size, Slider, Text, Vector, + VerticalAlignment, +}; +use palette::{self, Hsl, Limited, Srgb}; +use std::marker::PhantomData; +use std::ops::RangeInclusive; + +pub fn main() { + ColorPalette::run(Settings { + antialiasing: true, + ..Settings::default() + }) +} + +#[derive(Default)] +pub struct ColorPalette { + theme: Theme, + rgb: ColorPicker<Color>, + hsl: ColorPicker<palette::Hsl>, + hsv: ColorPicker<palette::Hsv>, + hwb: ColorPicker<palette::Hwb>, + lab: ColorPicker<palette::Lab>, + lch: ColorPicker<palette::Lch>, +} + +#[derive(Debug, Clone, Copy)] +pub enum Message { + RgbColorChanged(Color), + HslColorChanged(palette::Hsl), + HsvColorChanged(palette::Hsv), + HwbColorChanged(palette::Hwb), + LabColorChanged(palette::Lab), + LchColorChanged(palette::Lch), +} + +impl Sandbox for ColorPalette { + type Message = Message; + + fn new() -> Self { + Self::default() + } + + fn title(&self) -> String { + String::from("Color palette - Iced") + } + + fn update(&mut self, message: Message) { + let srgb = match message { + Message::RgbColorChanged(rgb) => palette::Srgb::from(rgb), + Message::HslColorChanged(hsl) => palette::Srgb::from(hsl), + Message::HsvColorChanged(hsv) => palette::Srgb::from(hsv), + Message::HwbColorChanged(hwb) => palette::Srgb::from(hwb), + Message::LabColorChanged(lab) => palette::Srgb::from(lab), + Message::LchColorChanged(lch) => palette::Srgb::from(lch), + }; + + self.theme = Theme::new(srgb.clamp()); + } + + fn view(&mut self) -> Element<Message> { + let base = self.theme.base; + + let srgb = palette::Srgb::from(base); + let hsl = palette::Hsl::from(srgb); + let hsv = palette::Hsv::from(srgb); + let hwb = palette::Hwb::from(srgb); + let lab = palette::Lab::from(srgb); + let lch = palette::Lch::from(srgb); + + Column::new() + .padding(10) + .spacing(10) + .push(self.rgb.view(base).map(Message::RgbColorChanged)) + .push(self.hsl.view(hsl).map(Message::HslColorChanged)) + .push(self.hsv.view(hsv).map(Message::HsvColorChanged)) + .push(self.hwb.view(hwb).map(Message::HwbColorChanged)) + .push(self.lab.view(lab).map(Message::LabColorChanged)) + .push(self.lch.view(lch).map(Message::LchColorChanged)) + .push(self.theme.view()) + .into() + } +} + +#[derive(Debug)] +pub struct Theme { + lower: Vec<Color>, + base: Color, + higher: Vec<Color>, + canvas_cache: canvas::Cache, +} + +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(Srgb::from(base)); + + let lower = [ + hsl.shift_hue(-135.0).lighten(0.075), + hsl.shift_hue(-120.0), + hsl.shift_hue(-105.0).darken(0.075), + hsl.darken(0.075), + ]; + + let higher = [ + hsl.lighten(0.075), + hsl.shift_hue(105.0).darken(0.075), + hsl.shift_hue(120.0), + hsl.shift_hue(135.0).lighten(0.075), + ]; + + Theme { + lower: lower + .iter() + .map(|&color| Srgb::from(color).clamp().into()) + .collect(), + base, + higher: higher + .iter() + .map(|&color| Srgb::from(color).clamp().into()) + .collect(), + canvas_cache: canvas::Cache::default(), + } + } + + pub fn len(&self) -> usize { + self.lower.len() + self.higher.len() + 1 + } + + pub fn colors(&self) -> impl Iterator<Item = &Color> { + self.lower + .iter() + .chain(std::iter::once(&self.base)) + .chain(self.higher.iter()) + } + + pub fn view(&mut self) -> Element<Message> { + Canvas::new(self) + .width(Length::Fill) + .height(Length::Fill) + .into() + } + + fn draw(&self, frame: &mut Frame) { + let pad = 20.0; + + let box_size = Size { + width: frame.width() / self.len() as f32, + height: frame.height() / 2.0 - pad, + }; + + let triangle = Path::new(|path| { + path.move_to(Point { x: 0.0, y: -0.5 }); + path.line_to(Point { x: -0.5, y: 0.0 }); + path.line_to(Point { x: 0.5, y: 0.0 }); + path.close(); + }); + + let mut text = canvas::Text { + horizontal_alignment: HorizontalAlignment::Center, + vertical_alignment: VerticalAlignment::Top, + size: 15.0, + ..canvas::Text::default() + }; + + for (i, &color) in self.colors().enumerate() { + let anchor = Point { + x: (i as f32) * box_size.width, + y: 0.0, + }; + frame.fill_rectangle(anchor, box_size, color); + + // We show a little indicator for the base color + if color == self.base { + let triangle_x = anchor.x + box_size.width / 2.0; + + frame.with_save(|frame| { + frame.translate(Vector::new(triangle_x, 0.0)); + frame.scale(10.0); + frame.rotate(std::f32::consts::PI); + + frame.fill(&triangle, Color::WHITE); + }); + + frame.with_save(|frame| { + frame.translate(Vector::new(triangle_x, box_size.height)); + frame.scale(10.0); + + frame.fill(&triangle, Color::WHITE); + }); + } + + frame.fill_text(canvas::Text { + content: color_hex_string(&color), + position: Point { + x: anchor.x + box_size.width / 2.0, + y: box_size.height, + }, + ..text + }); + } + + text.vertical_alignment = VerticalAlignment::Bottom; + + let hsl = Hsl::from(Srgb::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(graded.clamp()).into(); + + let anchor = Point { + x: (i as f32) * box_size.width, + y: box_size.height + 2.0 * pad, + }; + + frame.fill_rectangle(anchor, box_size, color); + + frame.fill_text(canvas::Text { + content: color_hex_string(&color), + position: Point { + x: anchor.x + box_size.width / 2.0, + y: box_size.height + 2.0 * pad, + }, + ..text + }); + } + } +} + +impl canvas::Program<Message> for Theme { + fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec<Geometry> { + let theme = self.canvas_cache.draw(bounds.size(), |frame| { + self.draw(frame); + }); + + vec![theme] + } +} + +impl Default for Theme { + fn default() -> Self { + Theme::new(Color::from_rgb8(75, 128, 190)) + } +} + +fn color_hex_string(color: &Color) -> String { + format!( + "#{:x}{:x}{:x}", + (255.0 * color.r).round() as u8, + (255.0 * color.g).round() as u8, + (255.0 * color.b).round() as u8 + ) +} + +#[derive(Default)] +struct ColorPicker<C: ColorSpace> { + sliders: [slider::State; 3], + color_space: PhantomData<C>, +} + +trait ColorSpace: Sized { + const LABEL: &'static str; + const COMPONENT_RANGES: [RangeInclusive<f64>; 3]; + + fn new(a: f32, b: f32, c: f32) -> Self; + + fn components(&self) -> [f32; 3]; + + fn to_string(&self) -> String; +} + +impl<C: 'static + ColorSpace + Copy> ColorPicker<C> { + fn view(&mut self, color: C) -> Element<C> { + let [c1, c2, c3] = color.components(); + let [s1, s2, s3] = &mut self.sliders; + let [cr1, cr2, cr3] = C::COMPONENT_RANGES; + + fn slider<C>( + state: &mut slider::State, + range: RangeInclusive<f64>, + component: f32, + update: impl Fn(f32) -> C + 'static, + ) -> Slider<f64, C> { + Slider::new(state, range, f64::from(component), move |v| { + update(v as f32) + }) + .step(0.01) + } + + Row::new() + .spacing(10) + .align_items(Align::Center) + .push(Text::new(C::LABEL).width(Length::Units(50))) + .push(slider(s1, cr1, c1, move |v| C::new(v, c2, c3))) + .push(slider(s2, cr2, c2, move |v| C::new(c1, v, c3))) + .push(slider(s3, cr3, c3, move |v| C::new(c1, c2, v))) + .push( + Text::new(color.to_string()) + .width(Length::Units(185)) + .size(14), + ) + .into() + } +} + +impl ColorSpace for Color { + const LABEL: &'static str = "RGB"; + const COMPONENT_RANGES: [RangeInclusive<f64>; 3] = + [0.0..=1.0, 0.0..=1.0, 0.0..=1.0]; + + fn new(r: f32, g: f32, b: f32) -> Self { + Color::from_rgb(r, g, b) + } + + fn components(&self) -> [f32; 3] { + [self.r, self.g, self.b] + } + + fn to_string(&self) -> String { + format!( + "rgb({:.0}, {:.0}, {:.0})", + 255.0 * self.r, + 255.0 * self.g, + 255.0 * self.b + ) + } +} + +impl ColorSpace for palette::Hsl { + const LABEL: &'static str = "HSL"; + const COMPONENT_RANGES: [RangeInclusive<f64>; 3] = + [0.0..=360.0, 0.0..=1.0, 0.0..=1.0]; + + fn new(hue: f32, saturation: f32, lightness: f32) -> Self { + palette::Hsl::new( + palette::RgbHue::from_degrees(hue), + saturation, + lightness, + ) + } + + fn components(&self) -> [f32; 3] { + [ + self.hue.to_positive_degrees(), + self.saturation, + self.lightness, + ] + } + + fn to_string(&self) -> String { + format!( + "hsl({:.1}, {:.1}%, {:.1}%)", + self.hue.to_positive_degrees(), + 100.0 * self.saturation, + 100.0 * self.lightness + ) + } +} + +impl ColorSpace for palette::Hsv { + const LABEL: &'static str = "HSV"; + const COMPONENT_RANGES: [RangeInclusive<f64>; 3] = + [0.0..=360.0, 0.0..=1.0, 0.0..=1.0]; + + fn new(hue: f32, saturation: f32, value: f32) -> Self { + palette::Hsv::new(palette::RgbHue::from_degrees(hue), saturation, value) + } + + fn components(&self) -> [f32; 3] { + [self.hue.to_positive_degrees(), self.saturation, self.value] + } + + fn to_string(&self) -> String { + format!( + "hsv({:.1}, {:.1}%, {:.1}%)", + self.hue.to_positive_degrees(), + 100.0 * self.saturation, + 100.0 * self.value + ) + } +} + +impl ColorSpace for palette::Hwb { + const LABEL: &'static str = "HWB"; + const COMPONENT_RANGES: [RangeInclusive<f64>; 3] = + [0.0..=360.0, 0.0..=1.0, 0.0..=1.0]; + + fn new(hue: f32, whiteness: f32, blackness: f32) -> Self { + palette::Hwb::new( + palette::RgbHue::from_degrees(hue), + whiteness, + blackness, + ) + } + + fn components(&self) -> [f32; 3] { + [ + self.hue.to_positive_degrees(), + self.whiteness, + self.blackness, + ] + } + + fn to_string(&self) -> String { + format!( + "hwb({:.1}, {:.1}%, {:.1}%)", + self.hue.to_positive_degrees(), + 100.0 * self.whiteness, + 100.0 * self.blackness + ) + } +} + +impl ColorSpace for palette::Lab { + const LABEL: &'static str = "Lab"; + const COMPONENT_RANGES: [RangeInclusive<f64>; 3] = + [0.0..=100.0, -128.0..=127.0, -128.0..=127.0]; + + fn new(l: f32, a: f32, b: f32) -> Self { + palette::Lab::new(l, a, b) + } + + fn components(&self) -> [f32; 3] { + [self.l, self.a, self.b] + } + + fn to_string(&self) -> String { + format!("Lab({:.1}, {:.1}, {:.1})", self.l, self.a, self.b) + } +} + +impl ColorSpace for palette::Lch { + const LABEL: &'static str = "Lch"; + const COMPONENT_RANGES: [RangeInclusive<f64>; 3] = + [0.0..=100.0, 0.0..=128.0, 0.0..=360.0]; + + fn new(l: f32, chroma: f32, hue: f32) -> Self { + palette::Lch::new(l, chroma, palette::LabHue::from_degrees(hue)) + } + + fn components(&self) -> [f32; 3] { + [self.l, self.chroma, self.hue.to_positive_degrees()] + } + + fn to_string(&self) -> String { + format!( + "Lch({:.1}, {:.1}, {:.1})", + self.l, + self.chroma, + self.hue.to_positive_degrees() + ) + } +} diff --git a/examples/custom_widget/Cargo.toml b/examples/custom_widget/Cargo.toml index 30747dc0..3942538d 100644 --- a/examples/custom_widget/Cargo.toml +++ b/examples/custom_widget/Cargo.toml @@ -8,4 +8,4 @@ publish = false [dependencies] iced = { path = "../.." } iced_native = { path = "../../native" } -iced_wgpu = { path = "../../wgpu" } +iced_graphics = { path = "../../graphics" } diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs index 0a570745..bcf896b0 100644 --- a/examples/custom_widget/src/main.rs +++ b/examples/custom_widget/src/main.rs @@ -9,11 +9,11 @@ mod circle { // Of course, you can choose to make the implementation renderer-agnostic, // if you wish to, by creating your own `Renderer` trait, which could be // implemented by `iced_wgpu` and other renderers. + use iced_graphics::{Backend, Defaults, Primitive, Renderer}; use iced_native::{ - layout, Background, Color, Element, Hasher, Layout, Length, - MouseCursor, Point, Size, Widget, + layout, mouse, Background, Color, Element, Hasher, Layout, Length, + Point, Size, Widget, }; - use iced_wgpu::{Defaults, Primitive, Renderer}; pub struct Circle { radius: u16, @@ -25,7 +25,10 @@ mod circle { } } - impl<Message> Widget<Message, Renderer> for Circle { + impl<Message, B> Widget<Message, Renderer<B>> for Circle + where + B: Backend, + { fn width(&self) -> Length { Length::Shrink } @@ -36,7 +39,7 @@ mod circle { fn layout( &self, - _renderer: &Renderer, + _renderer: &Renderer<B>, _limits: &layout::Limits, ) -> layout::Node { layout::Node::new(Size::new( @@ -53,11 +56,11 @@ mod circle { fn draw( &self, - _renderer: &mut Renderer, + _renderer: &mut Renderer<B>, _defaults: &Defaults, layout: Layout<'_>, _cursor_position: Point, - ) -> (Primitive, MouseCursor) { + ) -> (Primitive, mouse::Interaction) { ( Primitive::Quad { bounds: layout.bounds(), @@ -66,13 +69,16 @@ mod circle { border_width: 0, border_color: Color::TRANSPARENT, }, - MouseCursor::OutOfBounds, + mouse::Interaction::default(), ) } } - impl<'a, Message> Into<Element<'a, Message, Renderer>> for Circle { - fn into(self) -> Element<'a, Message, Renderer> { + impl<'a, Message, B> Into<Element<'a, Message, Renderer<B>>> for Circle + where + B: Backend, + { + fn into(self) -> Element<'a, Message, Renderer<B>> { Element::new(self) } } diff --git a/examples/game_of_life/Cargo.toml b/examples/game_of_life/Cargo.toml new file mode 100644 index 00000000..b9bb7f2a --- /dev/null +++ b/examples/game_of_life/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "game_of_life" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../..", features = ["canvas", "tokio", "debug"] } +tokio = { version = "0.2", features = ["blocking"] } +itertools = "0.9" +rustc-hash = "1.1" diff --git a/examples/game_of_life/README.md b/examples/game_of_life/README.md new file mode 100644 index 00000000..1aeb1455 --- /dev/null +++ b/examples/game_of_life/README.md @@ -0,0 +1,22 @@ +## Game of Life + +An interactive version of the [Game of Life], invented by [John Horton Conway]. + +It runs a simulation in a background thread while allowing interaction with a `Canvas` that displays an infinite grid with zooming, panning, and drawing support. + +The __[`main`]__ file contains the relevant code of the example. + +<div align="center"> + <a href="https://gfycat.com/briefaccurateaardvark"> + <img src="https://thumbs.gfycat.com/BriefAccurateAardvark-size_restricted.gif"> + </a> +</div> + +You can run it with `cargo run`: +``` +cargo run --package game_of_life +``` + +[`main`]: src/main.rs +[Game of Life]: https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life +[John Horton Conway]: https://en.wikipedia.org/wiki/John_Horton_Conway diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs new file mode 100644 index 00000000..080d55c0 --- /dev/null +++ b/examples/game_of_life/src/main.rs @@ -0,0 +1,803 @@ +//! This example showcases an interactive version of the Game of Life, invented +//! by John Conway. It leverages a `Canvas` together with other widgets. +mod style; + +use grid::Grid; +use iced::{ + button::{self, Button}, + executor, + slider::{self, Slider}, + time, Align, Application, Checkbox, Column, Command, Container, Element, + Length, Row, Settings, Subscription, Text, +}; +use std::time::{Duration, Instant}; + +pub fn main() { + GameOfLife::run(Settings { + antialiasing: true, + ..Settings::default() + }) +} + +#[derive(Default)] +struct GameOfLife { + grid: Grid, + controls: Controls, + is_playing: bool, + queued_ticks: usize, + speed: usize, + next_speed: Option<usize>, +} + +#[derive(Debug, Clone)] +enum Message { + Grid(grid::Message), + Tick(Instant), + TogglePlayback, + ToggleGrid(bool), + Next, + Clear, + SpeedChanged(f32), +} + +impl Application for GameOfLife { + type Message = Message; + type Executor = executor::Default; + type Flags = (); + + fn new(_flags: ()) -> (Self, Command<Message>) { + ( + Self { + speed: 1, + ..Self::default() + }, + Command::none(), + ) + } + + fn title(&self) -> String { + String::from("Game of Life - Iced") + } + + fn update(&mut self, message: Message) -> Command<Message> { + match message { + Message::Grid(message) => { + self.grid.update(message); + } + Message::Tick(_) | Message::Next => { + self.queued_ticks = (self.queued_ticks + 1).min(self.speed); + + if let Some(task) = self.grid.tick(self.queued_ticks) { + if let Some(speed) = self.next_speed.take() { + self.speed = speed; + } + + self.queued_ticks = 0; + + return Command::perform(task, Message::Grid); + } + } + Message::TogglePlayback => { + self.is_playing = !self.is_playing; + } + Message::ToggleGrid(show_grid_lines) => { + self.grid.toggle_lines(show_grid_lines); + } + Message::Clear => { + self.grid.clear(); + } + Message::SpeedChanged(speed) => { + if self.is_playing { + self.next_speed = Some(speed.round() as usize); + } else { + self.speed = speed.round() as usize; + } + } + } + + Command::none() + } + + fn subscription(&self) -> Subscription<Message> { + if self.is_playing { + time::every(Duration::from_millis(1000 / self.speed as u64)) + .map(Message::Tick) + } else { + Subscription::none() + } + } + + fn view(&mut self) -> Element<Message> { + let selected_speed = self.next_speed.unwrap_or(self.speed); + let controls = self.controls.view( + self.is_playing, + self.grid.are_lines_visible(), + selected_speed, + ); + + let content = Column::new() + .push(self.grid.view().map(Message::Grid)) + .push(controls); + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .style(style::Container) + .into() + } +} + +mod grid { + use iced::{ + canvas::{ + self, Cache, Canvas, Cursor, Event, Frame, Geometry, Path, Text, + }, + mouse, Color, Element, HorizontalAlignment, Length, Point, Rectangle, + Size, Vector, VerticalAlignment, + }; + use rustc_hash::{FxHashMap, FxHashSet}; + use std::future::Future; + use std::ops::RangeInclusive; + use std::time::{Duration, Instant}; + + pub struct Grid { + state: State, + interaction: Interaction, + life_cache: Cache, + grid_cache: Cache, + translation: Vector, + scaling: f32, + show_lines: bool, + last_tick_duration: Duration, + last_queued_ticks: usize, + version: usize, + } + + #[derive(Debug, Clone)] + pub enum Message { + Populate(Cell), + Unpopulate(Cell), + Ticked { + result: Result<Life, TickError>, + tick_duration: Duration, + version: usize, + }, + } + + #[derive(Debug, Clone)] + pub enum TickError { + JoinFailed, + } + + impl Default for Grid { + fn default() -> Self { + Self { + state: State::default(), + interaction: Interaction::None, + life_cache: Cache::default(), + grid_cache: Cache::default(), + translation: Vector::default(), + scaling: 1.0, + show_lines: true, + last_tick_duration: Duration::default(), + last_queued_ticks: 0, + version: 0, + } + } + } + + impl Grid { + const MIN_SCALING: f32 = 0.1; + const MAX_SCALING: f32 = 2.0; + + pub fn tick( + &mut self, + amount: usize, + ) -> Option<impl Future<Output = Message>> { + let version = self.version; + let tick = self.state.tick(amount)?; + + self.last_queued_ticks = amount; + + Some(async move { + let start = Instant::now(); + let result = tick.await; + let tick_duration = start.elapsed() / amount as u32; + + Message::Ticked { + result, + version, + tick_duration, + } + }) + } + + pub fn update(&mut self, message: Message) { + match message { + Message::Populate(cell) => { + self.state.populate(cell); + self.life_cache.clear(); + } + Message::Unpopulate(cell) => { + self.state.unpopulate(&cell); + self.life_cache.clear(); + } + Message::Ticked { + result: Ok(life), + version, + tick_duration, + } if version == self.version => { + self.state.update(life); + self.life_cache.clear(); + + self.last_tick_duration = tick_duration; + } + Message::Ticked { + result: Err(error), .. + } => { + dbg!(error); + } + Message::Ticked { .. } => {} + } + } + + pub fn view<'a>(&'a mut self) -> Element<'a, Message> { + Canvas::new(self) + .width(Length::Fill) + .height(Length::Fill) + .into() + } + + pub fn clear(&mut self) { + self.state = State::default(); + self.version += 1; + + self.life_cache.clear(); + } + + pub fn toggle_lines(&mut self, enabled: bool) { + self.show_lines = enabled; + } + + pub fn are_lines_visible(&self) -> bool { + self.show_lines + } + + fn visible_region(&self, size: Size) -> Region { + let width = size.width / self.scaling; + let height = size.height / self.scaling; + + Region { + x: -self.translation.x - width / 2.0, + y: -self.translation.y - height / 2.0, + width, + height, + } + } + + fn project(&self, position: Point, size: Size) -> Point { + let region = self.visible_region(size); + + Point::new( + position.x / self.scaling + region.x, + position.y / self.scaling + region.y, + ) + } + } + + impl<'a> canvas::Program<Message> for Grid { + fn update( + &mut self, + event: Event, + bounds: Rectangle, + cursor: Cursor, + ) -> Option<Message> { + if let Event::Mouse(mouse::Event::ButtonReleased(_)) = event { + self.interaction = Interaction::None; + } + + let cursor_position = cursor.position_in(&bounds)?; + let cell = Cell::at(self.project(cursor_position, bounds.size())); + let is_populated = self.state.contains(&cell); + + let (populate, unpopulate) = if is_populated { + (None, Some(Message::Unpopulate(cell))) + } else { + (Some(Message::Populate(cell)), None) + }; + + match event { + Event::Mouse(mouse_event) => match mouse_event { + mouse::Event::ButtonPressed(button) => match button { + mouse::Button::Left => { + self.interaction = if is_populated { + Interaction::Erasing + } else { + Interaction::Drawing + }; + + populate.or(unpopulate) + } + mouse::Button::Right => { + self.interaction = Interaction::Panning { + translation: self.translation, + start: cursor_position, + }; + + None + } + _ => None, + }, + mouse::Event::CursorMoved { .. } => { + match self.interaction { + Interaction::Drawing => populate, + Interaction::Erasing => unpopulate, + Interaction::Panning { translation, start } => { + self.translation = translation + + (cursor_position - start) + * (1.0 / self.scaling); + + self.life_cache.clear(); + self.grid_cache.clear(); + + None + } + _ => None, + } + } + mouse::Event::WheelScrolled { delta } => match delta { + mouse::ScrollDelta::Lines { y, .. } + | mouse::ScrollDelta::Pixels { y, .. } => { + if y < 0.0 && self.scaling > Self::MIN_SCALING + || y > 0.0 && self.scaling < Self::MAX_SCALING + { + let old_scaling = self.scaling; + + self.scaling = (self.scaling + * (1.0 + y / 30.0)) + .max(Self::MIN_SCALING) + .min(Self::MAX_SCALING); + + if let Some(cursor_to_center) = + cursor.position_from(bounds.center()) + { + let factor = self.scaling - old_scaling; + + self.translation = self.translation + - Vector::new( + cursor_to_center.x * factor + / (old_scaling * old_scaling), + cursor_to_center.y * factor + / (old_scaling * old_scaling), + ); + } + + self.life_cache.clear(); + self.grid_cache.clear(); + } + + None + } + }, + _ => None, + }, + } + } + + fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec<Geometry> { + let center = Vector::new(bounds.width / 2.0, bounds.height / 2.0); + + let life = self.life_cache.draw(bounds.size(), |frame| { + let background = Path::rectangle(Point::ORIGIN, frame.size()); + frame.fill(&background, Color::from_rgb8(0x40, 0x44, 0x4B)); + + frame.with_save(|frame| { + frame.translate(center); + frame.scale(self.scaling); + frame.translate(self.translation); + frame.scale(Cell::SIZE as f32); + + let region = self.visible_region(frame.size()); + + for cell in region.cull(self.state.cells()) { + frame.fill_rectangle( + Point::new(cell.j as f32, cell.i as f32), + Size::UNIT, + Color::WHITE, + ); + } + }); + }); + + let overlay = { + let mut frame = Frame::new(bounds.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| { + frame.translate(center); + frame.scale(self.scaling); + frame.translate(self.translation); + frame.scale(Cell::SIZE as f32); + + frame.fill_rectangle( + Point::new(cell.j as f32, cell.i as f32), + Size::UNIT, + Color { + a: 0.5, + ..Color::BLACK + }, + ); + }); + } + + let text = Text { + color: Color::WHITE, + size: 14.0, + position: Point::new(frame.width(), frame.height()), + horizontal_alignment: HorizontalAlignment::Right, + vertical_alignment: VerticalAlignment::Bottom, + ..Text::default() + }; + + if let Some(cell) = hovered_cell { + frame.fill_text(Text { + content: format!("({}, {})", cell.j, cell.i), + position: text.position - Vector::new(0.0, 16.0), + ..text + }); + } + + let cell_count = self.state.cell_count(); + + frame.fill_text(Text { + content: format!( + "{} cell{} @ {:?} ({})", + cell_count, + if cell_count == 1 { "" } else { "s" }, + self.last_tick_duration, + self.last_queued_ticks + ), + ..text + }); + + frame.into_geometry() + }; + + if self.scaling < 0.2 || !self.show_lines { + vec![life, overlay] + } else { + let grid = self.grid_cache.draw(bounds.size(), |frame| { + frame.translate(center); + frame.scale(self.scaling); + frame.translate(self.translation); + frame.scale(Cell::SIZE as f32); + + let region = self.visible_region(frame.size()); + let rows = region.rows(); + let columns = region.columns(); + let (total_rows, total_columns) = + (rows.clone().count(), columns.clone().count()); + let width = 2.0 / Cell::SIZE as f32; + let color = Color::from_rgb8(70, 74, 83); + + frame.translate(Vector::new(-width / 2.0, -width / 2.0)); + + for row in region.rows() { + frame.fill_rectangle( + Point::new(*columns.start() as f32, row as f32), + Size::new(total_columns as f32, width), + color, + ); + } + + for column in region.columns() { + frame.fill_rectangle( + Point::new(column as f32, *rows.start() as f32), + Size::new(width, total_rows as f32), + color, + ); + } + }); + + vec![life, grid, overlay] + } + } + + fn mouse_interaction( + &self, + bounds: Rectangle, + cursor: Cursor, + ) -> mouse::Interaction { + match self.interaction { + Interaction::Drawing => mouse::Interaction::Crosshair, + Interaction::Erasing => mouse::Interaction::Crosshair, + Interaction::Panning { .. } => mouse::Interaction::Grabbing, + Interaction::None if cursor.is_over(&bounds) => { + mouse::Interaction::Crosshair + } + _ => mouse::Interaction::default(), + } + } + } + + #[derive(Default)] + struct State { + life: Life, + births: FxHashSet<Cell>, + is_ticking: bool, + } + + impl State { + fn cell_count(&self) -> usize { + self.life.len() + self.births.len() + } + + fn contains(&self, cell: &Cell) -> bool { + self.life.contains(cell) || self.births.contains(cell) + } + + fn cells(&self) -> impl Iterator<Item = &Cell> { + self.life.iter().chain(self.births.iter()) + } + + fn populate(&mut self, cell: Cell) { + if self.is_ticking { + self.births.insert(cell); + } else { + self.life.populate(cell); + } + } + + fn unpopulate(&mut self, cell: &Cell) { + if self.is_ticking { + let _ = self.births.remove(cell); + } else { + self.life.unpopulate(cell); + } + } + + fn update(&mut self, mut life: Life) { + self.births.drain().for_each(|cell| life.populate(cell)); + + self.life = life; + self.is_ticking = false; + } + + fn tick( + &mut self, + amount: usize, + ) -> Option<impl Future<Output = Result<Life, TickError>>> { + if self.is_ticking { + return None; + } + + self.is_ticking = true; + + let mut life = self.life.clone(); + + Some(async move { + tokio::task::spawn_blocking(move || { + for _ in 0..amount { + life.tick(); + } + + life + }) + .await + .map_err(|_| TickError::JoinFailed) + }) + } + } + + #[derive(Clone, Default)] + pub struct Life { + cells: FxHashSet<Cell>, + } + + impl Life { + fn len(&self) -> usize { + self.cells.len() + } + + fn contains(&self, cell: &Cell) -> bool { + self.cells.contains(cell) + } + + fn populate(&mut self, cell: Cell) { + self.cells.insert(cell); + } + + fn unpopulate(&mut self, cell: &Cell) { + let _ = self.cells.remove(cell); + } + + fn tick(&mut self) { + let mut adjacent_life = FxHashMap::default(); + + for cell in &self.cells { + let _ = adjacent_life.entry(*cell).or_insert(0); + + for neighbor in Cell::neighbors(*cell) { + let amount = adjacent_life.entry(neighbor).or_insert(0); + + *amount += 1; + } + } + + for (cell, amount) in adjacent_life.iter() { + match amount { + 2 => {} + 3 => { + let _ = self.cells.insert(*cell); + } + _ => { + let _ = self.cells.remove(cell); + } + } + } + } + + pub fn iter(&self) -> impl Iterator<Item = &Cell> { + self.cells.iter() + } + } + + impl std::fmt::Debug for Life { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Life") + .field("cells", &self.cells.len()) + .finish() + } + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct Cell { + i: isize, + j: isize, + } + + impl Cell { + const SIZE: usize = 20; + + fn at(position: Point) -> Cell { + let i = (position.y / Cell::SIZE as f32).ceil() as isize; + let j = (position.x / Cell::SIZE as f32).ceil() as isize; + + Cell { + i: i.saturating_sub(1), + j: j.saturating_sub(1), + } + } + + fn cluster(cell: Cell) -> impl Iterator<Item = Cell> { + use itertools::Itertools; + + let rows = cell.i.saturating_sub(1)..=cell.i.saturating_add(1); + let columns = cell.j.saturating_sub(1)..=cell.j.saturating_add(1); + + rows.cartesian_product(columns).map(|(i, j)| Cell { i, j }) + } + + fn neighbors(cell: Cell) -> impl Iterator<Item = Cell> { + Cell::cluster(cell).filter(move |candidate| *candidate != cell) + } + } + + pub struct Region { + x: f32, + y: f32, + width: f32, + height: f32, + } + + impl Region { + fn rows(&self) -> RangeInclusive<isize> { + let first_row = (self.y / Cell::SIZE as f32).floor() as isize; + + let visible_rows = + (self.height / Cell::SIZE as f32).ceil() as isize; + + first_row..=first_row + visible_rows + } + + fn columns(&self) -> RangeInclusive<isize> { + let first_column = (self.x / Cell::SIZE as f32).floor() as isize; + + let visible_columns = + (self.width / Cell::SIZE as f32).ceil() as isize; + + first_column..=first_column + visible_columns + } + + fn cull<'a>( + &self, + cells: impl Iterator<Item = &'a Cell>, + ) -> impl Iterator<Item = &'a Cell> { + let rows = self.rows(); + let columns = self.columns(); + + cells.filter(move |cell| { + rows.contains(&cell.i) && columns.contains(&cell.j) + }) + } + } + + enum Interaction { + None, + Drawing, + Erasing, + Panning { translation: Vector, start: Point }, + } +} + +#[derive(Default)] +struct Controls { + toggle_button: button::State, + next_button: button::State, + clear_button: button::State, + speed_slider: slider::State, +} + +impl Controls { + fn view<'a>( + &'a mut self, + is_playing: bool, + is_grid_enabled: bool, + speed: usize, + ) -> Element<'a, Message> { + let playback_controls = Row::new() + .spacing(10) + .push( + Button::new( + &mut self.toggle_button, + Text::new(if is_playing { "Pause" } else { "Play" }), + ) + .on_press(Message::TogglePlayback) + .style(style::Button), + ) + .push( + Button::new(&mut self.next_button, Text::new("Next")) + .on_press(Message::Next) + .style(style::Button), + ); + + let speed_controls = Row::new() + .width(Length::Fill) + .align_items(Align::Center) + .spacing(10) + .push( + Slider::new( + &mut self.speed_slider, + 1.0..=1000.0, + speed as f32, + Message::SpeedChanged, + ) + .style(style::Slider), + ) + .push(Text::new(format!("x{}", speed)).size(16)); + + Row::new() + .padding(10) + .spacing(20) + .align_items(Align::Center) + .push(playback_controls) + .push(speed_controls) + .push( + Checkbox::new(is_grid_enabled, "Grid", Message::ToggleGrid) + .size(16) + .spacing(5) + .text_size(16), + ) + .push( + Button::new(&mut self.clear_button, Text::new("Clear")) + .on_press(Message::Clear) + .style(style::Clear), + ) + .into() + } +} diff --git a/examples/game_of_life/src/style.rs b/examples/game_of_life/src/style.rs new file mode 100644 index 00000000..d59569f2 --- /dev/null +++ b/examples/game_of_life/src/style.rs @@ -0,0 +1,134 @@ +use iced::{button, container, slider, Background, Color}; + +const ACTIVE: Color = Color::from_rgb( + 0x72 as f32 / 255.0, + 0x89 as f32 / 255.0, + 0xDA as f32 / 255.0, +); + +const DESTRUCTIVE: Color = Color::from_rgb( + 0xC0 as f32 / 255.0, + 0x47 as f32 / 255.0, + 0x47 as f32 / 255.0, +); + +const HOVERED: Color = Color::from_rgb( + 0x67 as f32 / 255.0, + 0x7B as f32 / 255.0, + 0xC4 as f32 / 255.0, +); + +pub struct Container; + +impl container::StyleSheet for Container { + fn style(&self) -> container::Style { + container::Style { + background: Some(Background::Color(Color::from_rgb8( + 0x36, 0x39, 0x3F, + ))), + text_color: Some(Color::WHITE), + ..container::Style::default() + } + } +} + +pub struct Button; + +impl button::StyleSheet for Button { + fn active(&self) -> button::Style { + button::Style { + background: Some(Background::Color(ACTIVE)), + border_radius: 3, + text_color: Color::WHITE, + ..button::Style::default() + } + } + + fn hovered(&self) -> button::Style { + button::Style { + background: Some(Background::Color(HOVERED)), + text_color: Color::WHITE, + ..self.active() + } + } + + fn pressed(&self) -> button::Style { + button::Style { + border_width: 1, + border_color: Color::WHITE, + ..self.hovered() + } + } +} + +pub struct Clear; + +impl button::StyleSheet for Clear { + fn active(&self) -> button::Style { + button::Style { + background: Some(Background::Color(DESTRUCTIVE)), + border_radius: 3, + text_color: Color::WHITE, + ..button::Style::default() + } + } + + fn hovered(&self) -> button::Style { + button::Style { + background: Some(Background::Color(Color { + a: 0.5, + ..DESTRUCTIVE + })), + text_color: Color::WHITE, + ..self.active() + } + } + + fn pressed(&self) -> button::Style { + button::Style { + border_width: 1, + border_color: Color::WHITE, + ..self.hovered() + } + } +} + +pub struct Slider; + +impl slider::StyleSheet for Slider { + fn active(&self) -> slider::Style { + slider::Style { + rail_colors: (ACTIVE, Color { a: 0.1, ..ACTIVE }), + handle: slider::Handle { + shape: slider::HandleShape::Circle { radius: 9 }, + color: ACTIVE, + border_width: 0, + border_color: Color::TRANSPARENT, + }, + } + } + + fn hovered(&self) -> slider::Style { + let active = self.active(); + + slider::Style { + handle: slider::Handle { + color: HOVERED, + ..active.handle + }, + ..active + } + } + + fn dragging(&self) -> slider::Style { + let active = self.active(); + + slider::Style { + handle: slider::Handle { + color: Color::from_rgb(0.85, 0.85, 0.85), + ..active.handle + }, + ..active + } + } +} diff --git a/examples/geometry/Cargo.toml b/examples/geometry/Cargo.toml index 9df52454..34eec4fb 100644 --- a/examples/geometry/Cargo.toml +++ b/examples/geometry/Cargo.toml @@ -8,4 +8,4 @@ publish = false [dependencies] iced = { path = "../.." } iced_native = { path = "../../native" } -iced_wgpu = { path = "../../wgpu" } +iced_graphics = { path = "../../graphics" } diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index 13a687ab..71ce0d8c 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -10,14 +10,14 @@ mod rainbow { // Of course, you can choose to make the implementation renderer-agnostic, // if you wish to, by creating your own `Renderer` trait, which could be // implemented by `iced_wgpu` and other renderers. + use iced_graphics::{ + triangle::{Mesh2D, Vertex2D}, + Backend, Defaults, Primitive, Renderer, + }; use iced_native::{ - layout, Element, Hasher, Layout, Length, MouseCursor, Point, Size, + layout, mouse, Element, Hasher, Layout, Length, Point, Size, Vector, Widget, }; - use iced_wgpu::{ - triangle::{Mesh2D, Vertex2D}, - Defaults, Primitive, Renderer, - }; pub struct Rainbow; @@ -27,7 +27,10 @@ mod rainbow { } } - impl<Message> Widget<Message, Renderer> for Rainbow { + impl<Message, B> Widget<Message, Renderer<B>> for Rainbow + where + B: Backend, + { fn width(&self) -> Length { Length::Fill } @@ -38,7 +41,7 @@ mod rainbow { fn layout( &self, - _renderer: &Renderer, + _renderer: &Renderer<B>, limits: &layout::Limits, ) -> layout::Node { let size = limits.width(Length::Fill).resolve(Size::ZERO); @@ -50,11 +53,11 @@ mod rainbow { fn draw( &self, - _renderer: &mut Renderer, + _renderer: &mut Renderer<B>, _defaults: &Defaults, layout: Layout<'_>, cursor_position: Point, - ) -> (Primitive, MouseCursor) { + ) -> (Primitive, mouse::Interaction) { let b = layout.bounds(); // R O Y G B I V @@ -85,66 +88,72 @@ mod rainbow { let posn_l = [0.0, b.height / 2.0]; ( - Primitive::Mesh2D { - origin: Point::new(b.x, b.y), - buffers: Mesh2D { - vertices: vec![ - Vertex2D { - position: posn_center, - color: [1.0, 1.0, 1.0, 1.0], - }, - Vertex2D { - position: posn_tl, - color: color_r, - }, - Vertex2D { - position: posn_t, - color: color_o, - }, - Vertex2D { - position: posn_tr, - color: color_y, - }, - Vertex2D { - position: posn_r, - color: color_g, - }, - Vertex2D { - position: posn_br, - color: color_gb, - }, - Vertex2D { - position: posn_b, - color: color_b, - }, - Vertex2D { - position: posn_bl, - color: color_i, - }, - Vertex2D { - position: posn_l, - color: color_v, - }, - ], - indices: vec![ - 0, 1, 2, // TL - 0, 2, 3, // T - 0, 3, 4, // TR - 0, 4, 5, // R - 0, 5, 6, // BR - 0, 6, 7, // B - 0, 7, 8, // BL - 0, 8, 1, // L - ], - }, + Primitive::Translate { + translation: Vector::new(b.x, b.y), + content: Box::new(Primitive::Mesh2D { + size: b.size(), + buffers: Mesh2D { + vertices: vec![ + Vertex2D { + position: posn_center, + color: [1.0, 1.0, 1.0, 1.0], + }, + Vertex2D { + position: posn_tl, + color: color_r, + }, + Vertex2D { + position: posn_t, + color: color_o, + }, + Vertex2D { + position: posn_tr, + color: color_y, + }, + Vertex2D { + position: posn_r, + color: color_g, + }, + Vertex2D { + position: posn_br, + color: color_gb, + }, + Vertex2D { + position: posn_b, + color: color_b, + }, + Vertex2D { + position: posn_bl, + color: color_i, + }, + Vertex2D { + position: posn_l, + color: color_v, + }, + ], + indices: vec![ + 0, 1, 2, // TL + 0, 2, 3, // T + 0, 3, 4, // TR + 0, 4, 5, // R + 0, 5, 6, // BR + 0, 6, 7, // B + 0, 7, 8, // BL + 0, 8, 1, // L + ], + }, + }), }, - MouseCursor::OutOfBounds, + mouse::Interaction::default(), ) } } - impl<'a, Message> Into<Element<'a, Message, Renderer>> for Rainbow { - fn into(self) -> Element<'a, Message, Renderer> { + impl<'a, Message, B> Into<Element<'a, Message, Renderer<B>>> for Rainbow + where + B: Backend, + { + fn into(self) -> Element<'a, Message, Renderer<B>> { Element::new(self) } } diff --git a/examples/integration/src/controls.rs b/examples/integration/src/controls.rs index 0999336b..824f9f53 100644 --- a/examples/integration/src/controls.rs +++ b/examples/integration/src/controls.rs @@ -1,15 +1,15 @@ -use crate::Scene; - use iced_wgpu::Renderer; use iced_winit::{ - slider, Align, Color, Column, Element, Length, Row, Slider, Text, + slider, Align, Color, Column, Command, Element, Length, Program, Row, + Slider, Text, }; pub struct Controls { + background_color: Color, sliders: [slider::State; 3], } -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum Message { BackgroundColorChanged(Color), } @@ -17,58 +17,64 @@ pub enum Message { impl Controls { pub fn new() -> Controls { Controls { + background_color: Color::BLACK, sliders: Default::default(), } } - pub fn update(&self, message: Message, scene: &mut Scene) { + pub fn background_color(&self) -> Color { + self.background_color + } +} + +impl Program for Controls { + type Renderer = Renderer; + type Message = Message; + + fn update(&mut self, message: Message) -> Command<Message> { match message { Message::BackgroundColorChanged(color) => { - scene.background_color = color; + self.background_color = color; } } + + Command::none() } - pub fn view(&mut self, scene: &Scene) -> Element<Message, Renderer> { + fn view(&mut self) -> Element<Message, Renderer> { let [r, g, b] = &mut self.sliders; - let background_color = scene.background_color; + let background_color = self.background_color; let sliders = Row::new() .width(Length::Units(500)) .spacing(20) - .push(Slider::new( - r, - 0.0..=1.0, - scene.background_color.r, - move |r| { + .push( + Slider::new(r, 0.0..=1.0, background_color.r, move |r| { Message::BackgroundColorChanged(Color { r, ..background_color }) - }, - )) - .push(Slider::new( - g, - 0.0..=1.0, - scene.background_color.g, - move |g| { + }) + .step(0.01), + ) + .push( + Slider::new(g, 0.0..=1.0, background_color.g, move |g| { Message::BackgroundColorChanged(Color { g, ..background_color }) - }, - )) - .push(Slider::new( - b, - 0.0..=1.0, - scene.background_color.b, - move |b| { + }) + .step(0.01), + ) + .push( + Slider::new(b, 0.0..=1.0, background_color.b, move |b| { Message::BackgroundColorChanged(Color { b, ..background_color }) - }, - )); + }) + .step(0.01), + ); Row::new() .width(Length::Fill) diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index 7203d4b6..33d4c361 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -4,14 +4,11 @@ mod scene; use controls::Controls; use scene::Scene; -use iced_wgpu::{ - wgpu, window::SwapChain, Primitive, Renderer, Settings, Target, -}; -use iced_winit::{ - futures, winit, Cache, Clipboard, MouseCursor, Size, UserInterface, -}; +use iced_wgpu::{wgpu, Backend, Renderer, Settings, Viewport}; +use iced_winit::{conversion, futures, program, winit, Debug, Size}; use winit::{ + dpi::PhysicalPosition, event::{Event, ModifiersState, WindowEvent}, event_loop::{ControlFlow, EventLoop}, }; @@ -22,12 +19,16 @@ pub fn main() { // Initialize winit let event_loop = EventLoop::new(); let window = winit::window::Window::new(&event_loop).unwrap(); - let mut logical_size = - window.inner_size().to_logical(window.scale_factor()); - let mut modifiers = ModifiersState::default(); - // Initialize WGPU + let physical_size = window.inner_size(); + let mut viewport = Viewport::with_physical_size( + Size::new(physical_size.width, physical_size.height), + window.scale_factor(), + ); + let mut cursor_position = PhysicalPosition::new(-1.0, -1.0); + let mut modifiers = ModifiersState::default(); + // Initialize wgpu let surface = wgpu::Surface::create(&window); let (mut device, queue) = futures::executor::block_on(async { let adapter = wgpu::Adapter::request( @@ -55,20 +56,35 @@ pub fn main() { let mut swap_chain = { let size = window.inner_size(); - SwapChain::new(&device, &surface, format, size.width, size.height) + device.create_swap_chain( + &surface, + &wgpu::SwapChainDescriptor { + usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT, + format: format, + width: size.width, + height: size.height, + present_mode: wgpu::PresentMode::Mailbox, + }, + ) }; let mut resized = false; - // Initialize iced - let mut events = Vec::new(); - let mut cache = Some(Cache::default()); - let mut renderer = Renderer::new(&mut device, Settings::default()); - let mut output = (Primitive::None, MouseCursor::OutOfBounds); - let clipboard = Clipboard::new(&window); - // Initialize scene and GUI controls - let mut scene = Scene::new(&device); - let mut controls = Controls::new(); + let scene = Scene::new(&mut device); + let controls = Controls::new(); + + // Initialize iced + let mut debug = Debug::new(); + let mut renderer = + Renderer::new(Backend::new(&mut device, Settings::default())); + + let mut state = program::State::new( + controls, + viewport.logical_size(), + conversion::cursor_position(cursor_position, viewport.scale_factor()), + &mut renderer, + &mut debug, + ); // Run event loop event_loop.run(move |event, _, control_flow| { @@ -78,18 +94,23 @@ pub fn main() { match event { Event::WindowEvent { event, .. } => { match event { + WindowEvent::CursorMoved { position, .. } => { + cursor_position = position; + } WindowEvent::ModifiersChanged(new_modifiers) => { modifiers = new_modifiers; } WindowEvent::Resized(new_size) => { - logical_size = - new_size.to_logical(window.scale_factor()); + viewport = Viewport::with_physical_size( + Size::new(new_size.width, new_size.height), + window.scale_factor(), + ); + resized = true; } WindowEvent::CloseRequested => { *control_flow = ControlFlow::Exit; } - _ => {} } @@ -99,115 +120,85 @@ pub fn main() { window.scale_factor(), modifiers, ) { - events.push(event); + state.queue_event(event); } } Event::MainEventsCleared => { - // If no relevant events happened, we can simply skip this - if events.is_empty() { - return; - } - - // We need to: - // 1. Process events of our user interface. - // 2. Update state as a result of any interaction. - // 3. Generate a new output for our renderer. - - // First, we build our user interface. - let mut user_interface = UserInterface::build( - controls.view(&scene), - Size::new(logical_size.width, logical_size.height), - cache.take().unwrap(), - &mut renderer, - ); - - // Then, we process the events, obtaining messages in return. - let messages = user_interface.update( - events.drain(..), - clipboard.as_ref().map(|c| c as _), - &renderer, - ); - - let user_interface = if messages.is_empty() { - // If there are no messages, no interactions we care about have - // happened. We can simply leave our user interface as it is. - user_interface - } else { - // If there are messages, we need to update our state - // accordingly and rebuild our user interface. - // We can only do this if we drop our user interface first - // by turning it into its cache. - cache = Some(user_interface.into_cache()); - - // In this example, `Controls` is the only part that cares - // about messages, so updating our state is pretty - // straightforward. - for message in messages { - controls.update(message, &mut scene); - } - - // Once the state has been changed, we rebuild our updated - // user interface. - UserInterface::build( - controls.view(&scene), - Size::new(logical_size.width, logical_size.height), - cache.take().unwrap(), + // If there are events pending + if !state.is_queue_empty() { + // We update iced + let _ = state.update( + viewport.logical_size(), + conversion::cursor_position( + cursor_position, + viewport.scale_factor(), + ), + None, &mut renderer, - ) - }; - - // Finally, we just need to draw a new output for our renderer, - output = user_interface.draw(&mut renderer); - - // update our cache, - cache = Some(user_interface.into_cache()); + &mut debug, + ); - // and request a redraw - window.request_redraw(); + // and request a redraw + window.request_redraw(); + } } Event::RedrawRequested(_) => { if resized { let size = window.inner_size(); - swap_chain = SwapChain::new( - &device, + swap_chain = device.create_swap_chain( &surface, - format, - size.width, - size.height, + &wgpu::SwapChainDescriptor { + usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT, + format: format, + width: size.width, + height: size.height, + present_mode: wgpu::PresentMode::Mailbox, + }, ); + + resized = false; } - let (frame, viewport) = - swap_chain.next_frame().expect("Next frame"); + let frame = swap_chain.get_next_texture().expect("Next frame"); let mut encoder = device.create_command_encoder( &wgpu::CommandEncoderDescriptor { label: None }, ); - // We draw the scene first - scene.draw(&mut encoder, &frame.view); + let program = state.program(); + + { + // We clear the frame + let mut render_pass = scene.clear( + &frame.view, + &mut encoder, + program.background_color(), + ); + + // Draw the scene + scene.draw(&mut render_pass); + } // And then iced on top - let mouse_cursor = renderer.draw( + let mouse_interaction = renderer.backend_mut().draw( &mut device, &mut encoder, - Target { - texture: &frame.view, - viewport, - }, - &output, - window.scale_factor(), - &["Some debug information!"], + &frame.view, + &viewport, + state.primitive(), + &debug.overlay(), ); // Then we submit the work queue.submit(&[encoder.finish()]); // And update the mouse cursor - window.set_cursor_icon(iced_winit::conversion::mouse_cursor( - mouse_cursor, - )); + window.set_cursor_icon( + iced_winit::conversion::mouse_interaction( + mouse_interaction, + ), + ); } _ => {} } diff --git a/examples/integration/src/scene.rs b/examples/integration/src/scene.rs index 22c6812a..74cbb925 100644 --- a/examples/integration/src/scene.rs +++ b/examples/integration/src/scene.rs @@ -2,60 +2,50 @@ use iced_wgpu::wgpu; use iced_winit::Color; pub struct Scene { - pub background_color: Color, pipeline: wgpu::RenderPipeline, - bind_group: wgpu::BindGroup, } impl Scene { pub fn new(device: &wgpu::Device) -> Scene { - let (pipeline, bind_group) = build_pipeline(device); + let pipeline = build_pipeline(device); - Scene { - background_color: Color::BLACK, - pipeline, - bind_group, - } + Scene { pipeline } } - pub fn draw( + pub fn clear<'a>( &self, - encoder: &mut wgpu::CommandEncoder, - target: &wgpu::TextureView, - ) { - let mut rpass = - encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - color_attachments: &[ - wgpu::RenderPassColorAttachmentDescriptor { - attachment: target, - resolve_target: None, - load_op: wgpu::LoadOp::Clear, - store_op: wgpu::StoreOp::Store, - clear_color: { - let [r, g, b, a] = - self.background_color.into_linear(); + target: &'a wgpu::TextureView, + encoder: &'a mut wgpu::CommandEncoder, + background_color: Color, + ) -> wgpu::RenderPass<'a> { + encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor { + attachment: target, + resolve_target: None, + load_op: wgpu::LoadOp::Clear, + store_op: wgpu::StoreOp::Store, + clear_color: { + let [r, g, b, a] = background_color.into_linear(); - wgpu::Color { - r: r as f64, - g: g as f64, - b: b as f64, - a: a as f64, - } - }, - }, - ], - depth_stencil_attachment: None, - }); + wgpu::Color { + r: r as f64, + g: g as f64, + b: b as f64, + a: a as f64, + } + }, + }], + depth_stencil_attachment: None, + }) + } - rpass.set_pipeline(&self.pipeline); - rpass.set_bind_group(0, &self.bind_group, &[]); - rpass.draw(0..3, 0..1); + pub fn draw<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { + render_pass.set_pipeline(&self.pipeline); + render_pass.draw(0..3, 0..1); } } -fn build_pipeline( - device: &wgpu::Device, -) -> (wgpu::RenderPipeline, wgpu::BindGroup) { +fn build_pipeline(device: &wgpu::Device) -> wgpu::RenderPipeline { let vs = include_bytes!("shader/vert.spv"); let fs = include_bytes!("shader/frag.spv"); @@ -67,21 +57,9 @@ fn build_pipeline( &wgpu::read_spirv(std::io::Cursor::new(&fs[..])).unwrap(), ); - let bind_group_layout = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: None, - bindings: &[], - }); - - let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: None, - layout: &bind_group_layout, - bindings: &[], - }); - let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - bind_group_layouts: &[&bind_group_layout], + bind_group_layouts: &[], }); let pipeline = @@ -119,5 +97,5 @@ fn build_pipeline( alpha_to_coverage_enabled: false, }); - (pipeline, bind_group) + pipeline } diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs index b4bbd68f..a821072f 100644 --- a/examples/pane_grid/src/main.rs +++ b/examples/pane_grid/src/main.rs @@ -103,7 +103,7 @@ impl Sandbox for Example { .height(Length::Fill) .spacing(10) .on_drag(Message::Dragged) - .on_resize(Message::Resized) + .on_resize(10, Message::Resized) .on_key_press(handle_hotkey); Container::new(pane_grid) diff --git a/examples/progress_bar/src/main.rs b/examples/progress_bar/src/main.rs index 43b09928..51b56eda 100644 --- a/examples/progress_bar/src/main.rs +++ b/examples/progress_bar/src/main.rs @@ -36,12 +36,15 @@ impl Sandbox for Progress { Column::new() .padding(20) .push(ProgressBar::new(0.0..=100.0, self.value)) - .push(Slider::new( - &mut self.progress_bar_slider, - 0.0..=100.0, - self.value, - Message::SliderChanged, - )) + .push( + Slider::new( + &mut self.progress_bar_slider, + 0.0..=100.0, + self.value, + Message::SliderChanged, + ) + .step(0.01), + ) .into() } } diff --git a/examples/solar_system/Cargo.toml b/examples/solar_system/Cargo.toml index c88cda50..44ced729 100644 --- a/examples/solar_system/Cargo.toml +++ b/examples/solar_system/Cargo.toml @@ -5,11 +5,6 @@ authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2018" publish = false -[features] -canvas = [] - [dependencies] -iced = { path = "../..", features = ["canvas", "async-std", "debug"] } -iced_native = { path = "../../native" } -async-std = { version = "1.0", features = ["unstable"] } +iced = { path = "../..", features = ["canvas", "tokio", "debug"] } rand = "0.7" diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index bcd1dc71..98bd3b21 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -7,8 +7,9 @@ //! //! [1]: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Basic_animations#An_animated_solar_system use iced::{ - canvas, executor, window, Application, Canvas, Color, Command, Container, - Element, Length, Point, Settings, Size, Subscription, Vector, + canvas::{self, Cursor, Path, Stroke}, + executor, time, window, Application, Canvas, Color, Command, Element, + Length, Point, Rectangle, Settings, Size, Subscription, Vector, }; use std::time::Instant; @@ -22,7 +23,6 @@ pub fn main() { struct SolarSystem { state: State, - solar_system: canvas::layer::Cache<State>, } #[derive(Debug, Clone, Copy)] @@ -39,7 +39,6 @@ impl Application for SolarSystem { ( SolarSystem { state: State::new(), - solar_system: Default::default(), }, Command::none(), ) @@ -53,7 +52,6 @@ impl Application for SolarSystem { match message { Message::Tick(instant) => { self.state.update(instant); - self.solar_system.clear(); } } @@ -66,24 +64,20 @@ impl Application for SolarSystem { } fn view(&mut self) -> Element<Message> { - let canvas = Canvas::new() + Canvas::new(&mut self.state) .width(Length::Fill) .height(Length::Fill) - .push(self.solar_system.with(&self.state)); - - Container::new(canvas) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() .into() } } #[derive(Debug)] struct State { + space_cache: canvas::Cache, + system_cache: canvas::Cache, + cursor_position: Point, start: Instant, - current: Instant, + now: Instant, stars: Vec<(Point, f32)>, } @@ -99,137 +93,122 @@ impl State { let (width, height) = window::Settings::default().size; State { + space_cache: Default::default(), + system_cache: Default::default(), + cursor_position: Point::ORIGIN, start: now, - current: now, - stars: { - use rand::Rng; - - let mut rng = rand::thread_rng(); - - (0..100) - .map(|_| { - ( - Point::new( - rng.gen_range(0.0, width as f32), - rng.gen_range(0.0, height as f32), - ), - rng.gen_range(0.5, 1.0), - ) - }) - .collect() - }, + now, + stars: Self::generate_stars(width, height), } } pub fn update(&mut self, now: Instant) { - self.current = now; + self.now = now; + self.system_cache.clear(); + } + + fn generate_stars(width: u32, height: u32) -> Vec<(Point, f32)> { + use rand::Rng; + + let mut rng = rand::thread_rng(); + + (0..100) + .map(|_| { + ( + Point::new( + rng.gen_range( + -(width as f32) / 2.0, + width as f32 / 2.0, + ), + rng.gen_range( + -(height as f32) / 2.0, + height as f32 / 2.0, + ), + ), + rng.gen_range(0.5, 1.0), + ) + }) + .collect() } } -impl canvas::Drawable for State { - fn draw(&self, frame: &mut canvas::Frame) { - use canvas::{Path, Stroke}; +impl<Message> canvas::Program<Message> for State { + fn draw( + &self, + bounds: Rectangle, + _cursor: Cursor, + ) -> Vec<canvas::Geometry> { use std::f32::consts::PI; - let center = frame.center(); + let background = self.space_cache.draw(bounds.size(), |frame| { + let space = Path::rectangle(Point::new(0.0, 0.0), frame.size()); - let space = Path::rectangle(Point::new(0.0, 0.0), frame.size()); + let stars = Path::new(|path| { + for (p, size) in &self.stars { + path.rectangle(*p, Size::new(*size, *size)); + } + }); - let stars = Path::new(|path| { - for (p, size) in &self.stars { - path.rectangle(*p, Size::new(*size, *size)); - } - }); + frame.fill(&space, Color::BLACK); - let sun = Path::circle(center, Self::SUN_RADIUS); - let orbit = Path::circle(center, Self::ORBIT_RADIUS); - - frame.fill(&space, Color::BLACK); - frame.fill(&stars, Color::WHITE); - frame.fill(&sun, Color::from_rgb8(0xF9, 0xD7, 0x1C)); - frame.stroke( - &orbit, - Stroke { - width: 1.0, - color: Color::from_rgba8(0, 153, 255, 0.1), - ..Stroke::default() - }, - ); + frame.translate(frame.center() - Point::ORIGIN); + frame.fill(&stars, Color::WHITE); + }); - let elapsed = self.current - self.start; - let elapsed_seconds = elapsed.as_secs() as f32; - let elapsed_millis = elapsed.subsec_millis() as f32; + let system = self.system_cache.draw(bounds.size(), |frame| { + let center = frame.center(); - frame.with_save(|frame| { - frame.translate(Vector::new(center.x, center.y)); - frame.rotate( - (2.0 * PI / 60.0) * elapsed_seconds - + (2.0 * PI / 60_000.0) * elapsed_millis, - ); - frame.translate(Vector::new(Self::ORBIT_RADIUS, 0.0)); + let sun = Path::circle(center, Self::SUN_RADIUS); + let orbit = Path::circle(center, Self::ORBIT_RADIUS); - let earth = Path::circle(Point::ORIGIN, Self::EARTH_RADIUS); - let shadow = Path::rectangle( - Point::new(0.0, -Self::EARTH_RADIUS), - Size::new(Self::EARTH_RADIUS * 4.0, Self::EARTH_RADIUS * 2.0), + frame.fill(&sun, Color::from_rgb8(0xF9, 0xD7, 0x1C)); + frame.stroke( + &orbit, + Stroke { + width: 1.0, + color: Color::from_rgba8(0, 153, 255, 0.1), + ..Stroke::default() + }, ); - frame.fill(&earth, Color::from_rgb8(0x6B, 0x93, 0xD6)); + let elapsed = self.now - self.start; + let rotation = (2.0 * PI / 60.0) * elapsed.as_secs() as f32 + + (2.0 * PI / 60_000.0) * elapsed.subsec_millis() as f32; frame.with_save(|frame| { - frame.rotate( - ((2.0 * PI) / 6.0) * elapsed_seconds - + ((2.0 * PI) / 6_000.0) * elapsed_millis, + frame.translate(Vector::new(center.x, center.y)); + frame.rotate(rotation); + frame.translate(Vector::new(Self::ORBIT_RADIUS, 0.0)); + + let earth = Path::circle(Point::ORIGIN, Self::EARTH_RADIUS); + let shadow = Path::rectangle( + Point::new(0.0, -Self::EARTH_RADIUS), + Size::new( + Self::EARTH_RADIUS * 4.0, + Self::EARTH_RADIUS * 2.0, + ), ); - frame.translate(Vector::new(0.0, Self::MOON_DISTANCE)); - - let moon = Path::circle(Point::ORIGIN, Self::MOON_RADIUS); - frame.fill(&moon, Color::WHITE); - }); - frame.fill( - &shadow, - Color { - a: 0.7, - ..Color::BLACK - }, - ); - }); - } -} - -mod time { - use iced::futures; - use std::time::Instant; - - pub fn every(duration: std::time::Duration) -> iced::Subscription<Instant> { - iced::Subscription::from_recipe(Every(duration)) - } + frame.fill(&earth, Color::from_rgb8(0x6B, 0x93, 0xD6)); - struct Every(std::time::Duration); + frame.with_save(|frame| { + frame.rotate(rotation * 10.0); + frame.translate(Vector::new(0.0, Self::MOON_DISTANCE)); - impl<H, I> iced_native::subscription::Recipe<H, I> for Every - where - H: std::hash::Hasher, - { - type Output = Instant; + let moon = Path::circle(Point::ORIGIN, Self::MOON_RADIUS); + frame.fill(&moon, Color::WHITE); + }); - fn hash(&self, state: &mut H) { - use std::hash::Hash; - - std::any::TypeId::of::<Self>().hash(state); - self.0.hash(state); - } - - fn stream( - self: Box<Self>, - _input: futures::stream::BoxStream<'static, I>, - ) -> futures::stream::BoxStream<'static, Self::Output> { - use futures::stream::StreamExt; + frame.fill( + &shadow, + Color { + a: 0.7, + ..Color::BLACK + }, + ); + }); + }); - async_std::stream::interval(self.0) - .map(|_| Instant::now()) - .boxed() - } + vec![background, system] } } diff --git a/examples/stopwatch/Cargo.toml b/examples/stopwatch/Cargo.toml index 1dae3b83..075aa111 100644 --- a/examples/stopwatch/Cargo.toml +++ b/examples/stopwatch/Cargo.toml @@ -6,7 +6,4 @@ edition = "2018" publish = false [dependencies] -iced = { path = "../.." } -iced_native = { path = "../../native" } -iced_futures = { path = "../../futures", features = ["async-std"] } -async-std = { version = "1.0", features = ["unstable"] } +iced = { path = "../..", features = ["tokio"] } diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs index 5a54ed2b..9de6d39e 100644 --- a/examples/stopwatch/src/main.rs +++ b/examples/stopwatch/src/main.rs @@ -1,6 +1,7 @@ use iced::{ - button, Align, Application, Button, Column, Command, Container, Element, - HorizontalAlignment, Length, Row, Settings, Subscription, Text, + button, executor, time, Align, Application, Button, Column, Command, + Container, Element, HorizontalAlignment, Length, Row, Settings, + Subscription, Text, }; use std::time::{Duration, Instant}; @@ -28,7 +29,7 @@ enum Message { } impl Application for Stopwatch { - type Executor = iced_futures::executor::AsyncStd; + type Executor = executor::Default; type Message = Message; type Flags = (); @@ -143,43 +144,6 @@ impl Application for Stopwatch { } } -mod time { - use iced::futures; - - pub fn every( - duration: std::time::Duration, - ) -> iced::Subscription<std::time::Instant> { - iced::Subscription::from_recipe(Every(duration)) - } - - struct Every(std::time::Duration); - - impl<H, I> iced_native::subscription::Recipe<H, I> for Every - where - H: std::hash::Hasher, - { - type Output = std::time::Instant; - - fn hash(&self, state: &mut H) { - use std::hash::Hash; - - std::any::TypeId::of::<Self>().hash(state); - self.0.hash(state); - } - - fn stream( - self: Box<Self>, - _input: futures::stream::BoxStream<'static, I>, - ) -> futures::stream::BoxStream<'static, Self::Output> { - use futures::stream::StreamExt; - - async_std::stream::interval(self.0) - .map(|_| std::time::Instant::now()) - .boxed() - } - } -} - mod style { use iced::{button, Background, Color, Vector}; diff --git a/examples/todos/Cargo.toml b/examples/todos/Cargo.toml index f945cde5..b236cc0d 100644 --- a/examples/todos/Cargo.toml +++ b/examples/todos/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" publish = false [dependencies] -iced = { path = "../..", features = ["async-std"] } +iced = { path = "../..", features = ["async-std", "debug"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index c9678b9d..4f8a4b32 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -190,7 +190,7 @@ enum Step { Welcome, Slider { state: slider::State, - value: u16, + value: u8, }, RowsAndColumns { layout: Layout, @@ -222,13 +222,13 @@ enum Step { #[derive(Debug, Clone)] pub enum StepMessage { - SliderChanged(f32), + SliderChanged(u8), LayoutChanged(Layout), - SpacingChanged(f32), - TextSizeChanged(f32), + SpacingChanged(u16), + TextSizeChanged(u16), TextColorChanged(Color), LanguageSelected(Language), - ImageWidthChanged(f32), + ImageWidthChanged(u16), InputChanged(String), ToggleSecureInput(bool), DebugToggled(bool), @@ -249,12 +249,12 @@ impl<'a> Step { } StepMessage::SliderChanged(new_value) => { if let Step::Slider { value, .. } = self { - *value = new_value.round() as u16; + *value = new_value; } } StepMessage::TextSizeChanged(new_size) => { if let Step::Text { size, .. } = self { - *size = new_size.round() as u16; + *size = new_size; } } StepMessage::TextColorChanged(new_color) => { @@ -269,12 +269,12 @@ impl<'a> Step { } StepMessage::SpacingChanged(new_spacing) => { if let Step::RowsAndColumns { spacing, .. } = self { - *spacing = new_spacing.round() as u16; + *spacing = new_spacing; } } StepMessage::ImageWidthChanged(new_width) => { if let Step::Image { width, .. } = self { - *width = new_width.round() as u16; + *width = new_width; } } StepMessage::InputChanged(new_value) => { @@ -384,7 +384,7 @@ impl<'a> Step { fn slider( state: &'a mut slider::State, - value: u16, + value: u8, ) -> Column<'a, StepMessage> { Self::container("Slider") .push(Text::new( @@ -397,8 +397,8 @@ impl<'a> Step { )) .push(Slider::new( state, - 0.0..=100.0, - value as f32, + 0..=100, + value, StepMessage::SliderChanged, )) .push( @@ -444,8 +444,8 @@ impl<'a> Step { .spacing(10) .push(Slider::new( spacing_slider, - 0.0..=80.0, - spacing as f32, + 0..=80, + spacing, StepMessage::SpacingChanged, )) .push( @@ -486,30 +486,25 @@ impl<'a> Step { ) .push(Slider::new( size_slider, - 10.0..=70.0, - size as f32, + 10..=70, + size, StepMessage::TextSizeChanged, )); let [red, green, blue] = color_sliders; + + let color_sliders = Row::new() + .spacing(10) + .push(color_slider(red, color.r, move |r| Color { r, ..color })) + .push(color_slider(green, color.g, move |g| Color { g, ..color })) + .push(color_slider(blue, color.b, move |b| Color { b, ..color })); + let color_section = Column::new() .padding(20) .spacing(20) .push(Text::new("And its color:")) .push(Text::new(&format!("{:?}", color)).color(color)) - .push( - Row::new() - .spacing(10) - .push(Slider::new(red, 0.0..=1.0, color.r, move |r| { - StepMessage::TextColorChanged(Color { r, ..color }) - })) - .push(Slider::new(green, 0.0..=1.0, color.g, move |g| { - StepMessage::TextColorChanged(Color { g, ..color }) - })) - .push(Slider::new(blue, 0.0..=1.0, color.b, move |b| { - StepMessage::TextColorChanged(Color { b, ..color }) - })), - ); + .push(color_sliders); Self::container("Text") .push(Text::new( @@ -559,8 +554,8 @@ impl<'a> Step { .push(ferris(width)) .push(Slider::new( slider, - 100.0..=500.0, - width as f32, + 100..=500, + width, StepMessage::ImageWidthChanged, )) .push( @@ -706,6 +701,17 @@ fn button<'a, Message>( .min_width(100) } +fn color_slider( + state: &mut slider::State, + component: f32, + update: impl Fn(f32) -> Color + 'static, +) -> Slider<f64, StepMessage> { + Slider::new(state, 0.0..=1.0, f64::from(component), move |c| { + StepMessage::TextColorChanged(update(c as f32)) + }) + .step(0.01) +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Language { Rust, diff --git a/futures/Cargo.toml b/futures/Cargo.toml index e0815d9d..275d0391 100644 --- a/futures/Cargo.toml +++ b/futures/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_futures" -version = "0.1.1" +version = "0.1.2" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2018" description = "Commands, subscriptions, and runtimes for Iced" @@ -22,11 +22,12 @@ version = "0.3" [target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio] version = "0.2" optional = true -features = ["rt-core", "rt-threaded"] +features = ["rt-core", "rt-threaded", "time", "stream"] [target.'cfg(not(target_arch = "wasm32"))'.dependencies.async-std] version = "1.0" optional = true +features = ["unstable"] [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen-futures = "0.4" diff --git a/futures/src/command.rs b/futures/src/command.rs index d4f99b82..063e9b68 100644 --- a/futures/src/command.rs +++ b/futures/src/command.rs @@ -27,6 +27,7 @@ impl<T> Command<T> { /// Creates a [`Command`] that performs the action of the given future. /// /// [`Command`]: struct.Command.html + #[cfg(not(target_arch = "wasm32"))] pub fn perform<A>( future: impl Future<Output = T> + 'static + Send, f: impl Fn(T) -> A + 'static + Send, @@ -36,9 +37,23 @@ impl<T> Command<T> { } } + /// Creates a [`Command`] that performs the action of the given future. + /// + /// [`Command`]: struct.Command.html + #[cfg(target_arch = "wasm32")] + pub fn perform<A>( + future: impl Future<Output = T> + 'static, + f: impl Fn(T) -> A + 'static + Send, + ) -> Command<A> { + Command { + futures: vec![Box::pin(future.map(f))], + } + } + /// Applies a transformation to the result of a [`Command`]. /// /// [`Command`]: struct.Command.html + #[cfg(not(target_arch = "wasm32"))] pub fn map<A>( mut self, f: impl Fn(T) -> A + 'static + Send + Sync, @@ -62,6 +77,30 @@ impl<T> Command<T> { } } + /// Applies a transformation to the result of a [`Command`]. + /// + /// [`Command`]: struct.Command.html + #[cfg(target_arch = "wasm32")] + pub fn map<A>(mut self, f: impl Fn(T) -> A + 'static) -> Command<A> + where + T: 'static, + { + let f = std::rc::Rc::new(f); + + Command { + futures: self + .futures + .drain(..) + .map(|future| { + let f = f.clone(); + + Box::pin(future.map(move |result| f(result))) + as BoxFuture<A> + }) + .collect(), + } + } + /// Creates a [`Command`] that performs the actions of all the given /// commands. /// @@ -85,6 +124,7 @@ impl<T> Command<T> { } } +#[cfg(not(target_arch = "wasm32"))] impl<T, A> From<A> for Command<T> where A: Future<Output = T> + 'static + Send, @@ -96,6 +136,18 @@ where } } +#[cfg(target_arch = "wasm32")] +impl<T, A> From<A> for Command<T> +where + A: Future<Output = T> + 'static, +{ + fn from(future: A) -> Self { + Self { + futures: vec![future.boxed_local()], + } + } +} + impl<T> std::fmt::Debug for Command<T> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Command").finish() diff --git a/futures/src/lib.rs b/futures/src/lib.rs index 966a9cdc..46fc59fc 100644 --- a/futures/src/lib.rs +++ b/futures/src/lib.rs @@ -14,6 +14,13 @@ mod runtime; pub mod executor; pub mod subscription; +#[cfg(all( + any(feature = "tokio", feature = "async-std"), + not(target_arch = "wasm32") +))] +#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio", feature = "async-std"))))] +pub mod time; + pub use command::Command; pub use executor::Executor; pub use runtime::Runtime; diff --git a/futures/src/time.rs b/futures/src/time.rs new file mode 100644 index 00000000..e87b4a83 --- /dev/null +++ b/futures/src/time.rs @@ -0,0 +1,70 @@ +//! Listen and react to time. +use crate::subscription::{self, Subscription}; + +/// Returns a [`Subscription`] that produces messages at a set interval. +/// +/// The first message is produced after a `duration`, and then continues to +/// produce more messages every `duration` after that. +/// +/// [`Subscription`]: ../subscription/struct.Subscription.html +pub fn every<H: std::hash::Hasher, E>( + duration: std::time::Duration, +) -> Subscription<H, E, std::time::Instant> { + Subscription::from_recipe(Every(duration)) +} + +struct Every(std::time::Duration); + +#[cfg(feature = "async-std")] +impl<H, E> subscription::Recipe<H, E> for Every +where + H: std::hash::Hasher, +{ + type Output = std::time::Instant; + + fn hash(&self, state: &mut H) { + use std::hash::Hash; + + std::any::TypeId::of::<Self>().hash(state); + self.0.hash(state); + } + + fn stream( + self: Box<Self>, + _input: futures::stream::BoxStream<'static, E>, + ) -> futures::stream::BoxStream<'static, Self::Output> { + use futures::stream::StreamExt; + + async_std::stream::interval(self.0) + .map(|_| std::time::Instant::now()) + .boxed() + } +} + +#[cfg(all(feature = "tokio", not(feature = "async-std")))] +impl<H, E> subscription::Recipe<H, E> for Every +where + H: std::hash::Hasher, +{ + type Output = std::time::Instant; + + fn hash(&self, state: &mut H) { + use std::hash::Hash; + + std::any::TypeId::of::<Self>().hash(state); + self.0.hash(state); + } + + fn stream( + self: Box<Self>, + _input: futures::stream::BoxStream<'static, E>, + ) -> futures::stream::BoxStream<'static, Self::Output> { + use futures::stream::StreamExt; + + let start = tokio::time::Instant::now() + self.0; + + tokio::time::interval_at(start, self.0) + .map(|_| std::time::Instant::now()) + .boxed() + } +} diff --git a/glow/Cargo.toml b/glow/Cargo.toml new file mode 100644 index 00000000..baf2eb2f --- /dev/null +++ b/glow/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "iced_glow" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2018" +description = "A glow renderer for iced" +license = "MIT AND OFL-1.1" +repository = "https://github.com/hecrj/iced" + +[features] +canvas = ["iced_graphics/canvas"] +default_system_font = ["iced_graphics/font-source"] +# Not supported yet! +image = [] +svg = [] + +[dependencies] +glow = "0.4" +glow_glyph = "0.2" +glyph_brush = "0.7" +euclid = "0.20" +bytemuck = "1.2" +glam = "0.8" +log = "0.4" + +[dependencies.iced_native] +version = "0.2" +path = "../native" + +[dependencies.iced_graphics] +version = "0.1" +path = "../graphics" +features = ["font-fallback", "font-icons", "opengl"] + +[package.metadata.docs.rs] +rustdoc-args = ["--cfg", "docsrs"] +all-features = true diff --git a/glow/src/backend.rs b/glow/src/backend.rs new file mode 100644 index 00000000..8b5b4f9c --- /dev/null +++ b/glow/src/backend.rs @@ -0,0 +1,227 @@ +use crate::quad; +use crate::text; +use crate::triangle; +use crate::{Settings, Transformation, Viewport}; +use iced_graphics::backend; +use iced_graphics::font; +use iced_graphics::Layer; +use iced_graphics::Primitive; +use iced_native::mouse; +use iced_native::{Font, HorizontalAlignment, Size, VerticalAlignment}; + +/// A [`glow`] graphics backend for [`iced`]. +/// +/// [`glow`]: https://github.com/grovesNL/glow +/// [`iced`]: https://github.com/hecrj/iced +#[derive(Debug)] +pub struct Backend { + quad_pipeline: quad::Pipeline, + text_pipeline: text::Pipeline, + triangle_pipeline: triangle::Pipeline, + default_text_size: u16, +} + +impl Backend { + /// Creates a new [`Backend`]. + /// + /// [`Backend`]: struct.Backend.html + pub fn new(gl: &glow::Context, settings: Settings) -> Self { + let text_pipeline = text::Pipeline::new(gl, settings.default_font); + let quad_pipeline = quad::Pipeline::new(gl); + let triangle_pipeline = triangle::Pipeline::new(gl); + + Self { + quad_pipeline, + text_pipeline, + triangle_pipeline, + default_text_size: settings.default_text_size, + } + } + + /// Draws the provided primitives in the default framebuffer. + /// + /// The text provided as overlay will be rendered on top of the primitives. + /// This is useful for rendering debug information. + pub fn draw<T: AsRef<str>>( + &mut self, + gl: &glow::Context, + viewport: &Viewport, + (primitive, mouse_interaction): &(Primitive, mouse::Interaction), + overlay_text: &[T], + ) -> mouse::Interaction { + let viewport_size = viewport.physical_size(); + let scale_factor = viewport.scale_factor() as f32; + let projection = viewport.projection(); + + let mut layers = Layer::generate(primitive, viewport); + layers.push(Layer::overlay(overlay_text, viewport)); + + for layer in layers { + self.flush( + gl, + scale_factor, + projection, + &layer, + viewport_size.height, + ); + } + + *mouse_interaction + } + + fn flush( + &mut self, + gl: &glow::Context, + scale_factor: f32, + transformation: Transformation, + layer: &Layer<'_>, + target_height: u32, + ) { + let mut bounds = (layer.bounds * scale_factor).snap(); + bounds.height = bounds.height.min(target_height); + + if !layer.quads.is_empty() { + self.quad_pipeline.draw( + gl, + target_height, + &layer.quads, + transformation, + scale_factor, + bounds, + ); + } + + if !layer.meshes.is_empty() { + let scaled = transformation + * Transformation::scale(scale_factor, scale_factor); + + self.triangle_pipeline.draw( + gl, + target_height, + scaled, + scale_factor, + &layer.meshes, + ); + } + + if !layer.text.is_empty() { + for text in layer.text.iter() { + // Target physical coordinates directly to avoid blurry text + let text = glow_glyph::Section { + // TODO: We `round` here to avoid rerasterizing text when + // its position changes slightly. This can make text feel a + // bit "jumpy". We may be able to do better once we improve + // our text rendering/caching pipeline. + screen_position: ( + (text.bounds.x * scale_factor).round(), + (text.bounds.y * scale_factor).round(), + ), + // TODO: Fix precision issues with some scale factors. + // + // The `ceil` here can cause some words to render on the + // same line when they should not. + // + // Ideally, `wgpu_glyph` should be able to compute layout + // using logical positions, and then apply the proper + // scaling when rendering. This would ensure that both + // measuring and rendering follow the same layout rules. + bounds: ( + (text.bounds.width * scale_factor).ceil(), + (text.bounds.height * scale_factor).ceil(), + ), + text: vec![glow_glyph::Text { + text: text.content, + scale: glow_glyph::ab_glyph::PxScale { + x: text.size * scale_factor, + y: text.size * scale_factor, + }, + font_id: self.text_pipeline.find_font(text.font), + extra: glow_glyph::Extra { + color: text.color, + z: 0.0, + }, + }], + layout: glow_glyph::Layout::default() + .h_align(match text.horizontal_alignment { + HorizontalAlignment::Left => { + glow_glyph::HorizontalAlign::Left + } + HorizontalAlignment::Center => { + glow_glyph::HorizontalAlign::Center + } + HorizontalAlignment::Right => { + glow_glyph::HorizontalAlign::Right + } + }) + .v_align(match text.vertical_alignment { + VerticalAlignment::Top => { + glow_glyph::VerticalAlign::Top + } + VerticalAlignment::Center => { + glow_glyph::VerticalAlign::Center + } + VerticalAlignment::Bottom => { + glow_glyph::VerticalAlign::Bottom + } + }), + ..Default::default() + }; + + self.text_pipeline.queue(text); + } + + self.text_pipeline.draw_queued( + gl, + transformation, + glow_glyph::Region { + x: bounds.x, + y: target_height - (bounds.y + bounds.height), + width: bounds.width, + height: bounds.height, + }, + ); + } + } +} + +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::ICONS; + const CHECKMARK_ICON: char = font::CHECKMARK_ICON; + + fn default_size(&self) -> u16 { + self.default_text_size + } + + fn measure( + &self, + contents: &str, + size: f32, + font: Font, + bounds: Size, + ) -> (f32, f32) { + self.text_pipeline.measure(contents, size, font, bounds) + } +} + +#[cfg(feature = "image")] +impl backend::Image for Backend { + fn dimensions(&self, _handle: &iced_native::image::Handle) -> (u32, u32) { + (50, 50) + } +} + +#[cfg(feature = "svg")] +impl backend::Svg for Backend { + fn viewport_dimensions( + &self, + _handle: &iced_native::svg::Handle, + ) -> (u32, u32) { + (50, 50) + } +} diff --git a/glow/src/lib.rs b/glow/src/lib.rs new file mode 100644 index 00000000..a6c8a75a --- /dev/null +++ b/glow/src/lib.rs @@ -0,0 +1,39 @@ +//! A [`glow`] renderer for [`iced_native`]. +//! +//! [`glow`]: https://github.com/grovesNL/glow +//! [`iced_native`]: https://github.com/hecrj/iced/tree/master/native +#![deny(missing_docs)] +#![deny(missing_debug_implementations)] +#![deny(unused_results)] +#![forbid(rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg))] + +mod backend; +mod program; +mod quad; +mod text; +mod triangle; + +pub mod settings; +pub mod widget; +pub mod window; + +pub use backend::Backend; +pub use settings::Settings; + +pub(crate) use iced_graphics::Transformation; + +#[doc(no_inline)] +pub use widget::*; + +pub use iced_graphics::Viewport; +pub use iced_native::{ + Background, Color, Command, HorizontalAlignment, Length, Vector, + VerticalAlignment, +}; + +/// A [`glow`] graphics renderer for [`iced`]. +/// +/// [`glow`]: https://github.com/grovesNL/glow +/// [`iced`]: https://github.com/hecrj/iced +pub type Renderer = iced_graphics::Renderer<Backend>; diff --git a/glow/src/program.rs b/glow/src/program.rs new file mode 100644 index 00000000..489a194f --- /dev/null +++ b/glow/src/program.rs @@ -0,0 +1,39 @@ +use glow::HasContext; + +pub unsafe fn create( + gl: &glow::Context, + shader_sources: &[(u32, &str)], +) -> <glow::Context as HasContext>::Program { + let program = gl.create_program().expect("Cannot create program"); + + let mut shaders = Vec::with_capacity(shader_sources.len()); + + for (shader_type, shader_source) in shader_sources.iter() { + let shader = gl + .create_shader(*shader_type) + .expect("Cannot create shader"); + + gl.shader_source(shader, shader_source); + gl.compile_shader(shader); + + if !gl.get_shader_compile_status(shader) { + panic!(gl.get_shader_info_log(shader)); + } + + gl.attach_shader(program, shader); + + shaders.push(shader); + } + + gl.link_program(program); + if !gl.get_program_link_status(program) { + panic!(gl.get_program_info_log(program)); + } + + for shader in shaders { + gl.detach_shader(program, shader); + gl.delete_shader(shader); + } + + program +} diff --git a/glow/src/quad.rs b/glow/src/quad.rs new file mode 100644 index 00000000..3a65338a --- /dev/null +++ b/glow/src/quad.rs @@ -0,0 +1,235 @@ +use crate::program; +use crate::Transformation; +use glow::HasContext; +use iced_graphics::layer; +use iced_native::Rectangle; + +const MAX_INSTANCES: usize = 100_000; + +#[derive(Debug)] +pub struct Pipeline { + program: <glow::Context as HasContext>::Program, + vertex_array: <glow::Context as HasContext>::VertexArray, + instances: <glow::Context as HasContext>::Buffer, + transform_location: <glow::Context as HasContext>::UniformLocation, + scale_location: <glow::Context as HasContext>::UniformLocation, + screen_height_location: <glow::Context as HasContext>::UniformLocation, + current_transform: Transformation, + current_scale: f32, + current_target_height: u32, +} + +impl Pipeline { + pub fn new(gl: &glow::Context) -> Pipeline { + let program = unsafe { + program::create( + gl, + &[ + (glow::VERTEX_SHADER, include_str!("shader/quad.vert")), + (glow::FRAGMENT_SHADER, include_str!("shader/quad.frag")), + ], + ) + }; + + let transform_location = + unsafe { gl.get_uniform_location(program, "u_Transform") } + .expect("Get transform location"); + + let scale_location = + unsafe { gl.get_uniform_location(program, "u_Scale") } + .expect("Get scale location"); + + let screen_height_location = + unsafe { gl.get_uniform_location(program, "u_ScreenHeight") } + .expect("Get target height location"); + + unsafe { + gl.use_program(Some(program)); + + let matrix: [f32; 16] = Transformation::identity().into(); + gl.uniform_matrix_4_f32_slice( + Some(transform_location), + false, + &matrix, + ); + + gl.uniform_1_f32(Some(scale_location), 1.0); + gl.uniform_1_f32(Some(screen_height_location), 0.0); + + gl.use_program(None); + } + + let (vertex_array, instances) = + unsafe { create_instance_buffer(gl, MAX_INSTANCES) }; + + Pipeline { + program, + vertex_array, + instances, + transform_location, + scale_location, + screen_height_location, + current_transform: Transformation::identity(), + current_scale: 1.0, + current_target_height: 0, + } + } + + pub fn draw( + &mut self, + gl: &glow::Context, + target_height: u32, + instances: &[layer::Quad], + transformation: Transformation, + scale: f32, + bounds: Rectangle<u32>, + ) { + unsafe { + gl.enable(glow::SCISSOR_TEST); + gl.scissor( + bounds.x as i32, + (target_height - (bounds.y + bounds.height)) as i32, + bounds.width as i32, + bounds.height as i32, + ); + + gl.use_program(Some(self.program)); + gl.bind_vertex_array(Some(self.vertex_array)); + gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.instances)); + } + + if transformation != self.current_transform { + unsafe { + let matrix: [f32; 16] = transformation.into(); + gl.uniform_matrix_4_f32_slice( + Some(self.transform_location), + false, + &matrix, + ); + + self.current_transform = transformation; + } + } + + if scale != self.current_scale { + unsafe { + gl.uniform_1_f32(Some(self.scale_location), scale); + } + + self.current_scale = scale; + } + + if target_height != self.current_target_height { + unsafe { + gl.uniform_1_f32( + Some(self.screen_height_location), + target_height as f32, + ); + } + + self.current_target_height = target_height; + } + + let mut i = 0; + let total = instances.len(); + + while i < total { + let end = (i + MAX_INSTANCES).min(total); + let amount = end - i; + + unsafe { + gl.buffer_sub_data_u8_slice( + glow::ARRAY_BUFFER, + 0, + bytemuck::cast_slice(&instances[i..end]), + ); + + gl.draw_arrays_instanced( + glow::TRIANGLE_STRIP, + 0, + 4, + amount as i32, + ); + } + + i += MAX_INSTANCES; + } + + unsafe { + gl.bind_vertex_array(None); + gl.use_program(None); + gl.disable(glow::SCISSOR_TEST); + } + } +} + +unsafe fn create_instance_buffer( + gl: &glow::Context, + size: usize, +) -> ( + <glow::Context as HasContext>::VertexArray, + <glow::Context as HasContext>::Buffer, +) { + let vertex_array = gl.create_vertex_array().expect("Create vertex array"); + let buffer = gl.create_buffer().expect("Create instance buffer"); + + gl.bind_vertex_array(Some(vertex_array)); + gl.bind_buffer(glow::ARRAY_BUFFER, Some(buffer)); + gl.buffer_data_size( + glow::ARRAY_BUFFER, + (size * std::mem::size_of::<layer::Quad>()) as i32, + glow::DYNAMIC_DRAW, + ); + + let stride = std::mem::size_of::<layer::Quad>() as i32; + + gl.enable_vertex_attrib_array(0); + gl.vertex_attrib_pointer_f32(0, 2, glow::FLOAT, false, stride, 0); + gl.vertex_attrib_divisor(0, 1); + + gl.enable_vertex_attrib_array(1); + gl.vertex_attrib_pointer_f32(1, 2, glow::FLOAT, false, stride, 4 * 2); + gl.vertex_attrib_divisor(1, 1); + + gl.enable_vertex_attrib_array(2); + gl.vertex_attrib_pointer_f32(2, 4, glow::FLOAT, false, stride, 4 * (2 + 2)); + gl.vertex_attrib_divisor(2, 1); + + gl.enable_vertex_attrib_array(3); + gl.vertex_attrib_pointer_f32( + 3, + 4, + glow::FLOAT, + false, + stride, + 4 * (2 + 2 + 4), + ); + gl.vertex_attrib_divisor(3, 1); + + gl.enable_vertex_attrib_array(4); + gl.vertex_attrib_pointer_f32( + 4, + 1, + glow::FLOAT, + false, + stride, + 4 * (2 + 2 + 4 + 4), + ); + gl.vertex_attrib_divisor(4, 1); + + gl.enable_vertex_attrib_array(5); + gl.vertex_attrib_pointer_f32( + 5, + 1, + glow::FLOAT, + false, + stride, + 4 * (2 + 2 + 4 + 4 + 1), + ); + gl.vertex_attrib_divisor(5, 1); + + gl.bind_vertex_array(None); + gl.bind_buffer(glow::ARRAY_BUFFER, None); + + (vertex_array, buffer) +} diff --git a/glow/src/settings.rs b/glow/src/settings.rs new file mode 100644 index 00000000..c2c605ef --- /dev/null +++ b/glow/src/settings.rs @@ -0,0 +1,31 @@ +//! Configure a renderer. +pub use iced_graphics::Antialiasing; + +/// The settings of a [`Renderer`]. +/// +/// [`Renderer`]: ../struct.Renderer.html +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Settings { + /// The bytes of the font that will be used by default. + /// + /// If `None` is provided, a default system font will be chosen. + pub default_font: Option<&'static [u8]>, + + /// The default size of text. + /// + /// By default, it will be set to 20. + pub default_text_size: u16, + + /// The antialiasing strategy that will be used for triangle primitives. + pub antialiasing: Option<Antialiasing>, +} + +impl Default for Settings { + fn default() -> Settings { + Settings { + default_font: None, + default_text_size: 20, + antialiasing: None, + } + } +} diff --git a/glow/src/shader/quad.frag b/glow/src/shader/quad.frag new file mode 100644 index 00000000..cea36bdc --- /dev/null +++ b/glow/src/shader/quad.frag @@ -0,0 +1,70 @@ +#version 330 + +uniform float u_ScreenHeight; + +in vec4 v_Color; +in vec4 v_BorderColor; +in vec2 v_Pos; +in vec2 v_Scale; +in float v_BorderRadius; +in float v_BorderWidth; + +out vec4 o_Color; + +float distance(in vec2 frag_coord, in vec2 position, in vec2 size, float radius) +{ + // TODO: Try SDF approach: https://www.shadertoy.com/view/wd3XRN + vec2 inner_size = size - vec2(radius, radius) * 2.0; + vec2 top_left = position + vec2(radius, radius); + vec2 bottom_right = top_left + inner_size; + + vec2 top_left_distance = top_left - frag_coord; + vec2 bottom_right_distance = frag_coord - bottom_right; + + vec2 distance = vec2( + 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(distance.x * distance.x + distance.y * distance.y); +} + +void main() { + vec4 mixed_color; + + vec2 fragCoord = vec2(gl_FragCoord.x, u_ScreenHeight - gl_FragCoord.y); + + // TODO: Remove branching (?) + if(v_BorderWidth > 0) { + float internal_border = max(v_BorderRadius - v_BorderWidth, 0.0); + + float internal_distance = distance( + fragCoord, + v_Pos + vec2(v_BorderWidth), + v_Scale - vec2(v_BorderWidth * 2.0), + internal_border + ); + + float border_mix = smoothstep( + max(internal_border - 0.5, 0.0), + internal_border + 0.5, + internal_distance + ); + + mixed_color = mix(v_Color, v_BorderColor, border_mix); + } else { + mixed_color = v_Color; + } + + float d = distance( + fragCoord, + v_Pos, + v_Scale, + v_BorderRadius + ); + + float radius_alpha = + 1.0 - smoothstep(max(v_BorderRadius - 0.5, 0.0), v_BorderRadius + 0.5, d); + + o_Color = vec4(mixed_color.xyz, mixed_color.w * radius_alpha); +} diff --git a/glow/src/shader/quad.vert b/glow/src/shader/quad.vert new file mode 100644 index 00000000..d37b5c8d --- /dev/null +++ b/glow/src/shader/quad.vert @@ -0,0 +1,47 @@ +#version 330 + +uniform mat4 u_Transform; +uniform float u_Scale; + +layout(location = 0) in vec2 i_Pos; +layout(location = 1) in vec2 i_Scale; +layout(location = 2) in vec4 i_Color; +layout(location = 3) in vec4 i_BorderColor; +layout(location = 4) in float i_BorderRadius; +layout(location = 5) in float i_BorderWidth; + +out vec4 v_Color; +out vec4 v_BorderColor; +out vec2 v_Pos; +out vec2 v_Scale; +out float v_BorderRadius; +out float v_BorderWidth; + +const vec2 positions[4] = vec2[]( + vec2(0.0, 0.0), + vec2(0.0, 1.0), + vec2(1.0, 0.0), + vec2(1.0, 1.0) +); + +void main() { + vec2 q_Pos = positions[gl_VertexID]; + vec2 p_Pos = i_Pos * u_Scale; + vec2 p_Scale = i_Scale * u_Scale; + + mat4 i_Transform = mat4( + vec4(p_Scale.x + 1.0, 0.0, 0.0, 0.0), + vec4(0.0, p_Scale.y + 1.0, 0.0, 0.0), + vec4(0.0, 0.0, 1.0, 0.0), + vec4(p_Pos - vec2(0.5, 0.5), 0.0, 1.0) + ); + + v_Color = i_Color; + v_BorderColor = i_BorderColor; + v_Pos = p_Pos; + v_Scale = p_Scale; + v_BorderRadius = i_BorderRadius * u_Scale; + v_BorderWidth = i_BorderWidth * u_Scale; + + gl_Position = u_Transform * i_Transform * vec4(q_Pos, 0.0, 1.0); +} diff --git a/glow/src/shader/triangle.frag b/glow/src/shader/triangle.frag new file mode 100644 index 00000000..d186784a --- /dev/null +++ b/glow/src/shader/triangle.frag @@ -0,0 +1,9 @@ +#version 330 + +in vec4 v_Color; + +out vec4 o_Color; + +void main() { + o_Color = v_Color; +} diff --git a/glow/src/shader/triangle.vert b/glow/src/shader/triangle.vert new file mode 100644 index 00000000..5723436a --- /dev/null +++ b/glow/src/shader/triangle.vert @@ -0,0 +1,13 @@ +#version 330 + +uniform mat4 u_Transform; + +layout(location = 0) in vec2 i_Position; +layout(location = 1) in vec4 i_Color; + +out vec4 v_Color; + +void main() { + gl_Position = u_Transform * vec4(i_Position, 0.0, 1.0); + v_Color = i_Color; +} diff --git a/glow/src/text.rs b/glow/src/text.rs new file mode 100644 index 00000000..925c7287 --- /dev/null +++ b/glow/src/text.rs @@ -0,0 +1,156 @@ +use crate::Transformation; +use glow_glyph::ab_glyph; +use iced_graphics::font; +use std::{cell::RefCell, collections::HashMap}; + +#[derive(Debug)] +pub struct Pipeline { + draw_brush: RefCell<glow_glyph::GlyphBrush>, + draw_font_map: RefCell<HashMap<String, glow_glyph::FontId>>, + measure_brush: RefCell<glyph_brush::GlyphBrush<()>>, +} + +impl Pipeline { + pub fn new(gl: &glow::Context, default_font: Option<&[u8]>) -> Self { + let default_font = default_font.map(|slice| slice.to_vec()); + + // TODO: Font customization + #[cfg(feature = "default_system_font")] + let default_font = { + default_font.or_else(|| { + font::Source::new() + .load(&[font::Family::SansSerif, font::Family::Serif]) + .ok() + }) + }; + + let default_font = + default_font.unwrap_or_else(|| font::FALLBACK.to_vec()); + + let font = ab_glyph::FontArc::try_from_vec(default_font) + .unwrap_or_else(|_| { + log::warn!( + "System font failed to load. Falling back to \ + embedded font..." + ); + + ab_glyph::FontArc::try_from_slice(font::FALLBACK) + .expect("Load fallback font") + }); + + let draw_brush = + glow_glyph::GlyphBrushBuilder::using_font(font.clone()) + .initial_cache_size((2048, 2048)) + .draw_cache_multithread(false) // TODO: Expose as a configuration flag + .build(&gl); + + let measure_brush = + glyph_brush::GlyphBrushBuilder::using_font(font).build(); + + Pipeline { + draw_brush: RefCell::new(draw_brush), + draw_font_map: RefCell::new(HashMap::new()), + measure_brush: RefCell::new(measure_brush), + } + } + + pub fn queue(&mut self, section: glow_glyph::Section<'_>) { + self.draw_brush.borrow_mut().queue(section); + } + + pub fn draw_queued( + &mut self, + gl: &glow::Context, + transformation: Transformation, + region: glow_glyph::Region, + ) { + self.draw_brush + .borrow_mut() + .draw_queued_with_transform_and_scissoring( + gl, + transformation.into(), + region, + ) + .expect("Draw text"); + } + + pub fn measure( + &self, + content: &str, + size: f32, + font: iced_native::Font, + bounds: iced_native::Size, + ) -> (f32, f32) { + use glow_glyph::GlyphCruncher; + + let glow_glyph::FontId(font_id) = self.find_font(font); + + let section = glow_glyph::Section { + bounds: (bounds.width, bounds.height), + text: vec![glow_glyph::Text { + text: content, + scale: size.into(), + font_id: glow_glyph::FontId(font_id), + extra: glow_glyph::Extra::default(), + }], + ..Default::default() + }; + + if let Some(bounds) = + self.measure_brush.borrow_mut().glyph_bounds(section) + { + (bounds.width().ceil(), bounds.height().ceil()) + } else { + (0.0, 0.0) + } + } + + pub fn trim_measurement_cache(&mut self) { + // TODO: We should probably use a `GlyphCalculator` for this. However, + // it uses a lifetimed `GlyphCalculatorGuard` with side-effects on drop. + // This makes stuff quite inconvenient. A manual method for trimming the + // cache would make our lives easier. + loop { + let action = self + .measure_brush + .borrow_mut() + .process_queued(|_, _| {}, |_| {}); + + match action { + Ok(_) => break, + Err(glyph_brush::BrushError::TextureTooSmall { suggested }) => { + let (width, height) = suggested; + + self.measure_brush + .borrow_mut() + .resize_texture(width, height); + } + } + } + } + + pub fn find_font(&self, font: iced_native::Font) -> glow_glyph::FontId { + match font { + iced_native::Font::Default => glow_glyph::FontId(0), + iced_native::Font::External { name, bytes } => { + if let Some(font_id) = self.draw_font_map.borrow().get(name) { + return *font_id; + } + + let font = ab_glyph::FontArc::try_from_slice(bytes) + .expect("Load font"); + + let _ = self.measure_brush.borrow_mut().add_font(font.clone()); + + let font_id = self.draw_brush.borrow_mut().add_font(font); + + let _ = self + .draw_font_map + .borrow_mut() + .insert(String::from(name), font_id); + + font_id + } + } + } +} diff --git a/glow/src/triangle.rs b/glow/src/triangle.rs new file mode 100644 index 00000000..eefd1c1f --- /dev/null +++ b/glow/src/triangle.rs @@ -0,0 +1,292 @@ +//! Draw meshes of triangles. +use crate::program; +use crate::Transformation; +use glow::HasContext; +use iced_graphics::layer; +use std::marker::PhantomData; + +pub use iced_graphics::triangle::{Mesh2D, Vertex2D}; + +const VERTEX_BUFFER_SIZE: usize = 10_000; +const INDEX_BUFFER_SIZE: usize = 10_000; + +#[derive(Debug)] +pub(crate) struct Pipeline { + program: <glow::Context as HasContext>::Program, + vertex_array: <glow::Context as HasContext>::VertexArray, + vertices: Buffer<Vertex2D>, + indices: Buffer<u32>, + transform_location: <glow::Context as HasContext>::UniformLocation, + current_transform: Transformation, +} + +impl Pipeline { + pub fn new(gl: &glow::Context) -> Pipeline { + let program = unsafe { + program::create( + gl, + &[ + (glow::VERTEX_SHADER, include_str!("shader/triangle.vert")), + ( + glow::FRAGMENT_SHADER, + include_str!("shader/triangle.frag"), + ), + ], + ) + }; + + let transform_location = + unsafe { gl.get_uniform_location(program, "u_Transform") } + .expect("Get transform location"); + + unsafe { + gl.use_program(Some(program)); + + let transform: [f32; 16] = Transformation::identity().into(); + gl.uniform_matrix_4_f32_slice( + Some(transform_location), + false, + &transform, + ); + + gl.use_program(None); + } + + let vertex_array = + unsafe { gl.create_vertex_array().expect("Create vertex array") }; + + unsafe { + gl.bind_vertex_array(Some(vertex_array)); + } + + let vertices = unsafe { + Buffer::new( + gl, + glow::ARRAY_BUFFER, + glow::DYNAMIC_DRAW, + VERTEX_BUFFER_SIZE, + ) + }; + + let indices = unsafe { + Buffer::new( + gl, + glow::ELEMENT_ARRAY_BUFFER, + glow::DYNAMIC_DRAW, + INDEX_BUFFER_SIZE, + ) + }; + + unsafe { + let stride = std::mem::size_of::<Vertex2D>() as i32; + + gl.enable_vertex_attrib_array(0); + gl.vertex_attrib_pointer_f32(0, 2, glow::FLOAT, false, stride, 0); + + gl.enable_vertex_attrib_array(1); + gl.vertex_attrib_pointer_f32( + 1, + 4, + glow::FLOAT, + false, + stride, + 4 * 2, + ); + + gl.bind_vertex_array(None); + } + + Pipeline { + program, + vertex_array, + vertices, + indices, + transform_location, + current_transform: Transformation::identity(), + } + } + + pub fn draw( + &mut self, + gl: &glow::Context, + target_height: u32, + transformation: Transformation, + scale_factor: f32, + meshes: &[layer::Mesh<'_>], + ) { + unsafe { + gl.enable(glow::MULTISAMPLE); + gl.enable(glow::SCISSOR_TEST); + gl.use_program(Some(self.program)); + gl.bind_vertex_array(Some(self.vertex_array)); + } + + // This looks a bit crazy, but we are just counting how many vertices + // and indices we will need to handle. + // TODO: Improve readability + let (total_vertices, total_indices) = meshes + .iter() + .map(|layer::Mesh { buffers, .. }| { + (buffers.vertices.len(), buffers.indices.len()) + }) + .fold((0, 0), |(total_v, total_i), (v, i)| { + (total_v + v, total_i + i) + }); + + // Then we ensure the current buffers are big enough, resizing if + // necessary + unsafe { + self.vertices.bind(gl, total_vertices); + self.indices.bind(gl, total_indices); + } + + // We upload all the vertices and indices upfront + let mut last_vertex = 0; + let mut last_index = 0; + + for layer::Mesh { buffers, .. } in meshes { + unsafe { + gl.buffer_sub_data_u8_slice( + glow::ARRAY_BUFFER, + (last_vertex * std::mem::size_of::<Vertex2D>()) as i32, + bytemuck::cast_slice(&buffers.vertices), + ); + + gl.buffer_sub_data_u8_slice( + glow::ELEMENT_ARRAY_BUFFER, + (last_index * std::mem::size_of::<u32>()) as i32, + bytemuck::cast_slice(&buffers.indices), + ); + + last_vertex += buffers.vertices.len(); + last_index += buffers.indices.len(); + } + } + + // Then we draw each mesh using offsets + let mut last_vertex = 0; + let mut last_index = 0; + + for layer::Mesh { + buffers, + origin, + clip_bounds, + } in meshes + { + let transform = + transformation * Transformation::translate(origin.x, origin.y); + + let clip_bounds = (*clip_bounds * scale_factor).snap(); + + unsafe { + if self.current_transform != transform { + let matrix: [f32; 16] = transform.into(); + gl.uniform_matrix_4_f32_slice( + Some(self.transform_location), + false, + &matrix, + ); + + self.current_transform = transform; + } + + gl.scissor( + clip_bounds.x as i32, + (target_height - (clip_bounds.y + clip_bounds.height)) + as i32, + clip_bounds.width as i32, + clip_bounds.height as i32, + ); + + gl.draw_elements_base_vertex( + glow::TRIANGLES, + buffers.indices.len() as i32, + glow::UNSIGNED_INT, + (last_index * std::mem::size_of::<u32>()) as i32, + last_vertex as i32, + ); + + last_vertex += buffers.vertices.len(); + last_index += buffers.indices.len(); + } + } + + unsafe { + gl.bind_vertex_array(None); + gl.use_program(None); + gl.disable(glow::SCISSOR_TEST); + gl.disable(glow::MULTISAMPLE); + } + } +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +struct Uniforms { + transform: [f32; 16], +} + +unsafe impl bytemuck::Zeroable for Uniforms {} +unsafe impl bytemuck::Pod for Uniforms {} + +impl Default for Uniforms { + fn default() -> Self { + Self { + transform: *Transformation::identity().as_ref(), + } + } +} + +impl From<Transformation> for Uniforms { + fn from(transformation: Transformation) -> Uniforms { + Self { + transform: transformation.into(), + } + } +} + +#[derive(Debug)] +struct Buffer<T> { + raw: <glow::Context as HasContext>::Buffer, + target: u32, + usage: u32, + size: usize, + phantom: PhantomData<T>, +} + +impl<T> Buffer<T> { + pub unsafe fn new( + gl: &glow::Context, + target: u32, + usage: u32, + size: usize, + ) -> Self { + let raw = gl.create_buffer().expect("Create buffer"); + + let mut buffer = Buffer { + raw, + target, + usage, + size: 0, + phantom: PhantomData, + }; + + buffer.bind(gl, size); + + buffer + } + + pub unsafe fn bind(&mut self, gl: &glow::Context, size: usize) { + gl.bind_buffer(self.target, Some(self.raw)); + + if self.size < size { + gl.buffer_data_size( + self.target, + (size * std::mem::size_of::<T>()) as i32, + self.usage, + ); + + self.size = size; + } + } +} diff --git a/glow/src/widget.rs b/glow/src/widget.rs new file mode 100644 index 00000000..9968092b --- /dev/null +++ b/glow/src/widget.rs @@ -0,0 +1,58 @@ +//! Use the widgets supported out-of-the-box. +//! +//! # Re-exports +//! For convenience, the contents of this module are available at the root +//! module. Therefore, you can directly type: +//! +//! ``` +//! use iced_glow::{button, Button}; +//! ``` +use crate::Renderer; + +pub mod button; +pub mod checkbox; +pub mod container; +pub mod pane_grid; +pub mod progress_bar; +pub mod radio; +pub mod scrollable; +pub mod slider; +pub mod text_input; + +#[doc(no_inline)] +pub use button::Button; +#[doc(no_inline)] +pub use checkbox::Checkbox; +#[doc(no_inline)] +pub use container::Container; +#[doc(no_inline)] +pub use pane_grid::PaneGrid; +#[doc(no_inline)] +pub use progress_bar::ProgressBar; +#[doc(no_inline)] +pub use radio::Radio; +#[doc(no_inline)] +pub use scrollable::Scrollable; +#[doc(no_inline)] +pub use slider::Slider; +#[doc(no_inline)] +pub use text_input::TextInput; + +#[cfg(feature = "canvas")] +#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] +pub mod canvas; + +#[cfg(feature = "canvas")] +#[doc(no_inline)] +pub use canvas::Canvas; + +pub use iced_native::{Image, Space}; + +/// A container that distributes its contents vertically. +pub type Column<'a, Message> = iced_native::Column<'a, Message, Renderer>; + +/// A container that distributes its contents horizontally. +pub type Row<'a, Message> = iced_native::Row<'a, Message, Renderer>; + +/// A paragraph of text. +pub type Text = iced_native::Text<Renderer>; diff --git a/glow/src/widget/button.rs b/glow/src/widget/button.rs new file mode 100644 index 00000000..fee7a7f8 --- /dev/null +++ b/glow/src/widget/button.rs @@ -0,0 +1,15 @@ +//! Allow your users to perform actions by pressing a button. +//! +//! A [`Button`] has some local [`State`]. +//! +//! [`Button`]: type.Button.html +//! [`State`]: struct.State.html +use crate::Renderer; + +pub use iced_graphics::button::{Style, StyleSheet}; +pub use iced_native::button::State; + +/// A widget that produces a message when clicked. +/// +/// This is an alias of an `iced_native` button with an `iced_wgpu::Renderer`. +pub type Button<'a, Message> = iced_native::Button<'a, Message, Renderer>; diff --git a/glow/src/widget/canvas.rs b/glow/src/widget/canvas.rs new file mode 100644 index 00000000..bef34857 --- /dev/null +++ b/glow/src/widget/canvas.rs @@ -0,0 +1,9 @@ +//! 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! +//! +//! [`Canvas`]: struct.Canvas.html +//! [`Frame`]: struct.Frame.html +pub use iced_graphics::canvas::*; diff --git a/glow/src/widget/checkbox.rs b/glow/src/widget/checkbox.rs new file mode 100644 index 00000000..d27d77cc --- /dev/null +++ b/glow/src/widget/checkbox.rs @@ -0,0 +1,9 @@ +//! Show toggle controls using checkboxes. +use crate::Renderer; + +pub use iced_graphics::checkbox::{Style, StyleSheet}; + +/// A box that can be checked. +/// +/// This is an alias of an `iced_native` checkbox with an `iced_wgpu::Renderer`. +pub type Checkbox<Message> = iced_native::Checkbox<Message, Renderer>; diff --git a/glow/src/widget/container.rs b/glow/src/widget/container.rs new file mode 100644 index 00000000..bc26cef2 --- /dev/null +++ b/glow/src/widget/container.rs @@ -0,0 +1,10 @@ +//! Decorate content and apply alignment. +use crate::Renderer; + +pub use iced_graphics::container::{Style, StyleSheet}; + +/// An element decorating some content. +/// +/// This is an alias of an `iced_native` container with a default +/// `Renderer`. +pub type Container<'a, Message> = iced_native::Container<'a, Message, Renderer>; diff --git a/glow/src/widget/pane_grid.rs b/glow/src/widget/pane_grid.rs new file mode 100644 index 00000000..6f437df7 --- /dev/null +++ b/glow/src/widget/pane_grid.rs @@ -0,0 +1,24 @@ +//! Let your users split regions of your application and organize layout dynamically. +//! +//! [](https://gfycat.com/mixedflatjellyfish) +//! +//! # Example +//! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing, +//! drag and drop, and hotkey support. +//! +//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.1/examples/pane_grid +//! [`PaneGrid`]: type.PaneGrid.html +use crate::Renderer; + +pub use iced_native::pane_grid::{ + Axis, Content, Direction, DragEvent, Focus, KeyPressEvent, Node, Pane, + ResizeEvent, Split, State, +}; + +/// A collection of panes distributed using either vertical or horizontal splits +/// to completely fill the space available. +/// +/// [](https://gfycat.com/mixedflatjellyfish) +/// +/// This is an alias of an `iced_native` pane grid with an `iced_wgpu::Renderer`. +pub type PaneGrid<'a, Message> = iced_native::PaneGrid<'a, Message, Renderer>; diff --git a/glow/src/widget/progress_bar.rs b/glow/src/widget/progress_bar.rs new file mode 100644 index 00000000..a636a3a6 --- /dev/null +++ b/glow/src/widget/progress_bar.rs @@ -0,0 +1,15 @@ +//! Allow your users to visually track the progress of a computation. +//! +//! A [`ProgressBar`] has a range of possible values and a current value, +//! as well as a length, height and style. +//! +//! [`ProgressBar`]: type.ProgressBar.html +use crate::Renderer; + +pub use iced_graphics::progress_bar::{Style, StyleSheet}; + +/// A bar that displays progress. +/// +/// This is an alias of an `iced_native` progress bar with an +/// `iced_wgpu::Renderer`. +pub type ProgressBar = iced_native::ProgressBar<Renderer>; diff --git a/glow/src/widget/radio.rs b/glow/src/widget/radio.rs new file mode 100644 index 00000000..0b843d1f --- /dev/null +++ b/glow/src/widget/radio.rs @@ -0,0 +1,10 @@ +//! Create choices using radio buttons. +use crate::Renderer; + +pub use iced_graphics::radio::{Style, StyleSheet}; + +/// A circular button representing a choice. +/// +/// This is an alias of an `iced_native` radio button with an +/// `iced_wgpu::Renderer`. +pub type Radio<Message> = iced_native::Radio<Message, Renderer>; diff --git a/glow/src/widget/scrollable.rs b/glow/src/widget/scrollable.rs new file mode 100644 index 00000000..fabb4318 --- /dev/null +++ b/glow/src/widget/scrollable.rs @@ -0,0 +1,13 @@ +//! Navigate an endless amount of content with a scrollbar. +use crate::Renderer; + +pub use iced_graphics::scrollable::{Scrollbar, Scroller, StyleSheet}; +pub use iced_native::scrollable::State; + +/// A widget that can vertically display an infinite amount of content +/// with a scrollbar. +/// +/// This is an alias of an `iced_native` scrollable with a default +/// `Renderer`. +pub type Scrollable<'a, Message> = + iced_native::Scrollable<'a, Message, Renderer>; diff --git a/glow/src/widget/slider.rs b/glow/src/widget/slider.rs new file mode 100644 index 00000000..3a8c2595 --- /dev/null +++ b/glow/src/widget/slider.rs @@ -0,0 +1,16 @@ +//! Display an interactive selector of a single value from a range of values. +//! +//! A [`Slider`] has some local [`State`]. +//! +//! [`Slider`]: struct.Slider.html +//! [`State`]: struct.State.html +use crate::Renderer; + +pub use iced_graphics::slider::{Handle, HandleShape, Style, StyleSheet}; +pub use iced_native::slider::State; + +/// An horizontal bar and a handle that selects a single value from a range of +/// values. +/// +/// This is an alias of an `iced_native` slider with an `iced_wgpu::Renderer`. +pub type Slider<'a, T, Message> = iced_native::Slider<'a, T, Message, Renderer>; diff --git a/glow/src/widget/text_input.rs b/glow/src/widget/text_input.rs new file mode 100644 index 00000000..1da3fbe6 --- /dev/null +++ b/glow/src/widget/text_input.rs @@ -0,0 +1,15 @@ +//! Display fields that can be filled with text. +//! +//! A [`TextInput`] has some local [`State`]. +//! +//! [`TextInput`]: struct.TextInput.html +//! [`State`]: struct.State.html +use crate::Renderer; + +pub use iced_graphics::text_input::{Style, StyleSheet}; +pub use iced_native::text_input::State; + +/// A field that can be filled with text. +/// +/// This is an alias of an `iced_native` text input with an `iced_wgpu::Renderer`. +pub type TextInput<'a, Message> = iced_native::TextInput<'a, Message, Renderer>; diff --git a/glow/src/window.rs b/glow/src/window.rs new file mode 100644 index 00000000..aac5fb9e --- /dev/null +++ b/glow/src/window.rs @@ -0,0 +1,4 @@ +//! Display rendering results on windows. +mod compositor; + +pub use compositor::Compositor; diff --git a/glow/src/window/compositor.rs b/glow/src/window/compositor.rs new file mode 100644 index 00000000..3ad10b59 --- /dev/null +++ b/glow/src/window/compositor.rs @@ -0,0 +1,76 @@ +use crate::{Backend, Color, Renderer, Settings, Viewport}; + +use core::ffi::c_void; +use glow::HasContext; +use iced_graphics::{Antialiasing, Size}; +use iced_native::mouse; + +/// A window graphics backend for iced powered by `glow`. +#[allow(missing_debug_implementations)] +pub struct Compositor { + gl: glow::Context, +} + +impl iced_graphics::window::GLCompositor for Compositor { + type Settings = Settings; + type Renderer = Renderer; + + unsafe fn new( + settings: Self::Settings, + loader_function: impl FnMut(&str) -> *const c_void, + ) -> (Self, Self::Renderer) { + let gl = glow::Context::from_loader_function(loader_function); + + // Enable auto-conversion from/to sRGB + gl.enable(glow::FRAMEBUFFER_SRGB); + + // Enable alpha blending + gl.enable(glow::BLEND); + gl.blend_func(glow::SRC_ALPHA, glow::ONE_MINUS_SRC_ALPHA); + + // Disable multisampling by default + gl.disable(glow::MULTISAMPLE); + + let renderer = Renderer::new(Backend::new(&gl, settings)); + + (Self { gl }, renderer) + } + + fn sample_count(settings: &Settings) -> u32 { + settings + .antialiasing + .map(Antialiasing::sample_count) + .unwrap_or(0) + } + + fn resize_viewport(&mut self, physical_size: Size<u32>) { + unsafe { + self.gl.viewport( + 0, + 0, + physical_size.width as i32, + physical_size.height as i32, + ); + } + } + + fn draw<T: AsRef<str>>( + &mut self, + renderer: &mut Self::Renderer, + viewport: &Viewport, + color: Color, + output: &<Self::Renderer as iced_native::Renderer>::Output, + overlay: &[T], + ) -> mouse::Interaction { + let gl = &self.gl; + + let [r, g, b, a] = color.into_linear(); + + unsafe { + gl.clear_color(r, g, b, a); + gl.clear(glow::COLOR_BUFFER_BIT); + } + + renderer.backend_mut().draw(gl, viewport, output, overlay) + } +} diff --git a/glutin/Cargo.toml b/glutin/Cargo.toml new file mode 100644 index 00000000..4652112c --- /dev/null +++ b/glutin/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "iced_glutin" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2018" +description = "A glutin runtime for Iced" +license = "MIT" +repository = "https://github.com/hecrj/iced" +documentation = "https://docs.rs/iced_glutin" +keywords = ["gui", "ui", "graphics", "interface", "widgets"] +categories = ["gui"] + +[features] +debug = ["iced_winit/debug"] + +[dependencies] +glutin = "0.24" + +[dependencies.iced_native] +version = "0.2" +path = "../native" + +[dependencies.iced_winit] +version = "0.1" +path = "../winit" + +[dependencies.iced_graphics] +version = "0.1" +path = "../graphics" +features = ["opengl"] diff --git a/glutin/README.md b/glutin/README.md new file mode 100644 index 00000000..34dec1b3 --- /dev/null +++ b/glutin/README.md @@ -0,0 +1,27 @@ +# `iced_winit` +[][documentation] +[](https://crates.io/crates/iced_winit) +[](https://github.com/hecrj/iced/blob/master/LICENSE) +[](https://iced.zulipchat.com) + +`iced_winit` offers some convenient abstractions on top of [`iced_native`] to quickstart development when using [`winit`]. + +It exposes a renderer-agnostic `Application` trait that can be implemented and then run with a simple call. The use of this trait is optional. A `conversion` module is provided for users that decide to implement a custom event loop. + + + +[documentation]: https://docs.rs/iced_winit +[`iced_native`]: ../native +[`winit`]: https://github.com/rust-windowing/winit + +## Installation +Add `iced_winit` as a dependency in your `Cargo.toml`: + +```toml +iced_winit = "0.1" +``` + +__Iced moves fast and the `master` branch can contain breaking changes!__ If +you want to learn about a specific release, check out [the release list]. + +[the release list]: https://github.com/hecrj/iced/releases diff --git a/glutin/src/application.rs b/glutin/src/application.rs new file mode 100644 index 00000000..3be9b65f --- /dev/null +++ b/glutin/src/application.rs @@ -0,0 +1,257 @@ +//! Create interactive, native cross-platform applications. +use crate::{mouse, Executor, Runtime, Size}; +use iced_graphics::window; +use iced_graphics::Viewport; +use iced_winit::application; +use iced_winit::conversion; +use iced_winit::{Clipboard, Debug, Proxy, Settings}; + +pub use iced_winit::Application; +pub use iced_winit::{program, Program}; + +/// Runs an [`Application`] with an executor, compositor, and the provided +/// settings. +/// +/// [`Application`]: trait.Application.html +pub fn run<A, E, C>( + settings: Settings<A::Flags>, + compositor_settings: C::Settings, +) where + A: Application + 'static, + E: Executor + 'static, + C: window::GLCompositor<Renderer = A::Renderer> + 'static, +{ + use glutin::{ + event, + event_loop::{ControlFlow, EventLoop}, + ContextBuilder, + }; + + let mut debug = Debug::new(); + debug.startup_started(); + + let event_loop = EventLoop::with_user_event(); + let mut runtime = { + let executor = E::new().expect("Create executor"); + let proxy = Proxy::new(event_loop.create_proxy()); + + Runtime::new(executor, proxy) + }; + + let flags = settings.flags; + let (application, init_command) = runtime.enter(|| A::new(flags)); + runtime.spawn(init_command); + + let subscription = application.subscription(); + runtime.track(subscription); + + let mut title = application.title(); + let mut mode = application.mode(); + let mut background_color = application.background_color(); + let mut scale_factor = application.scale_factor(); + + let context = { + let builder = settings.window.into_builder( + &title, + mode, + event_loop.primary_monitor(), + ); + + let context = ContextBuilder::new() + .with_vsync(true) + .with_multisampling(C::sample_count(&compositor_settings) as u16) + .build_windowed(builder, &event_loop) + .expect("Open window"); + + #[allow(unsafe_code)] + unsafe { + context.make_current().expect("Make OpenGL context current") + } + }; + + let clipboard = Clipboard::new(&context.window()); + let mut cursor_position = glutin::dpi::PhysicalPosition::new(-1.0, -1.0); + let mut mouse_interaction = mouse::Interaction::default(); + let mut modifiers = glutin::event::ModifiersState::default(); + + let physical_size = context.window().inner_size(); + let mut viewport = Viewport::with_physical_size( + Size::new(physical_size.width, physical_size.height), + context.window().scale_factor() * scale_factor, + ); + let mut resized = false; + + #[allow(unsafe_code)] + let (mut compositor, mut renderer) = unsafe { + C::new(compositor_settings, |address| { + context.get_proc_address(address) + }) + }; + + let mut state = program::State::new( + application, + viewport.logical_size(), + conversion::cursor_position(cursor_position, viewport.scale_factor()), + &mut renderer, + &mut debug, + ); + debug.startup_finished(); + + event_loop.run(move |event, _, control_flow| match event { + event::Event::MainEventsCleared => { + if state.is_queue_empty() { + return; + } + + let command = runtime.enter(|| { + state.update( + viewport.logical_size(), + conversion::cursor_position( + cursor_position, + viewport.scale_factor(), + ), + clipboard.as_ref().map(|c| c as _), + &mut renderer, + &mut debug, + ) + }); + + // If the application was updated + if let Some(command) = command { + runtime.spawn(command); + + let program = state.program(); + + // Update subscriptions + let subscription = program.subscription(); + runtime.track(subscription); + + // Update window title + let new_title = program.title(); + + if title != new_title { + context.window().set_title(&new_title); + + title = new_title; + } + + // Update window mode + let new_mode = program.mode(); + + if mode != new_mode { + context.window().set_fullscreen(conversion::fullscreen( + context.window().current_monitor(), + new_mode, + )); + + mode = new_mode; + } + + // Update background color + background_color = program.background_color(); + + // Update scale factor + let new_scale_factor = program.scale_factor(); + + if scale_factor != new_scale_factor { + let size = context.window().inner_size(); + + viewport = Viewport::with_physical_size( + Size::new(size.width, size.height), + context.window().scale_factor() * new_scale_factor, + ); + + // We relayout the UI with the new logical size. + // The queue is empty, therefore this will never produce + // a `Command`. + // + // TODO: Properly queue `WindowResized` + let _ = state.update( + viewport.logical_size(), + conversion::cursor_position( + cursor_position, + viewport.scale_factor(), + ), + clipboard.as_ref().map(|c| c as _), + &mut renderer, + &mut debug, + ); + + scale_factor = new_scale_factor; + } + } + + context.window().request_redraw(); + } + event::Event::UserEvent(message) => { + state.queue_message(message); + } + event::Event::RedrawRequested(_) => { + debug.render_started(); + + if resized { + let physical_size = viewport.physical_size(); + + context.resize(glutin::dpi::PhysicalSize::new( + physical_size.width, + physical_size.height, + )); + + compositor.resize_viewport(physical_size); + + resized = false; + } + + let new_mouse_interaction = compositor.draw( + &mut renderer, + &viewport, + background_color, + state.primitive(), + &debug.overlay(), + ); + + context.swap_buffers().expect("Swap buffers"); + + debug.render_finished(); + + if new_mouse_interaction != mouse_interaction { + context.window().set_cursor_icon( + conversion::mouse_interaction(new_mouse_interaction), + ); + + mouse_interaction = new_mouse_interaction; + } + + // TODO: Handle animations! + // Maybe we can use `ControlFlow::WaitUntil` for this. + } + event::Event::WindowEvent { + event: window_event, + .. + } => { + application::handle_window_event( + &window_event, + context.window(), + scale_factor, + control_flow, + &mut cursor_position, + &mut modifiers, + &mut viewport, + &mut resized, + &mut debug, + ); + + if let Some(event) = conversion::window_event( + &window_event, + viewport.scale_factor(), + modifiers, + ) { + state.queue_event(event.clone()); + runtime.broadcast(event); + } + } + _ => { + *control_flow = ControlFlow::Wait; + } + }) +} diff --git a/glutin/src/lib.rs b/glutin/src/lib.rs new file mode 100644 index 00000000..b0e0bdd4 --- /dev/null +++ b/glutin/src/lib.rs @@ -0,0 +1,23 @@ +//! A windowing shell for [`iced`], on top of [`glutin`]. +//! +//! [`iced`]: https://github.com/hecrj/iced +//! [`glutin`]: https://github.com/rust-windowing/glutin +#![deny(missing_docs)] +#![deny(missing_debug_implementations)] +#![deny(unused_results)] +#![deny(unsafe_code)] +#![forbid(rust_2018_idioms)] + +pub use glutin; +#[doc(no_inline)] +pub use iced_native::*; + +pub mod application; + +pub use iced_winit::settings; +pub use iced_winit::Mode; + +#[doc(no_inline)] +pub use application::Application; +#[doc(no_inline)] +pub use settings::Settings; diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml new file mode 100644 index 00000000..8e078d75 --- /dev/null +++ b/graphics/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "iced_graphics" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2018" + +[features] +canvas = ["lyon"] +font-source = ["font-kit"] +font-fallback = [] +font-icons = [] +opengl = [] + +[dependencies] +bytemuck = "1.2" +glam = "0.8" +raw-window-handle = "0.3" + +[dependencies.iced_native] +version = "0.2" +path = "../native" + +[dependencies.iced_style] +version = "0.1" +path = "../style" + +[dependencies.lyon] +version = "0.15" +optional = true + +[dependencies.font-kit] +version = "0.6" +optional = true + +[package.metadata.docs.rs] +rustdoc-args = ["--cfg", "docsrs"] +all-features = true diff --git a/wgpu/src/text/icons.ttf b/graphics/fonts/Icons.ttf Binary files differindex 1c832f86..1c832f86 100644 --- a/wgpu/src/text/icons.ttf +++ b/graphics/fonts/Icons.ttf diff --git a/wgpu/fonts/Lato-Regular.ttf b/graphics/fonts/Lato-Regular.ttf Binary files differindex 33eba8b1..33eba8b1 100644 --- a/wgpu/fonts/Lato-Regular.ttf +++ b/graphics/fonts/Lato-Regular.ttf diff --git a/wgpu/fonts/OFL.txt b/graphics/fonts/OFL.txt index dfca0da4..dfca0da4 100644 --- a/wgpu/fonts/OFL.txt +++ b/graphics/fonts/OFL.txt diff --git a/graphics/src/antialiasing.rs b/graphics/src/antialiasing.rs new file mode 100644 index 00000000..34d94711 --- /dev/null +++ b/graphics/src/antialiasing.rs @@ -0,0 +1,26 @@ +/// An antialiasing strategy. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Antialiasing { + /// Multisample AA with 2 samples + MSAAx2, + /// Multisample AA with 4 samples + MSAAx4, + /// Multisample AA with 8 samples + MSAAx8, + /// Multisample AA with 16 samples + MSAAx16, +} + +impl Antialiasing { + /// Returns the amount of samples of the [`Antialiasing`]. + /// + /// [`Antialiasing`]: enum.Antialiasing.html + pub fn sample_count(self) -> u32 { + match self { + Antialiasing::MSAAx2 => 2, + Antialiasing::MSAAx4 => 4, + Antialiasing::MSAAx8 => 8, + Antialiasing::MSAAx16 => 16, + } + } +} diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs new file mode 100644 index 00000000..b73c636e --- /dev/null +++ b/graphics/src/backend.rs @@ -0,0 +1,53 @@ +//! Write a graphics backend. +use iced_native::image; +use iced_native::svg; +use iced_native::{Font, Size}; + +/// The graphics backend of a [`Renderer`]. +/// +/// [`Renderer`]: ../struct.Renderer.html +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. + const ICON_FONT: Font; + + /// The `char` representing a ✔ icon in the [`ICON_FONT`]. + /// + /// [`ICON_FONT`]: #associatedconst.ICON_FONt + const CHECKMARK_ICON: char; + + /// Returns the default size of text. + fn default_size(&self) -> u16; + + /// Measures the text contents with the given size and font, + /// returning the size of a laid out paragraph that fits in the provided + /// bounds. + fn measure( + &self, + contents: &str, + size: f32, + font: Font, + bounds: Size, + ) -> (f32, f32); +} + +/// A graphics backend that supports image rendering. +pub trait Image { + /// Returns the dimensions of the provided image. + fn dimensions(&self, handle: &image::Handle) -> (u32, u32); +} + +/// A graphics backend that supports SVG rendering. +pub trait Svg { + /// Returns the viewport dimensions of the provided SVG. + fn viewport_dimensions(&self, handle: &svg::Handle) -> (u32, u32); +} diff --git a/wgpu/src/defaults.rs b/graphics/src/defaults.rs index 11718a87..11718a87 100644 --- a/wgpu/src/defaults.rs +++ b/graphics/src/defaults.rs diff --git a/graphics/src/font.rs b/graphics/src/font.rs new file mode 100644 index 00000000..bcc28857 --- /dev/null +++ b/graphics/src/font.rs @@ -0,0 +1,33 @@ +//! Find system fonts or use the built-in ones. +#[cfg(feature = "font-source")] +mod source; + +#[cfg(feature = "font-source")] +#[cfg_attr(docsrs, doc(cfg(feature = "font-source")))] +pub use source::Source; + +#[cfg(feature = "font-source")] +#[cfg_attr(docsrs, doc(cfg(feature = "font-source")))] +pub use font_kit::{ + error::SelectionError as LoadError, family_name::FamilyName as Family, +}; + +/// A built-in fallback font, for convenience. +#[cfg(feature = "font-fallback")] +#[cfg_attr(docsrs, doc(cfg(feature = "font-fallback")))] +pub const FALLBACK: &[u8] = include_bytes!("../fonts/Lato-Regular.ttf"); + +/// A built-in icon font, for convenience. +#[cfg(feature = "font-icons")] +#[cfg_attr(docsrs, doc(cfg(feature = "font-icons")))] +pub const ICONS: iced_native::Font = iced_native::Font::External { + name: "iced_wgpu icons", + bytes: include_bytes!("../fonts/Icons.ttf"), +}; + +/// The `char` representing a ✔ icon in the built-in [`ICONS`] font. +/// +/// [`ICONS`]: const.ICONS.html +#[cfg(feature = "font-icons")] +#[cfg_attr(docsrs, doc(cfg(feature = "font-icons")))] +pub const CHECKMARK_ICON: char = '\u{F00C}'; diff --git a/wgpu/src/text/font.rs b/graphics/src/font/source.rs index 7346ccdb..917291ff 100644 --- a/wgpu/src/text/font.rs +++ b/graphics/src/font/source.rs @@ -1,18 +1,24 @@ -pub use font_kit::{ - error::SelectionError as LoadError, family_name::FamilyName as Family, -}; +use crate::font::{Family, LoadError}; +/// A font source that can find and load system fonts. +#[allow(missing_debug_implementations)] pub struct Source { raw: font_kit::source::SystemSource, } impl Source { + /// Creates a new [`Source`]. + /// + /// [`Source`]: struct.Source.html pub fn new() -> Self { Source { raw: font_kit::source::SystemSource::new(), } } + /// Finds and loads a font matching the set of provided family priorities. + /// + /// [`Source`]: struct.Source.html pub fn load(&self, families: &[Family]) -> Result<Vec<u8>, LoadError> { let font = self.raw.select_best_match( families, diff --git a/graphics/src/layer.rs b/graphics/src/layer.rs new file mode 100644 index 00000000..6aca738e --- /dev/null +++ b/graphics/src/layer.rs @@ -0,0 +1,359 @@ +//! Organize rendering primitives into a flattened list of layers. +use crate::image; +use crate::svg; +use crate::triangle; +use crate::{ + Background, Font, HorizontalAlignment, Point, Primitive, Rectangle, Size, + Vector, VerticalAlignment, Viewport, +}; + +/// A group of primitives that should be clipped together. +#[derive(Debug, Clone)] +pub struct Layer<'a> { + /// The clipping bounds of the [`Layer`]. + /// + /// [`Layer`]: struct.Layer.html + pub bounds: Rectangle, + + /// The quads of the [`Layer`]. + /// + /// [`Layer`]: struct.Layer.html + pub quads: Vec<Quad>, + + /// The triangle meshes of the [`Layer`]. + /// + /// [`Layer`]: struct.Layer.html + pub meshes: Vec<Mesh<'a>>, + + /// The text of the [`Layer`]. + /// + /// [`Layer`]: struct.Layer.html + pub text: Vec<Text<'a>>, + + /// The images of the [`Layer`]. + /// + /// [`Layer`]: struct.Layer.html + pub images: Vec<Image>, +} + +impl<'a> Layer<'a> { + /// Creates a new [`Layer`] with the given clipping bounds. + /// + /// [`Layer`]: struct.Layer.html + pub fn new(bounds: Rectangle) -> Self { + Self { + bounds, + quads: Vec::new(), + meshes: Vec::new(), + text: Vec::new(), + images: Vec::new(), + } + } + + /// Creates a new [`Layer`] for the provided overlay text. + /// + /// This can be useful for displaying debug information. + /// + /// [`Layer`]: struct.Layer.html + pub fn overlay(lines: &'a [impl AsRef<str>], viewport: &Viewport) -> Self { + let mut overlay = + Layer::new(Rectangle::with_size(viewport.logical_size())); + + for (i, line) in lines.iter().enumerate() { + let text = Text { + content: line.as_ref(), + bounds: Rectangle::new( + Point::new(11.0, 11.0 + 25.0 * i as f32), + Size::INFINITY, + ), + color: [0.9, 0.9, 0.9, 1.0], + size: 20.0, + font: Font::Default, + horizontal_alignment: HorizontalAlignment::Left, + vertical_alignment: VerticalAlignment::Top, + }; + + overlay.text.push(text); + + overlay.text.push(Text { + bounds: text.bounds + Vector::new(-1.0, -1.0), + color: [0.0, 0.0, 0.0, 1.0], + ..text + }); + } + + overlay + } + + /// Distributes the given [`Primitive`] and generates a list of layers based + /// on its contents. + /// + /// [`Primitive`]: ../enum.Primitive.html + pub fn generate( + primitive: &'a Primitive, + viewport: &Viewport, + ) -> Vec<Self> { + let first_layer = + Layer::new(Rectangle::with_size(viewport.logical_size())); + + let mut layers = vec![first_layer]; + + Self::process_primitive(&mut layers, Vector::new(0.0, 0.0), primitive); + + layers + } + + fn process_primitive( + layers: &mut Vec<Self>, + translation: Vector, + primitive: &'a Primitive, + ) { + match primitive { + Primitive::None => {} + Primitive::Group { primitives } => { + // TODO: Inspect a bit and regroup (?) + for primitive in primitives { + Self::process_primitive(layers, translation, primitive) + } + } + Primitive::Text { + content, + bounds, + size, + color, + font, + horizontal_alignment, + vertical_alignment, + } => { + let layer = layers.last_mut().unwrap(); + + layer.text.push(Text { + content, + bounds: *bounds + translation, + size: *size, + color: color.into_linear(), + font: *font, + horizontal_alignment: *horizontal_alignment, + vertical_alignment: *vertical_alignment, + }); + } + Primitive::Quad { + bounds, + background, + border_radius, + border_width, + border_color, + } => { + let layer = layers.last_mut().unwrap(); + + // TODO: Move some of these computations to the GPU (?) + layer.quads.push(Quad { + position: [ + bounds.x + translation.x, + bounds.y + translation.y, + ], + size: [bounds.width, bounds.height], + color: match background { + Background::Color(color) => color.into_linear(), + }, + border_radius: *border_radius as f32, + border_width: *border_width as f32, + border_color: border_color.into_linear(), + }); + } + Primitive::Mesh2D { buffers, size } => { + let layer = layers.last_mut().unwrap(); + + let bounds = Rectangle::new( + Point::new(translation.x, translation.y), + *size, + ); + + // Only draw visible content + if let Some(clip_bounds) = layer.bounds.intersection(&bounds) { + layer.meshes.push(Mesh { + origin: Point::new(translation.x, translation.y), + buffers, + clip_bounds, + }); + } + } + Primitive::Clip { + bounds, + offset, + content, + } => { + let layer = layers.last_mut().unwrap(); + let translated_bounds = *bounds + translation; + + // Only draw visible content + if let Some(clip_bounds) = + layer.bounds.intersection(&translated_bounds) + { + let clip_layer = Layer::new(clip_bounds); + let new_layer = Layer::new(layer.bounds); + + layers.push(clip_layer); + Self::process_primitive( + layers, + translation + - Vector::new(offset.x as f32, offset.y as f32), + content, + ); + layers.push(new_layer); + } + } + Primitive::Translate { + translation: new_translation, + content, + } => { + Self::process_primitive( + layers, + translation + *new_translation, + &content, + ); + } + Primitive::Cached { cache } => { + Self::process_primitive(layers, translation, &cache); + } + Primitive::Image { handle, bounds } => { + let layer = layers.last_mut().unwrap(); + + layer.images.push(Image::Raster { + handle: handle.clone(), + bounds: *bounds + translation, + }); + } + Primitive::Svg { handle, bounds } => { + let layer = layers.last_mut().unwrap(); + + layer.images.push(Image::Vector { + handle: handle.clone(), + bounds: *bounds + translation, + }); + } + } + } +} + +/// 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`]. + /// + /// [`Quad`]: struct.Quad.html + pub position: [f32; 2], + + /// The size of the [`Quad`]. + /// + /// [`Quad`]: struct.Quad.html + pub size: [f32; 2], + + /// The color of the [`Quad`], in __linear RGB__. + /// + /// [`Quad`]: struct.Quad.html + pub color: [f32; 4], + + /// The border color of the [`Quad`], in __linear RGB__. + /// + /// [`Quad`]: struct.Quad.html + pub border_color: [f32; 4], + + /// The border radius of the [`Quad`]. + /// + /// [`Quad`]: struct.Quad.html + pub border_radius: f32, + + /// The border width of the [`Quad`]. + /// + /// [`Quad`]: struct.Quad.html + pub border_width: f32, +} + +/// A mesh of triangles. +#[derive(Debug, Clone, Copy)] +pub struct Mesh<'a> { + /// The origin of the vertices of the [`Mesh`]. + /// + /// [`Mesh`]: struct.Mesh.html + pub origin: Point, + + /// The vertex and index buffers of the [`Mesh`]. + /// + /// [`Mesh`]: struct.Mesh.html + pub buffers: &'a triangle::Mesh2D, + + /// The clipping bounds of the [`Mesh`]. + /// + /// [`Mesh`]: struct.Mesh.html + pub clip_bounds: Rectangle<f32>, +} + +/// A paragraph of text. +#[derive(Debug, Clone, Copy)] +pub struct Text<'a> { + /// The content of the [`Text`]. + /// + /// [`Text`]: struct.Text.html + pub content: &'a str, + + /// The layout bounds of the [`Text`]. + /// + /// [`Text`]: struct.Text.html + pub bounds: Rectangle, + + /// The color of the [`Text`], in __linear RGB_. + /// + /// [`Text`]: struct.Text.html + pub color: [f32; 4], + + /// The size of the [`Text`]. + /// + /// [`Text`]: struct.Text.html + pub size: f32, + + /// The font of the [`Text`]. + /// + /// [`Text`]: struct.Text.html + pub font: Font, + + /// The horizontal alignment of the [`Text`]. + /// + /// [`Text`]: struct.Text.html + pub horizontal_alignment: HorizontalAlignment, + + /// The vertical alignment of the [`Text`]. + /// + /// [`Text`]: struct.Text.html + pub vertical_alignment: VerticalAlignment, +} + +/// A raster or vector image. +#[derive(Debug, Clone)] +pub enum Image { + /// A raster image. + Raster { + /// The handle of a raster image. + handle: image::Handle, + + /// The bounds of the image. + bounds: Rectangle, + }, + /// A vector image. + Vector { + /// The handle of a vector image. + handle: svg::Handle, + + /// The bounds of the image. + bounds: Rectangle, + }, +} + +#[allow(unsafe_code)] +unsafe impl bytemuck::Zeroable for Quad {} + +#[allow(unsafe_code)] +unsafe impl bytemuck::Pod for Quad {} diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs new file mode 100644 index 00000000..38d8dffa --- /dev/null +++ b/graphics/src/lib.rs @@ -0,0 +1,40 @@ +//! A bunch of backend-agnostic types that can be leveraged to build a renderer +//! for [`iced`]. +//! +//! [`iced`]: https://github.com/hecrj/iced +#![deny(missing_docs)] +#![deny(missing_debug_implementations)] +#![deny(unused_results)] +#![deny(unsafe_code)] +#![forbid(rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg))] +mod antialiasing; +mod primitive; +mod renderer; +mod transformation; +mod viewport; +mod widget; + +pub mod backend; +pub mod defaults; +pub mod font; +pub mod layer; +pub mod triangle; +pub mod window; + +#[doc(no_inline)] +pub use widget::*; + +pub use antialiasing::Antialiasing; +pub use backend::Backend; +pub use defaults::Defaults; +pub use layer::Layer; +pub use primitive::Primitive; +pub use renderer::Renderer; +pub use transformation::Transformation; +pub use viewport::Viewport; + +pub use iced_native::{ + Background, Color, Font, HorizontalAlignment, Point, Rectangle, Size, + Vector, VerticalAlignment, +}; diff --git a/wgpu/src/primitive.rs b/graphics/src/primitive.rs index 46d9e624..95dbf7dd 100644 --- a/wgpu/src/primitive.rs +++ b/graphics/src/primitive.rs @@ -1,5 +1,5 @@ use iced_native::{ - image, svg, Background, Color, Font, HorizontalAlignment, Point, Rectangle, + image, svg, Background, Color, Font, HorizontalAlignment, Rectangle, Size, Vector, VerticalAlignment, }; @@ -70,24 +70,31 @@ pub enum Primitive { /// The content of the clip content: Box<Primitive>, }, + /// A primitive that applies a translation + Translate { + /// The translation vector + translation: Vector, + + /// The primitive to translate + content: Box<Primitive>, + }, /// A low-level primitive to render a mesh of triangles. /// /// It can be used to render many kinds of geometry freely. Mesh2D { - /// The top-left coordinate of the mesh - origin: Point, - /// The vertex and index buffers of the mesh buffers: triangle::Mesh2D, + + /// The size of the drawable region of the mesh. + /// + /// Any geometry that falls out of this region will be clipped. + size: Size, }, /// A cached primitive. /// /// This can be useful if you are implementing a widget where primitive /// generation is expensive. Cached { - /// The origin of the coordinate system of the cached primitives - origin: Point, - /// The cached primitive cache: Arc<Primitive>, }, diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs new file mode 100644 index 00000000..c9360f3a --- /dev/null +++ b/graphics/src/renderer.rs @@ -0,0 +1,98 @@ +use crate::{Backend, Defaults, Primitive}; +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::{Background, Color, Element, Point, Widget}; + +/// A backend-agnostic renderer that supports all the built-in widgets. +#[derive(Debug)] +pub struct Renderer<B: Backend> { + backend: B, +} + +impl<B: Backend> Renderer<B> { + /// Creates a new [`Renderer`] from the given [`Backend`]. + /// + /// [`Renderer`]: struct.Renderer.html + /// [`Backend`]: backend/trait.Backend.html + pub fn new(backend: B) -> Self { + Self { backend } + } + + /// Returns a reference to the [`Backend`] of the [`Renderer`]. + /// + /// [`Renderer`]: struct.Renderer.html + /// [`Backend`]: backend/trait.Backend.html + pub fn backend(&self) -> &B { + &self.backend + } + + /// Returns a mutable reference to the [`Backend`] of the [`Renderer`]. + /// + /// [`Renderer`]: struct.Renderer.html + /// [`Backend`]: backend/trait.Backend.html + pub fn backend_mut(&mut self) -> &mut B { + &mut self.backend + } +} + +impl<B> iced_native::Renderer for Renderer<B> +where + B: Backend, +{ + type Output = (Primitive, mouse::Interaction); + type Defaults = Defaults; + + fn layout<'a, Message>( + &mut self, + element: &Element<'a, Message, Self>, + limits: &layout::Limits, + ) -> layout::Node { + let layout = element.layout(self, limits); + + self.backend.trim_measurements(); + + layout + } +} + +impl<B> layout::Debugger for Renderer<B> +where + B: Backend, +{ + fn explain<Message>( + &mut self, + defaults: &Defaults, + widget: &dyn Widget<Message, Self>, + layout: Layout<'_>, + cursor_position: Point, + color: Color, + ) -> Self::Output { + let (primitive, cursor) = + widget.draw(self, defaults, layout, cursor_position); + + let mut primitives = Vec::new(); + + explain_layout(layout, color, &mut primitives); + primitives.push(primitive); + + (Primitive::Group { primitives }, cursor) + } +} + +fn explain_layout( + layout: Layout<'_>, + color: Color, + primitives: &mut Vec<Primitive>, +) { + primitives.push(Primitive::Quad { + bounds: layout.bounds(), + background: Background::Color(Color::TRANSPARENT), + border_radius: 0, + border_width: 1, + border_color: [0.6, 0.6, 0.6, 0.5].into(), + }); + + for child in layout.children() { + explain_layout(child, color, primitives); + } +} diff --git a/wgpu/src/transformation.rs b/graphics/src/transformation.rs index ff3b1d00..ff3b1d00 100644 --- a/wgpu/src/transformation.rs +++ b/graphics/src/transformation.rs diff --git a/graphics/src/triangle.rs b/graphics/src/triangle.rs new file mode 100644 index 00000000..ce879ffc --- /dev/null +++ b/graphics/src/triangle.rs @@ -0,0 +1,32 @@ +//! Draw geometry using meshes of triangles. + +/// A set of [`Vertex2D`] and indices representing a list of triangles. +/// +/// [`Vertex2D`]: struct.Vertex2D.html +#[derive(Clone, Debug)] +pub struct Mesh2D { + /// The vertices of the mesh + pub vertices: Vec<Vertex2D>, + + /// The list of vertex indices that defines the triangles of the mesh. + /// + /// Therefore, this list should always have a length that is a multiple of + /// 3. + pub indices: Vec<u32>, +} + +/// A two-dimensional vertex with some color in __linear__ RGBA. +#[derive(Copy, Clone, Debug)] +#[repr(C)] +pub struct Vertex2D { + /// The vertex position + pub position: [f32; 2], + /// The vertex color in __linear__ RGBA. + pub color: [f32; 4], +} + +#[allow(unsafe_code)] +unsafe impl bytemuck::Zeroable for Vertex2D {} + +#[allow(unsafe_code)] +unsafe impl bytemuck::Pod for Vertex2D {} diff --git a/graphics/src/viewport.rs b/graphics/src/viewport.rs new file mode 100644 index 00000000..66122e6d --- /dev/null +++ b/graphics/src/viewport.rs @@ -0,0 +1,70 @@ +use crate::{Size, Transformation}; + +/// A viewing region for displaying computer graphics. +#[derive(Debug)] +pub struct Viewport { + physical_size: Size<u32>, + logical_size: Size<f32>, + scale_factor: f64, + projection: Transformation, +} + +impl Viewport { + /// Creates a new [`Viewport`] with the given physical dimensions and scale + /// factor. + /// + /// [`Viewport`]: struct.Viewport.html + pub fn with_physical_size(size: Size<u32>, scale_factor: f64) -> Viewport { + Viewport { + physical_size: size, + logical_size: Size::new( + (size.width as f64 / scale_factor) as f32, + (size.height as f64 / scale_factor) as f32, + ), + scale_factor, + projection: Transformation::orthographic(size.width, size.height), + } + } + + /// Returns the physical size of the [`Viewport`]. + /// + /// [`Viewport`]: struct.Viewport.html + pub fn physical_size(&self) -> Size<u32> { + self.physical_size + } + + /// Returns the physical width of the [`Viewport`]. + /// + /// [`Viewport`]: struct.Viewport.html + pub fn physical_width(&self) -> u32 { + self.physical_size.height + } + + /// Returns the physical height of the [`Viewport`]. + /// + /// [`Viewport`]: struct.Viewport.html + pub fn physical_height(&self) -> u32 { + self.physical_size.height + } + + /// Returns the logical size of the [`Viewport`]. + /// + /// [`Viewport`]: struct.Viewport.html + pub fn logical_size(&self) -> Size<f32> { + self.logical_size + } + + /// Returns the scale factor of the [`Viewport`]. + /// + /// [`Viewport`]: struct.Viewport.html + pub fn scale_factor(&self) -> f64 { + self.scale_factor + } + + /// Returns the projection transformation of the [`Viewport`]. + /// + /// [`Viewport`]: struct.Viewport.html + pub fn projection(&self) -> Transformation { + self.projection + } +} diff --git a/graphics/src/widget.rs b/graphics/src/widget.rs new file mode 100644 index 00000000..1f6d6559 --- /dev/null +++ b/graphics/src/widget.rs @@ -0,0 +1,59 @@ +//! Use the widgets supported out-of-the-box. +//! +//! # Re-exports +//! For convenience, the contents of this module are available at the root +//! module. Therefore, you can directly type: +//! +//! ``` +//! use iced_graphics::{button, Button}; +//! ``` +pub mod button; +pub mod checkbox; +pub mod container; +pub mod image; +pub mod pane_grid; +pub mod progress_bar; +pub mod radio; +pub mod scrollable; +pub mod slider; +pub mod svg; +pub mod text_input; + +mod column; +mod row; +mod space; +mod text; + +#[doc(no_inline)] +pub use button::Button; +#[doc(no_inline)] +pub use checkbox::Checkbox; +#[doc(no_inline)] +pub use container::Container; +#[doc(no_inline)] +pub use pane_grid::PaneGrid; +#[doc(no_inline)] +pub use progress_bar::ProgressBar; +#[doc(no_inline)] +pub use radio::Radio; +#[doc(no_inline)] +pub use scrollable::Scrollable; +#[doc(no_inline)] +pub use slider::Slider; +#[doc(no_inline)] +pub use text_input::TextInput; + +pub use column::Column; +pub use image::Image; +pub use row::Row; +pub use space::Space; +pub use svg::Svg; +pub use text::Text; + +#[cfg(feature = "canvas")] +#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] +pub mod canvas; + +#[cfg(feature = "canvas")] +#[doc(no_inline)] +pub use canvas::Canvas; diff --git a/wgpu/src/renderer/widget/button.rs b/graphics/src/widget/button.rs index 359b4866..ecabc868 100644 --- a/wgpu/src/renderer/widget/button.rs +++ b/graphics/src/widget/button.rs @@ -1,9 +1,29 @@ -use crate::{button::StyleSheet, defaults, Defaults, Primitive, Renderer}; +//! Allow your users to perform actions by pressing a button. +//! +//! A [`Button`] has some local [`State`]. +//! +//! [`Button`]: type.Button.html +//! [`State`]: struct.State.html +use crate::defaults::{self, Defaults}; +use crate::{Backend, Primitive, Renderer}; +use iced_native::mouse; use iced_native::{ - Background, Color, Element, Layout, MouseCursor, Point, Rectangle, Vector, + Background, Color, Element, Layout, Point, Rectangle, Vector, }; -impl iced_native::button::Renderer for Renderer { +pub use iced_native::button::State; +pub use iced_style::button::{Style, StyleSheet}; + +/// A widget that produces a message when clicked. +/// +/// This is an alias of an `iced_native` button with an `iced_wgpu::Renderer`. +pub type Button<'a, Message, Backend> = + iced_native::Button<'a, Message, Renderer<Backend>>; + +impl<B> iced_native::button::Renderer for Renderer<B> +where + B: Backend, +{ const DEFAULT_PADDING: u16 = 5; type Style = Box<dyn StyleSheet>; @@ -83,10 +103,10 @@ impl iced_native::button::Renderer for Renderer { } else { content }, - if is_mouse_over { - MouseCursor::Pointer + if is_mouse_over && !is_disabled { + mouse::Interaction::Pointer } else { - MouseCursor::OutOfBounds + mouse::Interaction::default() }, ) } diff --git a/graphics/src/widget/canvas.rs b/graphics/src/widget/canvas.rs new file mode 100644 index 00000000..b8466239 --- /dev/null +++ b/graphics/src/widget/canvas.rs @@ -0,0 +1,236 @@ +//! 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! +//! +//! [`Canvas`]: struct.Canvas.html +//! [`Frame`]: struct.Frame.html +use crate::{Backend, Defaults, Primitive, Renderer}; +use iced_native::{ + layout, mouse, Clipboard, Element, Hasher, Layout, Length, Point, Size, + Vector, Widget, +}; +use std::hash::Hash; +use std::marker::PhantomData; + +pub mod path; + +mod cache; +mod cursor; +mod event; +mod fill; +mod frame; +mod geometry; +mod program; +mod stroke; +mod text; + +pub use cache::Cache; +pub use cursor::Cursor; +pub use event::Event; +pub use fill::{Fill, FillRule}; +pub use frame::Frame; +pub use geometry::Geometry; +pub use path::Path; +pub use program::Program; +pub use stroke::{LineCap, LineJoin, Stroke}; +pub use text::Text; + +/// A widget capable of drawing 2D graphics. +/// +/// [`Canvas`]: struct.Canvas.html +/// +/// # Examples +/// The repository has a couple of [examples] showcasing how to use a +/// [`Canvas`]: +/// +/// - [`clock`], an application that uses the [`Canvas`] widget to draw a clock +/// and its hands to display the current time. +/// - [`game_of_life`], an interactive version of the Game of Life, invented by +/// John Conway. +/// - [`solar_system`], an animated solar system drawn using the [`Canvas`] widget +/// and showcasing how to compose different transforms. +/// +/// [examples]: https://github.com/hecrj/iced/tree/master/examples +/// [`clock`]: https://github.com/hecrj/iced/tree/master/examples/clock +/// [`game_of_life`]: https://github.com/hecrj/iced/tree/master/examples/game_of_life +/// [`solar_system`]: https://github.com/hecrj/iced/tree/master/examples/solar_system +/// +/// ## Drawing a simple circle +/// If you want to get a quick overview, here's how we can draw a simple circle: +/// +/// ```no_run +/// # mod iced { +/// # pub use iced_graphics::canvas; +/// # pub use iced_native::{Color, Rectangle}; +/// # } +/// use iced::canvas::{self, Canvas, Cursor, Fill, Frame, Geometry, Path, Program}; +/// use iced::{Color, Rectangle}; +/// +/// // First, we define the data we need for drawing +/// #[derive(Debug)] +/// struct Circle { +/// radius: f32, +/// } +/// +/// // Then, we implement the `Program` trait +/// impl Program<()> for Circle { +/// fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec<Geometry>{ +/// // We prepare a new `Frame` +/// let mut frame = Frame::new(bounds.size()); +/// +/// // We create a `Path` representing a simple circle +/// let circle = Path::circle(frame.center(), self.radius); +/// +/// // And fill it with some color +/// frame.fill(&circle, Color::BLACK); +/// +/// // Finally, we produce the geometry +/// vec![frame.into_geometry()] +/// } +/// } +/// +/// // Finally, we simply use our `Circle` to create the `Canvas`! +/// let canvas = Canvas::new(Circle { radius: 50.0 }); +/// ``` +#[derive(Debug)] +pub struct Canvas<Message, P: Program<Message>> { + width: Length, + height: Length, + program: P, + phantom: PhantomData<Message>, +} + +impl<Message, P: Program<Message>> Canvas<Message, P> { + const DEFAULT_SIZE: u16 = 100; + + /// Creates a new [`Canvas`]. + /// + /// [`Canvas`]: struct.Canvas.html + pub fn new(program: P) -> Self { + Canvas { + width: Length::Units(Self::DEFAULT_SIZE), + height: Length::Units(Self::DEFAULT_SIZE), + program, + phantom: PhantomData, + } + } + + /// Sets the width of the [`Canvas`]. + /// + /// [`Canvas`]: struct.Canvas.html + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the height of the [`Canvas`]. + /// + /// [`Canvas`]: struct.Canvas.html + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } +} + +impl<Message, P, B> Widget<Message, Renderer<B>> for Canvas<Message, P> +where + P: Program<Message>, + B: Backend, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + _renderer: &Renderer<B>, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + let size = limits.resolve(Size::ZERO); + + layout::Node::new(size) + } + + fn on_event( + &mut self, + event: iced_native::Event, + layout: Layout<'_>, + cursor_position: Point, + messages: &mut Vec<Message>, + _renderer: &Renderer<B>, + _clipboard: Option<&dyn Clipboard>, + ) { + let bounds = layout.bounds(); + + let canvas_event = match event { + iced_native::Event::Mouse(mouse_event) => { + Some(Event::Mouse(mouse_event)) + } + _ => None, + }; + + let cursor = Cursor::from_window_position(cursor_position); + + if let Some(canvas_event) = canvas_event { + if let Some(message) = + self.program.update(canvas_event, bounds, cursor) + { + messages.push(message); + } + } + } + + fn draw( + &self, + _renderer: &mut Renderer<B>, + _defaults: &Defaults, + layout: Layout<'_>, + cursor_position: Point, + ) -> (Primitive, mouse::Interaction) { + let bounds = layout.bounds(); + let translation = Vector::new(bounds.x, bounds.y); + let cursor = Cursor::from_window_position(cursor_position); + + ( + Primitive::Translate { + translation, + content: Box::new(Primitive::Group { + primitives: self + .program + .draw(bounds, cursor) + .into_iter() + .map(Geometry::into_primitive) + .collect(), + }), + }, + self.program.mouse_interaction(bounds, cursor), + ) + } + + fn hash_layout(&self, state: &mut Hasher) { + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + + self.width.hash(state); + self.height.hash(state); + } +} + +impl<'a, Message, P, B> From<Canvas<Message, P>> + for Element<'a, Message, Renderer<B>> +where + Message: 'static, + P: Program<Message> + 'a, + B: Backend, +{ + fn from(canvas: Canvas<Message, P>) -> Element<'a, Message, Renderer<B>> { + Element::new(canvas) + } +} diff --git a/graphics/src/widget/canvas/cache.rs b/graphics/src/widget/canvas/cache.rs new file mode 100644 index 00000000..4b28d164 --- /dev/null +++ b/graphics/src/widget/canvas/cache.rs @@ -0,0 +1,108 @@ +use crate::{ + canvas::{Frame, Geometry}, + Primitive, +}; + +use iced_native::Size; +use std::{cell::RefCell, sync::Arc}; + +enum State { + Empty, + Filled { + bounds: Size, + primitive: Arc<Primitive>, + }, +} + +impl Default for State { + fn default() -> Self { + State::Empty + } +} +/// A simple cache that stores generated [`Geometry`] to avoid recomputation. +/// +/// A [`Cache`] will not redraw its geometry unless the dimensions of its layer +/// change or it is explicitly cleared. +/// +/// [`Layer`]: ../trait.Layer.html +/// [`Cache`]: struct.Cache.html +/// [`Geometry`]: struct.Geometry.html +#[derive(Debug, Default)] +pub struct Cache { + state: RefCell<State>, +} + +impl Cache { + /// Creates a new empty [`Cache`]. + /// + /// [`Cache`]: struct.Cache.html + pub fn new() -> Self { + Cache { + state: Default::default(), + } + } + + /// Clears the [`Cache`], forcing a redraw the next time it is used. + /// + /// [`Cache`]: struct.Cache.html + pub fn clear(&mut self) { + *self.state.borrow_mut() = State::Empty; + } + + /// Draws [`Geometry`] using the provided closure and stores it in the + /// [`Cache`]. + /// + /// The closure will only be called when + /// - the bounds have changed since the previous draw call. + /// - the [`Cache`] is empty or has been explicitly cleared. + /// + /// Otherwise, the previously stored [`Geometry`] will be returned. The + /// [`Cache`] is not cleared in this case. In other words, it will keep + /// returning the stored [`Geometry`] if needed. + /// + /// [`Cache`]: struct.Cache.html + pub fn draw(&self, bounds: Size, draw_fn: impl Fn(&mut Frame)) -> Geometry { + use std::ops::Deref; + + if let State::Filled { + bounds: cached_bounds, + primitive, + } = self.state.borrow().deref() + { + if *cached_bounds == bounds { + return Geometry::from_primitive(Primitive::Cached { + cache: primitive.clone(), + }); + } + } + + let mut frame = Frame::new(bounds); + draw_fn(&mut frame); + + let primitive = { + let geometry = frame.into_geometry(); + + Arc::new(geometry.into_primitive()) + }; + + *self.state.borrow_mut() = State::Filled { + bounds, + primitive: primitive.clone(), + }; + + Geometry::from_primitive(Primitive::Cached { cache: primitive }) + } +} + +impl std::fmt::Debug for State { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + State::Empty => write!(f, "Empty"), + State::Filled { primitive, bounds } => f + .debug_struct("Filled") + .field("primitive", primitive) + .field("bounds", bounds) + .finish(), + } + } +} diff --git a/graphics/src/widget/canvas/cursor.rs b/graphics/src/widget/canvas/cursor.rs new file mode 100644 index 00000000..456760ea --- /dev/null +++ b/graphics/src/widget/canvas/cursor.rs @@ -0,0 +1,72 @@ +use iced_native::{Point, Rectangle}; + +/// The mouse cursor state. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Cursor { + /// The cursor has a defined position. + Available(Point), + + /// The cursor is currently unavailable (i.e. out of bounds or busy). + Unavailable, +} + +impl Cursor { + // TODO: Remove this once this type is used in `iced_native` to encode + // proper cursor availability + pub(crate) fn from_window_position(position: Point) -> Self { + if position.x < 0.0 || position.y < 0.0 { + Cursor::Unavailable + } else { + Cursor::Available(position) + } + } + + /// Returns the absolute position of the [`Cursor`], if available. + /// + /// [`Cursor`]: enum.Cursor.html + 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`. + /// + /// [`Cursor`]: enum.Cursor.html + 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. + /// + /// [`Cursor`]: enum.Cursor.html + 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. + /// + /// [`Cursor`]: enum.Cursor.html + pub fn is_over(&self, bounds: &Rectangle) -> bool { + match self { + Cursor::Available(position) => bounds.contains(*position), + Cursor::Unavailable => false, + } + } +} diff --git a/graphics/src/widget/canvas/event.rs b/graphics/src/widget/canvas/event.rs new file mode 100644 index 00000000..ad11f51e --- /dev/null +++ b/graphics/src/widget/canvas/event.rs @@ -0,0 +1,10 @@ +use iced_native::mouse; + +/// A [`Canvas`] event. +/// +/// [`Canvas`]: struct.Event.html +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Event { + /// A mouse event. + Mouse(mouse::Event), +} diff --git a/graphics/src/widget/canvas/fill.rs b/graphics/src/widget/canvas/fill.rs new file mode 100644 index 00000000..56495435 --- /dev/null +++ b/graphics/src/widget/canvas/fill.rs @@ -0,0 +1,60 @@ +use iced_native::Color; + +/// The style used to fill geometry. +#[derive(Debug, Clone, Copy)] +pub struct Fill { + /// The color used to fill geometry. + /// + /// By default, it is set to `BLACK`. + pub color: Color, + + /// The fill rule defines how to determine what is inside and what is + /// outside of a shape. + /// + /// See the [SVG specification][1] for more details. + /// + /// By default, it is set to `NonZero`. + /// + /// [1]: https://www.w3.org/TR/SVG/painting.html#FillRuleProperty + pub rule: FillRule, +} + +impl Default for Fill { + fn default() -> Fill { + Fill { + color: Color::BLACK, + rule: FillRule::NonZero, + } + } +} + +impl From<Color> for Fill { + fn from(color: Color) -> Fill { + Fill { + color, + ..Fill::default() + } + } +} + +/// The fill rule defines how to determine what is inside and what is outside of +/// a shape. +/// +/// See the [SVG specification][1]. +/// +/// [1]: https://www.w3.org/TR/SVG/painting.html#FillRuleProperty +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(missing_docs)] +pub enum FillRule { + NonZero, + EvenOdd, +} + +impl From<FillRule> for lyon::tessellation::FillRule { + fn from(rule: FillRule) -> lyon::tessellation::FillRule { + match rule { + FillRule::NonZero => lyon::tessellation::FillRule::NonZero, + FillRule::EvenOdd => lyon::tessellation::FillRule::EvenOdd, + } + } +} diff --git a/wgpu/src/widget/canvas/frame.rs b/graphics/src/widget/canvas/frame.rs index de4717f1..b5c6a2b1 100644 --- a/wgpu/src/widget/canvas/frame.rs +++ b/graphics/src/widget/canvas/frame.rs @@ -1,7 +1,7 @@ use iced_native::{Point, Rectangle, Size, Vector}; use crate::{ - canvas::{Fill, Path, Stroke, Text}, + canvas::{Fill, Geometry, Path, Stroke, Text}, triangle, Primitive, }; @@ -10,8 +10,7 @@ use crate::{ /// [`Canvas`]: struct.Canvas.html #[derive(Debug)] pub struct Frame { - width: f32, - height: f32, + size: Size, buffers: lyon::tessellation::VertexBuffers<triangle::Vertex2D, u32>, primitives: Vec<Primitive>, transforms: Transforms, @@ -36,10 +35,9 @@ impl Frame { /// top-left corner of its bounds. /// /// [`Frame`]: struct.Frame.html - pub fn new(width: f32, height: f32) -> Frame { + pub fn new(size: Size) -> Frame { Frame { - width, - height, + size, buffers: lyon::tessellation::VertexBuffers::new(), primitives: Vec::new(), transforms: Transforms { @@ -57,7 +55,7 @@ impl Frame { /// [`Frame`]: struct.Frame.html #[inline] pub fn width(&self) -> f32 { - self.width + self.size.width } /// Returns the width of the [`Frame`]. @@ -65,7 +63,7 @@ impl Frame { /// [`Frame`]: struct.Frame.html #[inline] pub fn height(&self) -> f32 { - self.height + self.size.height } /// Returns the dimensions of the [`Frame`]. @@ -73,7 +71,7 @@ impl Frame { /// [`Frame`]: struct.Frame.html #[inline] pub fn size(&self) -> Size { - Size::new(self.width, self.height) + self.size } /// Returns the coordinate of the center of the [`Frame`]. @@ -81,7 +79,7 @@ impl Frame { /// [`Frame`]: struct.Frame.html #[inline] pub fn center(&self) -> Point { - Point::new(self.width / 2.0, self.height / 2.0) + Point::new(self.size.width / 2.0, self.size.height / 2.0) } /// Draws the given [`Path`] on the [`Frame`] by filling it with the @@ -94,34 +92,64 @@ impl Frame { BuffersBuilder, FillOptions, FillTessellator, }; + let Fill { color, rule } = fill.into(); + let mut buffers = BuffersBuilder::new( &mut self.buffers, - FillVertex(match fill.into() { - Fill::Color(color) => color.into_linear(), - }), + FillVertex(color.into_linear()), ); let mut tessellator = FillTessellator::new(); + let options = FillOptions::default().with_fill_rule(rule.into()); let result = if self.transforms.current.is_identity { - tessellator.tessellate_path( - path.raw(), - &FillOptions::default(), - &mut buffers, - ) + tessellator.tessellate_path(path.raw(), &options, &mut buffers) } else { let path = path.transformed(&self.transforms.current.raw); - tessellator.tessellate_path( - path.raw(), - &FillOptions::default(), - &mut buffers, - ) + tessellator.tessellate_path(path.raw(), &options, &mut buffers) }; let _ = result.expect("Tessellate path"); } + /// Draws an axis-aligned rectangle given its top-left corner coordinate and + /// its `Size` on the [`Frame`] by filling it with the provided style. + /// + /// [`Frame`]: struct.Frame.html + pub fn fill_rectangle( + &mut self, + top_left: Point, + size: Size, + fill: impl Into<Fill>, + ) { + use lyon::tessellation::{BuffersBuilder, FillOptions}; + + let Fill { color, rule } = fill.into(); + + let mut buffers = BuffersBuilder::new( + &mut self.buffers, + FillVertex(color.into_linear()), + ); + + let top_left = + self.transforms.current.raw.transform_point( + lyon::math::Point::new(top_left.x, top_left.y), + ); + + let size = + self.transforms.current.raw.transform_vector( + lyon::math::Vector::new(size.width, size.height), + ); + + let _ = lyon::tessellation::basic_shapes::fill_rectangle( + &lyon::math::Rect::new(top_left, size.into()), + &FillOptions::default().with_fill_rule(rule.into()), + &mut buffers, + ) + .expect("Fill rectangle"); + } + /// Draws the stroke of the given [`Path`] on the [`Frame`] with the /// provided style. /// @@ -262,28 +290,43 @@ impl Frame { self.transforms.current.is_identity = false; } - /// Produces the primitive representing everything drawn on the [`Frame`]. + /// Produces the [`Geometry`] representing everything drawn on the [`Frame`]. /// /// [`Frame`]: struct.Frame.html - pub fn into_primitive(mut self) -> Primitive { + /// [`Geometry`]: struct.Geometry.html + pub fn into_geometry(mut self) -> Geometry { if !self.buffers.indices.is_empty() { self.primitives.push(Primitive::Mesh2D { - origin: Point::ORIGIN, buffers: triangle::Mesh2D { vertices: self.buffers.vertices, indices: self.buffers.indices, }, + size: self.size, }); } - Primitive::Group { + Geometry::from_primitive(Primitive::Group { primitives: self.primitives, - } + }) } } struct FillVertex([f32; 4]); +impl lyon::tessellation::BasicVertexConstructor<triangle::Vertex2D> + for FillVertex +{ + fn new_vertex( + &mut self, + position: lyon::math::Point, + ) -> triangle::Vertex2D { + triangle::Vertex2D { + position: [position.x, position.y], + color: self.0, + } + } +} + impl lyon::tessellation::FillVertexConstructor<triangle::Vertex2D> for FillVertex { diff --git a/graphics/src/widget/canvas/geometry.rs b/graphics/src/widget/canvas/geometry.rs new file mode 100644 index 00000000..4cadee39 --- /dev/null +++ b/graphics/src/widget/canvas/geometry.rs @@ -0,0 +1,34 @@ +use crate::Primitive; + +/// A bunch of shapes that can be drawn. +/// +/// [`Geometry`] can be easily generated with a [`Frame`] or stored in a +/// [`Cache`]. +/// +/// [`Geometry`]: struct.Geometry.html +/// [`Frame`]: struct.Frame.html +/// [`Cache`]: struct.Cache.html +#[derive(Debug, Clone)] +pub struct Geometry(Primitive); + +impl Geometry { + pub(crate) fn from_primitive(primitive: Primitive) -> Self { + Self(primitive) + } + + /// Turns the [`Geometry`] into a [`Primitive`]. + /// + /// This can be useful if you are building a custom widget. + /// + /// [`Geometry`]: struct.Geometry.html + /// [`Primitive`]: ../enum.Primitive.html + pub fn into_primitive(self) -> Primitive { + self.0 + } +} + +impl From<Geometry> for Primitive { + fn from(geometry: Geometry) -> Primitive { + geometry.0 + } +} diff --git a/wgpu/src/widget/canvas/path.rs b/graphics/src/widget/canvas/path.rs index c26bf187..c26bf187 100644 --- a/wgpu/src/widget/canvas/path.rs +++ b/graphics/src/widget/canvas/path.rs diff --git a/wgpu/src/widget/canvas/path/arc.rs b/graphics/src/widget/canvas/path/arc.rs index 343191f1..343191f1 100644 --- a/wgpu/src/widget/canvas/path/arc.rs +++ b/graphics/src/widget/canvas/path/arc.rs diff --git a/wgpu/src/widget/canvas/path/builder.rs b/graphics/src/widget/canvas/path/builder.rs index 6511fa52..e0e52845 100644 --- a/wgpu/src/widget/canvas/path/builder.rs +++ b/graphics/src/widget/canvas/path/builder.rs @@ -84,7 +84,7 @@ impl Builder { radii: math::Vector::new(arc.radii.x, arc.radii.y), x_rotation: math::Angle::radians(arc.rotation), start_angle: math::Angle::radians(arc.start_angle), - sweep_angle: math::Angle::radians(arc.end_angle), + sweep_angle: math::Angle::radians(arc.end_angle - arc.start_angle), }; let _ = self.raw.move_to(arc.sample(0.0)); diff --git a/graphics/src/widget/canvas/program.rs b/graphics/src/widget/canvas/program.rs new file mode 100644 index 00000000..725d9d72 --- /dev/null +++ b/graphics/src/widget/canvas/program.rs @@ -0,0 +1,85 @@ +use crate::canvas::{Cursor, Event, Geometry}; +use iced_native::{mouse, Rectangle}; + +/// The state and logic of a [`Canvas`]. +/// +/// A [`Program`] can mutate internal state and produce messages for an +/// application. +/// +/// [`Canvas`]: struct.Canvas.html +/// [`Program`]: trait.Program.html +pub trait Program<Message> { + /// Updates the state of the [`Program`]. + /// + /// When a [`Program`] is used in a [`Canvas`], the runtime will call this + /// method for each [`Event`]. + /// + /// This method can optionally return a `Message` to notify an application + /// of any meaningful interactions. + /// + /// By default, this method does and returns nothing. + /// + /// [`Program`]: trait.Program.html + /// [`Canvas`]: struct.Canvas.html + /// [`Event`]: enum.Event.html + fn update( + &mut self, + _event: Event, + _bounds: Rectangle, + _cursor: Cursor, + ) -> Option<Message> { + None + } + + /// Draws the state of the [`Program`], producing a bunch of [`Geometry`]. + /// + /// [`Geometry`] can be easily generated with a [`Frame`] or stored in a + /// [`Cache`]. + /// + /// [`Program`]: trait.Program.html + /// [`Geometry`]: struct.Geometry.html + /// [`Frame`]: struct.Frame.html + /// [`Cache`]: struct.Cache.html + fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec<Geometry>; + + /// Returns the current mouse interaction of the [`Program`]. + /// + /// The interaction returned will be in effect even if the cursor position + /// is out of bounds of the program's [`Canvas`]. + /// + /// [`Program`]: trait.Program.html + /// [`Canvas`]: struct.Canvas.html + fn mouse_interaction( + &self, + _bounds: Rectangle, + _cursor: Cursor, + ) -> mouse::Interaction { + mouse::Interaction::default() + } +} + +impl<T, Message> Program<Message> for &mut T +where + T: Program<Message>, +{ + fn update( + &mut self, + event: Event, + bounds: Rectangle, + cursor: Cursor, + ) -> Option<Message> { + T::update(self, event, bounds, cursor) + } + + fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec<Geometry> { + T::draw(self, bounds, cursor) + } + + fn mouse_interaction( + &self, + bounds: Rectangle, + cursor: Cursor, + ) -> mouse::Interaction { + T::mouse_interaction(self, bounds, cursor) + } +} diff --git a/wgpu/src/widget/canvas/stroke.rs b/graphics/src/widget/canvas/stroke.rs index 46d669c4..5b6fc56a 100644 --- a/wgpu/src/widget/canvas/stroke.rs +++ b/graphics/src/widget/canvas/stroke.rs @@ -14,6 +14,38 @@ pub struct Stroke { pub line_join: LineJoin, } +impl Stroke { + /// Sets the color of the [`Stroke`]. + /// + /// [`Stroke`]: struct.Stroke.html + pub fn with_color(self, color: Color) -> Stroke { + Stroke { color, ..self } + } + + /// Sets the width of the [`Stroke`]. + /// + /// [`Stroke`]: struct.Stroke.html + pub fn with_width(self, width: f32) -> Stroke { + Stroke { width, ..self } + } + + /// Sets the [`LineCap`] of the [`Stroke`]. + /// + /// [`LineCap`]: enum.LineCap.html + /// [`Stroke`]: struct.Stroke.html + pub fn with_line_cap(self, line_cap: LineCap) -> Stroke { + Stroke { line_cap, ..self } + } + + /// Sets the [`LineJoin`] of the [`Stroke`]. + /// + /// [`LineJoin`]: enum.LineJoin.html + /// [`Stroke`]: struct.Stroke.html + pub fn with_line_join(self, line_join: LineJoin) -> Stroke { + Stroke { line_join, ..self } + } +} + impl Default for Stroke { fn default() -> Stroke { Stroke { diff --git a/wgpu/src/widget/canvas/text.rs b/graphics/src/widget/canvas/text.rs index c4cae30e..c4cae30e 100644 --- a/wgpu/src/widget/canvas/text.rs +++ b/graphics/src/widget/canvas/text.rs diff --git a/wgpu/src/renderer/widget/checkbox.rs b/graphics/src/widget/checkbox.rs index c0f1bf21..cb7fd2cf 100644 --- a/wgpu/src/renderer/widget/checkbox.rs +++ b/graphics/src/widget/checkbox.rs @@ -1,9 +1,22 @@ -use crate::{checkbox::StyleSheet, Primitive, Renderer}; -use iced_native::{ - checkbox, HorizontalAlignment, MouseCursor, Rectangle, VerticalAlignment, -}; +//! Show toggle controls using checkboxes. +use crate::backend::{self, Backend}; +use crate::{Primitive, Renderer}; +use iced_native::checkbox; +use iced_native::mouse; +use iced_native::{HorizontalAlignment, Rectangle, VerticalAlignment}; -impl checkbox::Renderer for Renderer { +pub use iced_style::checkbox::{Style, StyleSheet}; + +/// A box that can be checked. +/// +/// This is an alias of an `iced_native` checkbox with an `iced_wgpu::Renderer`. +pub type Checkbox<Message, Backend> = + iced_native::Checkbox<Message, Renderer<Backend>>; + +impl<B> checkbox::Renderer for Renderer<B> +where + B: Backend + backend::Text, +{ type Style = Box<dyn StyleSheet>; const DEFAULT_SIZE: u16 = 20; @@ -35,8 +48,8 @@ impl checkbox::Renderer for Renderer { Primitive::Group { primitives: if is_checked { let check = Primitive::Text { - content: crate::text::CHECKMARK_ICON.to_string(), - font: crate::text::BUILTIN_ICONS, + content: B::CHECKMARK_ICON.to_string(), + font: B::ICON_FONT, size: bounds.height * 0.7, bounds: Rectangle { x: bounds.center_x(), @@ -54,9 +67,9 @@ impl checkbox::Renderer for Renderer { }, }, if is_mouse_over { - MouseCursor::Pointer + mouse::Interaction::Pointer } else { - MouseCursor::OutOfBounds + mouse::Interaction::default() }, ) } diff --git a/graphics/src/widget/column.rs b/graphics/src/widget/column.rs new file mode 100644 index 00000000..6c7235c7 --- /dev/null +++ b/graphics/src/widget/column.rs @@ -0,0 +1,43 @@ +use crate::{Backend, Primitive, Renderer}; +use iced_native::column; +use iced_native::mouse; +use iced_native::{Element, Layout, Point}; + +/// A container that distributes its contents vertically. +pub type Column<'a, Message, Backend> = + iced_native::Column<'a, Message, Renderer<Backend>>; + +impl<B> column::Renderer for Renderer<B> +where + B: Backend, +{ + fn draw<Message>( + &mut self, + defaults: &Self::Defaults, + content: &[Element<'_, Message, Self>], + layout: Layout<'_>, + cursor_position: Point, + ) -> Self::Output { + let mut mouse_interaction = mouse::Interaction::default(); + + ( + Primitive::Group { + primitives: content + .iter() + .zip(layout.children()) + .map(|(child, layout)| { + let (primitive, new_mouse_interaction) = + child.draw(self, defaults, layout, cursor_position); + + if new_mouse_interaction > mouse_interaction { + mouse_interaction = new_mouse_interaction; + } + + primitive + }) + .collect(), + }, + mouse_interaction, + ) + } +} diff --git a/wgpu/src/renderer/widget/container.rs b/graphics/src/widget/container.rs index dda7dc8a..070cb48b 100644 --- a/wgpu/src/renderer/widget/container.rs +++ b/graphics/src/widget/container.rs @@ -1,7 +1,22 @@ -use crate::{container, defaults, Defaults, Primitive, Renderer}; +//! Decorate content and apply alignment. +use crate::container; +use crate::defaults::{self, Defaults}; +use crate::{Backend, Primitive, Renderer}; use iced_native::{Background, Color, Element, Layout, Point, Rectangle}; -impl iced_native::container::Renderer for Renderer { +pub use iced_style::container::{Style, StyleSheet}; + +/// An element decorating some content. +/// +/// This is an alias of an `iced_native` container with a default +/// `Renderer`. +pub type Container<'a, Message, Backend> = + iced_native::Container<'a, Message, Renderer<Backend>>; + +impl<B> iced_native::container::Renderer for Renderer<B> +where + B: Backend, +{ type Style = Box<dyn container::StyleSheet>; fn draw<Message>( @@ -21,7 +36,7 @@ impl iced_native::container::Renderer for Renderer { }, }; - let (content, mouse_cursor) = + let (content, mouse_interaction) = content.draw(self, &defaults, content_layout, cursor_position); if style.background.is_some() || style.border_width > 0 { @@ -39,10 +54,10 @@ impl iced_native::container::Renderer for Renderer { Primitive::Group { primitives: vec![quad, content], }, - mouse_cursor, + mouse_interaction, ) } else { - (content, mouse_cursor) + (content, mouse_interaction) } } } diff --git a/graphics/src/widget/image.rs b/graphics/src/widget/image.rs new file mode 100644 index 00000000..30f446e8 --- /dev/null +++ b/graphics/src/widget/image.rs @@ -0,0 +1,31 @@ +//! Display images in your user interface. +use crate::backend::{self, Backend}; +use crate::{Primitive, Renderer}; +use iced_native::image; +use iced_native::mouse; +use iced_native::Layout; + +pub use iced_native::image::{Handle, Image}; + +impl<B> image::Renderer for Renderer<B> +where + B: Backend + backend::Image, +{ + fn dimensions(&self, handle: &image::Handle) -> (u32, u32) { + self.backend().dimensions(handle) + } + + fn draw( + &mut self, + handle: image::Handle, + layout: Layout<'_>, + ) -> Self::Output { + ( + Primitive::Image { + handle, + bounds: layout.bounds(), + }, + mouse::Interaction::default(), + ) + } +} diff --git a/wgpu/src/renderer/widget/pane_grid.rs b/graphics/src/widget/pane_grid.rs index 2d201fec..56af683d 100644 --- a/wgpu/src/renderer/widget/pane_grid.rs +++ b/graphics/src/widget/pane_grid.rs @@ -1,10 +1,36 @@ -use crate::{Primitive, Renderer}; -use iced_native::{ - pane_grid::{self, Axis, Pane}, - Element, Layout, MouseCursor, Point, Rectangle, Vector, +//! Let your users split regions of your application and organize layout dynamically. +//! +//! [](https://gfycat.com/mixedflatjellyfish) +//! +//! # Example +//! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing, +//! drag and drop, and hotkey support. +//! +//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.1/examples/pane_grid +//! [`PaneGrid`]: type.PaneGrid.html +use crate::{Backend, Primitive, Renderer}; +use iced_native::mouse; +use iced_native::pane_grid; +use iced_native::{Element, Layout, Point, Rectangle, Vector}; + +pub use iced_native::pane_grid::{ + Axis, Direction, DragEvent, Focus, KeyPressEvent, Pane, ResizeEvent, Split, + State, }; -impl pane_grid::Renderer for Renderer { +/// A collection of panes distributed using either vertical or horizontal splits +/// to completely fill the space available. +/// +/// [](https://gfycat.com/mixedflatjellyfish) +/// +/// This is an alias of an `iced_native` pane grid with an `iced_wgpu::Renderer`. +pub type PaneGrid<'a, Message, Backend> = + iced_native::PaneGrid<'a, Message, Renderer<Backend>>; + +impl<B> pane_grid::Renderer for Renderer<B> +where + B: Backend, +{ fn draw<Message>( &mut self, defaults: &Self::Defaults, @@ -22,7 +48,7 @@ impl pane_grid::Renderer for Renderer { cursor_position }; - let mut mouse_cursor = MouseCursor::OutOfBounds; + let mut mouse_interaction = mouse::Interaction::default(); let mut dragged_pane = None; let mut panes: Vec<_> = content @@ -30,11 +56,11 @@ impl pane_grid::Renderer for Renderer { .zip(layout.children()) .enumerate() .map(|(i, ((id, pane), layout))| { - let (primitive, new_mouse_cursor) = + let (primitive, new_mouse_interaction) = pane.draw(self, defaults, layout, pane_cursor_position); - if new_mouse_cursor > mouse_cursor { - mouse_cursor = new_mouse_cursor; + if new_mouse_interaction > mouse_interaction { + mouse_interaction = new_mouse_interaction; } if Some(*id) == dragging { @@ -59,12 +85,12 @@ impl pane_grid::Renderer for Renderer { height: bounds.height + 0.5, }, offset: Vector::new(0, 0), - content: Box::new(Primitive::Cached { - origin: Point::new( + content: Box::new(Primitive::Translate { + translation: Vector::new( cursor_position.x - bounds.x - bounds.width / 2.0, cursor_position.y - bounds.y - bounds.height / 2.0, ), - cache: std::sync::Arc::new(pane), + content: Box::new(pane), }), }; @@ -78,14 +104,14 @@ impl pane_grid::Renderer for Renderer { ( Primitive::Group { primitives }, if dragging.is_some() { - MouseCursor::Grabbing + mouse::Interaction::Grabbing } else if let Some(axis) = resizing { match axis { - Axis::Horizontal => MouseCursor::ResizingVertically, - Axis::Vertical => MouseCursor::ResizingHorizontally, + Axis::Horizontal => mouse::Interaction::ResizingVertically, + Axis::Vertical => mouse::Interaction::ResizingHorizontally, } } else { - mouse_cursor + mouse_interaction }, ) } diff --git a/wgpu/src/renderer/widget/progress_bar.rs b/graphics/src/widget/progress_bar.rs index 34e33276..48acb3c1 100644 --- a/wgpu/src/renderer/widget/progress_bar.rs +++ b/graphics/src/widget/progress_bar.rs @@ -1,7 +1,26 @@ -use crate::{progress_bar::StyleSheet, Primitive, Renderer}; -use iced_native::{progress_bar, Color, MouseCursor, Rectangle}; +//! Allow your users to visually track the progress of a computation. +//! +//! A [`ProgressBar`] has a range of possible values and a current value, +//! as well as a length, height and style. +//! +//! [`ProgressBar`]: type.ProgressBar.html +use crate::{Backend, Primitive, Renderer}; +use iced_native::mouse; +use iced_native::progress_bar; +use iced_native::{Color, Rectangle}; -impl progress_bar::Renderer for Renderer { +pub use iced_style::progress_bar::{Style, StyleSheet}; + +/// A bar that displays progress. +/// +/// This is an alias of an `iced_native` progress bar with an +/// `iced_wgpu::Renderer`. +pub type ProgressBar<Backend> = iced_native::ProgressBar<Renderer<Backend>>; + +impl<B> progress_bar::Renderer for Renderer<B> +where + B: Backend, +{ type Style = Box<dyn StyleSheet>; const DEFAULT_HEIGHT: u16 = 30; @@ -48,7 +67,7 @@ impl progress_bar::Renderer for Renderer { } else { background }, - MouseCursor::OutOfBounds, + mouse::Interaction::default(), ) } } diff --git a/wgpu/src/renderer/widget/radio.rs b/graphics/src/widget/radio.rs index 564f066b..dd8b5f17 100644 --- a/wgpu/src/renderer/widget/radio.rs +++ b/graphics/src/widget/radio.rs @@ -1,15 +1,29 @@ -use crate::{radio::StyleSheet, Primitive, Renderer}; -use iced_native::{radio, Background, Color, MouseCursor, Rectangle}; +//! Create choices using radio buttons. +use crate::{Backend, Primitive, Renderer}; +use iced_native::mouse; +use iced_native::radio; +use iced_native::{Background, Color, Rectangle}; + +pub use iced_style::radio::{Style, StyleSheet}; + +/// A circular button representing a choice. +/// +/// This is an alias of an `iced_native` radio button with an +/// `iced_wgpu::Renderer`. +pub type Radio<Message, Backend> = + iced_native::Radio<Message, Renderer<Backend>>; const SIZE: f32 = 28.0; const DOT_SIZE: f32 = SIZE / 2.0; -impl radio::Renderer for Renderer { +impl<B> radio::Renderer for Renderer<B> +where + B: Backend, +{ type Style = Box<dyn StyleSheet>; - fn default_size(&self) -> u32 { - SIZE as u32 - } + const DEFAULT_SIZE: u16 = SIZE as u16; + const DEFAULT_SPACING: u16 = 15; fn draw( &mut self, @@ -55,9 +69,9 @@ impl radio::Renderer for Renderer { }, }, if is_mouse_over { - MouseCursor::Pointer + mouse::Interaction::Pointer } else { - MouseCursor::OutOfBounds + mouse::Interaction::default() }, ) } diff --git a/graphics/src/widget/row.rs b/graphics/src/widget/row.rs new file mode 100644 index 00000000..4c1dbadc --- /dev/null +++ b/graphics/src/widget/row.rs @@ -0,0 +1,43 @@ +use crate::{Backend, Primitive, Renderer}; +use iced_native::mouse; +use iced_native::row; +use iced_native::{Element, Layout, Point}; + +/// A container that distributes its contents horizontally. +pub type Row<'a, Message, Backend> = + iced_native::Row<'a, Message, Renderer<Backend>>; + +impl<B> row::Renderer for Renderer<B> +where + B: Backend, +{ + fn draw<Message>( + &mut self, + defaults: &Self::Defaults, + content: &[Element<'_, Message, Self>], + layout: Layout<'_>, + cursor_position: Point, + ) -> Self::Output { + let mut mouse_interaction = mouse::Interaction::default(); + + ( + Primitive::Group { + primitives: content + .iter() + .zip(layout.children()) + .map(|(child, layout)| { + let (primitive, new_mouse_interaction) = + child.draw(self, defaults, layout, cursor_position); + + if new_mouse_interaction > mouse_interaction { + mouse_interaction = new_mouse_interaction; + } + + primitive + }) + .collect(), + }, + mouse_interaction, + ) + } +} diff --git a/wgpu/src/renderer/widget/scrollable.rs b/graphics/src/widget/scrollable.rs index 732523e3..b149db0a 100644 --- a/wgpu/src/renderer/widget/scrollable.rs +++ b/graphics/src/widget/scrollable.rs @@ -1,12 +1,27 @@ -use crate::{Primitive, Renderer}; -use iced_native::{ - scrollable, Background, Color, MouseCursor, Rectangle, Vector, -}; +//! Navigate an endless amount of content with a scrollbar. +use crate::{Backend, Primitive, Renderer}; +use iced_native::mouse; +use iced_native::scrollable; +use iced_native::{Background, Color, Rectangle, Vector}; + +pub use iced_native::scrollable::State; +pub use iced_style::scrollable::{Scrollbar, Scroller, StyleSheet}; + +/// A widget that can vertically display an infinite amount of content +/// with a scrollbar. +/// +/// This is an alias of an `iced_native` scrollable with a default +/// `Renderer`. +pub type Scrollable<'a, Message, Backend> = + iced_native::Scrollable<'a, Message, Renderer<Backend>>; const SCROLLBAR_WIDTH: u16 = 10; const SCROLLBAR_MARGIN: u16 = 2; -impl scrollable::Renderer for Renderer { +impl<B> scrollable::Renderer for Renderer<B> +where + B: Backend, +{ type Style = Box<dyn iced_style::scrollable::StyleSheet>; fn scrollbar( @@ -56,7 +71,7 @@ impl scrollable::Renderer for Renderer { scrollbar: Option<scrollable::Scrollbar>, offset: u32, style_sheet: &Self::Style, - (content, mouse_cursor): Self::Output, + (content, mouse_interaction): Self::Output, ) -> Self::Output { ( if let Some(scrollbar) = scrollbar { @@ -118,9 +133,9 @@ impl scrollable::Renderer for Renderer { content }, if is_mouse_over_scrollbar || state.is_scroller_grabbed() { - MouseCursor::Idle + mouse::Interaction::Idle } else { - mouse_cursor + mouse_interaction }, ) } diff --git a/wgpu/src/renderer/widget/slider.rs b/graphics/src/widget/slider.rs index c8ebd0da..da8b5a86 100644 --- a/wgpu/src/renderer/widget/slider.rs +++ b/graphics/src/widget/slider.rs @@ -1,12 +1,30 @@ -use crate::{ - slider::{HandleShape, StyleSheet}, - Primitive, Renderer, -}; -use iced_native::{slider, Background, Color, MouseCursor, Point, Rectangle}; +//! Display an interactive selector of a single value from a range of values. +//! +//! A [`Slider`] has some local [`State`]. +//! +//! [`Slider`]: struct.Slider.html +//! [`State`]: struct.State.html +use crate::{Backend, Primitive, Renderer}; +use iced_native::mouse; +use iced_native::slider; +use iced_native::{Background, Color, Point, Rectangle}; + +pub use iced_native::slider::State; +pub use iced_style::slider::{Handle, HandleShape, Style, StyleSheet}; + +/// An horizontal bar and a handle that selects a single value from a range of +/// values. +/// +/// This is an alias of an `iced_native` slider with an `iced_wgpu::Renderer`. +pub type Slider<'a, T, Message, Backend> = + iced_native::Slider<'a, T, Message, Renderer<Backend>>; const HANDLE_HEIGHT: f32 = 22.0; -impl slider::Renderer for Renderer { +impl<B> slider::Renderer for Renderer<B> +where + B: Backend, +{ type Style = Box<dyn StyleSheet>; fn height(&self) -> u32 { @@ -95,11 +113,11 @@ impl slider::Renderer for Renderer { primitives: vec![rail_top, rail_bottom, handle], }, if is_dragging { - MouseCursor::Grabbing + mouse::Interaction::Grabbing } else if is_mouse_over { - MouseCursor::Grab + mouse::Interaction::Grab } else { - MouseCursor::OutOfBounds + mouse::Interaction::default() }, ) } diff --git a/graphics/src/widget/space.rs b/graphics/src/widget/space.rs new file mode 100644 index 00000000..1f31eabe --- /dev/null +++ b/graphics/src/widget/space.rs @@ -0,0 +1,15 @@ +use crate::{Backend, Primitive, Renderer}; +use iced_native::mouse; +use iced_native::space; +use iced_native::Rectangle; + +pub use iced_native::Space; + +impl<B> space::Renderer for Renderer<B> +where + B: Backend, +{ + fn draw(&mut self, _bounds: Rectangle) -> Self::Output { + (Primitive::None, mouse::Interaction::default()) + } +} diff --git a/wgpu/src/renderer/widget/svg.rs b/graphics/src/widget/svg.rs index 67bc3fe1..8b5ed66a 100644 --- a/wgpu/src/renderer/widget/svg.rs +++ b/graphics/src/widget/svg.rs @@ -1,9 +1,16 @@ +//! Display vector graphics in your application. +use crate::backend::{self, Backend}; use crate::{Primitive, Renderer}; -use iced_native::{svg, Layout, MouseCursor}; +use iced_native::{mouse, svg, Layout}; -impl svg::Renderer for Renderer { +pub use iced_native::svg::{Handle, Svg}; + +impl<B> svg::Renderer for Renderer<B> +where + B: Backend + backend::Svg, +{ fn dimensions(&self, handle: &svg::Handle) -> (u32, u32) { - self.image_pipeline.viewport_dimensions(handle) + self.backend().viewport_dimensions(handle) } fn draw( @@ -16,7 +23,7 @@ impl svg::Renderer for Renderer { handle, bounds: layout.bounds(), }, - MouseCursor::OutOfBounds, + mouse::Interaction::default(), ) } } diff --git a/wgpu/src/renderer/widget/text.rs b/graphics/src/widget/text.rs index 80bff574..7e22e680 100644 --- a/wgpu/src/renderer/widget/text.rs +++ b/graphics/src/widget/text.rs @@ -1,13 +1,28 @@ +//! Write some text for your users to read. +use crate::backend::{self, Backend}; use crate::{Primitive, Renderer}; +use iced_native::mouse; +use iced_native::text; use iced_native::{ - text, Color, Font, HorizontalAlignment, MouseCursor, Rectangle, Size, - VerticalAlignment, + Color, Font, HorizontalAlignment, Rectangle, Size, VerticalAlignment, }; +/// A paragraph of text. +/// +/// This is an alias of an `iced_native` text with an `iced_wgpu::Renderer`. +pub type Text<Backend> = iced_native::Text<Renderer<Backend>>; + use std::f32; -impl text::Renderer for Renderer { - const DEFAULT_SIZE: u16 = 20; +impl<B> text::Renderer for Renderer<B> +where + B: Backend + backend::Text, +{ + type Font = Font; + + fn default_size(&self) -> u16 { + self.backend().default_size() + } fn measure( &self, @@ -16,7 +31,7 @@ impl text::Renderer for Renderer { font: Font, bounds: Size, ) -> (f32, f32) { - self.text_pipeline + self.backend() .measure(content, f32::from(size), font, bounds) } @@ -53,7 +68,7 @@ impl text::Renderer for Renderer { horizontal_alignment, vertical_alignment, }, - MouseCursor::OutOfBounds, + mouse::Interaction::default(), ) } } diff --git a/wgpu/src/renderer/widget/text_input.rs b/graphics/src/widget/text_input.rs index 6f72db68..f13f6606 100644 --- a/wgpu/src/renderer/widget/text_input.rs +++ b/graphics/src/widget/text_input.rs @@ -1,13 +1,33 @@ -use crate::{text_input::StyleSheet, Primitive, Renderer}; - +//! Display fields that can be filled with text. +//! +//! A [`TextInput`] has some local [`State`]. +//! +//! [`TextInput`]: struct.TextInput.html +//! [`State`]: struct.State.html +use crate::backend::{self, Backend}; +use crate::{Primitive, Renderer}; +use iced_native::mouse; +use iced_native::text_input::{self, cursor}; use iced_native::{ - text_input::{self, cursor}, - Background, Color, Font, HorizontalAlignment, MouseCursor, Point, - Rectangle, Size, Vector, VerticalAlignment, + Background, Color, Font, HorizontalAlignment, Point, Rectangle, Size, + Vector, VerticalAlignment, }; use std::f32; -impl text_input::Renderer for Renderer { +pub use iced_native::text_input::State; +pub use iced_style::text_input::{Style, StyleSheet}; + +/// A field that can be filled with text. +/// +/// This is an alias of an `iced_native` text input with an `iced_wgpu::Renderer`. +pub type TextInput<'a, Message, Backend> = + iced_native::TextInput<'a, Message, Renderer<Backend>>; + +impl<B> text_input::Renderer for Renderer<B> +where + B: Backend + backend::Text, +{ + type Font = Font; type Style = Box<dyn StyleSheet>; fn default_size(&self) -> u16 { @@ -16,19 +36,10 @@ impl text_input::Renderer for Renderer { } fn measure_value(&self, value: &str, size: u16, font: Font) -> f32 { - let (mut width, _) = self.text_pipeline.measure( - value, - f32::from(size), - font, - Size::INFINITY, - ); - - let spaces_around = value.len() - value.trim().len(); + let backend = self.backend(); - if spaces_around > 0 { - let space_width = self.text_pipeline.space_width(size as f32); - width += spaces_around as f32 * space_width; - } + let (width, _) = + backend.measure(value, f32::from(size), font, Size::INFINITY); width } @@ -232,22 +243,25 @@ impl text_input::Renderer for Renderer { primitives: vec![input, contents], }, if is_mouse_over { - MouseCursor::Text + mouse::Interaction::Text } else { - MouseCursor::OutOfBounds + mouse::Interaction::default() }, ) } } -fn measure_cursor_and_scroll_offset( - renderer: &Renderer, +fn measure_cursor_and_scroll_offset<B>( + renderer: &Renderer<B>, text_bounds: Rectangle, value: &text_input::Value, size: u16, cursor_index: usize, font: Font, -) -> (f32, f32) { +) -> (f32, f32) +where + B: Backend + backend::Text, +{ use iced_native::text_input::Renderer; let text_before_cursor = value.until(cursor_index).to_string(); diff --git a/graphics/src/window.rs b/graphics/src/window.rs new file mode 100644 index 00000000..3e74db5f --- /dev/null +++ b/graphics/src/window.rs @@ -0,0 +1,10 @@ +//! Draw graphics to window surfaces. +mod compositor; + +#[cfg(feature = "opengl")] +mod gl_compositor; + +pub use compositor::Compositor; + +#[cfg(feature = "opengl")] +pub use gl_compositor::GLCompositor; diff --git a/native/src/window/backend.rs b/graphics/src/window/compositor.rs index 3bc691cd..aa625f43 100644 --- a/native/src/window/backend.rs +++ b/graphics/src/window/compositor.rs @@ -1,14 +1,14 @@ -use crate::MouseCursor; - +use crate::{Color, Viewport}; +use iced_native::mouse; use raw_window_handle::HasRawWindowHandle; -/// A graphics backend that can render to windows. -pub trait Backend: Sized { +/// A graphics compositor that can draw to windows. +pub trait Compositor: Sized { /// The settings of the backend. type Settings: Default; /// The iced renderer of the backend. - type Renderer: crate::Renderer; + type Renderer: iced_native::Renderer; /// The surface of the backend. type Surface; @@ -16,7 +16,7 @@ pub trait Backend: Sized { /// The swap chain of the backend. type SwapChain; - /// Creates a new [`Backend`] and an associated iced renderer. + /// Creates a new [`Backend`]. /// /// [`Backend`]: trait.Backend.html fn new(settings: Self::Settings) -> (Self, Self::Renderer); @@ -48,8 +48,9 @@ pub trait Backend: Sized { &mut self, renderer: &mut Self::Renderer, swap_chain: &mut Self::SwapChain, - output: &<Self::Renderer as crate::Renderer>::Output, - scale_factor: f64, + viewport: &Viewport, + background_color: Color, + output: &<Self::Renderer as iced_native::Renderer>::Output, overlay: &[T], - ) -> MouseCursor; + ) -> mouse::Interaction; } diff --git a/graphics/src/window/gl_compositor.rs b/graphics/src/window/gl_compositor.rs new file mode 100644 index 00000000..2ba39d6e --- /dev/null +++ b/graphics/src/window/gl_compositor.rs @@ -0,0 +1,68 @@ +use crate::{Color, Size, Viewport}; +use iced_native::mouse; + +use core::ffi::c_void; + +/// A basic OpenGL compositor. +/// +/// A compositor is responsible for initializing a renderer and managing window +/// surfaces. +/// +/// For now, this compositor only deals with a single global surface +/// for drawing. However, the trait will most likely change in the near future +/// to handle multiple surfaces at once. +/// +/// If you implement an OpenGL renderer, you can implement this trait to ease +/// integration with existing windowing shells, like `iced_glutin`. +pub trait GLCompositor: Sized { + /// The renderer of the [`Compositor`]. + /// + /// This should point to your renderer type, which could be a type alias + /// of the [`Renderer`] provided in this crate with with a specific + /// [`Backend`]. + /// + /// [`Compositor`]: trait.Compositor.html + /// [`Renderer`]: ../struct.Renderer.html + /// [`Backend`]: ../backend/trait.Backend.html + type Renderer: iced_native::Renderer; + + /// The settings of the [`Compositor`]. + /// + /// It's up to you to decide the configuration supported by your renderer! + type Settings: Default; + + /// Creates a new [`Compositor`] and [`Renderer`] with the given + /// [`Settings`] and an OpenGL address loader function. + /// + /// [`Compositor`]: trait.Compositor.html + /// [`Renderer`]: #associatedtype.Renderer + /// [`Backend`]: ../backend/trait.Backend.html + #[allow(unsafe_code)] + unsafe fn new( + settings: Self::Settings, + loader_function: impl FnMut(&str) -> *const c_void, + ) -> (Self, Self::Renderer); + + /// Returns the amount of samples that should be used when configuring + /// an OpenGL context for this [`Compositor`]. + /// + /// [`Compositor`]: trait.Compositor.html + fn sample_count(settings: &Self::Settings) -> u32; + + /// Resizes the viewport of the [`Compositor`]. + /// + /// [`Compositor`]: trait.Compositor.html + fn resize_viewport(&mut self, physical_size: Size<u32>); + + /// Draws the provided output with the given [`Renderer`]. + /// + /// [`Compositor`]: trait.Compositor.html + fn draw<T: AsRef<str>>( + &mut self, + renderer: &mut Self::Renderer, + viewport: &Viewport, + background_color: Color, + output: &<Self::Renderer as iced_native::Renderer>::Output, + overlay: &[T], + ) -> mouse::Interaction; +} diff --git a/native/Cargo.toml b/native/Cargo.toml index ca58d75c..13052a93 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -1,16 +1,19 @@ [package] name = "iced_native" -version = "0.2.1" +version = "0.2.2" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2018" description = "A renderer-agnostic library for native GUIs" license = "MIT" repository = "https://github.com/hecrj/iced" +[features] +debug = [] + [dependencies] twox-hash = "1.5" -raw-window-handle = "0.3" unicode-segmentation = "1.6" +num-traits = "0.2" [dependencies.iced_core] version = "0.2" diff --git a/winit/src/debug/basic.rs b/native/src/debug/basic.rs index d46edba6..5338d0d9 100644 --- a/winit/src/debug/basic.rs +++ b/native/src/debug/basic.rs @@ -1,5 +1,7 @@ +#![allow(missing_docs)] use std::{collections::VecDeque, time}; +/// A bunch of time measurements for debugging purposes. #[derive(Debug)] pub struct Debug { is_enabled: bool, @@ -30,6 +32,9 @@ pub struct Debug { } impl Debug { + /// Creates a new [`Debug`]. + /// + /// [`Debug`]: struct.Debug.html pub fn new() -> Self { let now = time::Instant::now(); diff --git a/winit/src/debug/null.rs b/native/src/debug/null.rs index 2a9430cd..60e6122d 100644 --- a/winit/src/debug/null.rs +++ b/native/src/debug/null.rs @@ -1,3 +1,4 @@ +#![allow(missing_docs)] #[derive(Debug)] pub struct Debug; diff --git a/native/src/element.rs b/native/src/element.rs index f29580fc..73e39012 100644 --- a/native/src/element.rs +++ b/native/src/element.rs @@ -81,7 +81,7 @@ where /// /// ``` /// # mod counter { - /// # use iced_native::{text, Text}; + /// # type Text = iced_native::Text<iced_native::renderer::Null>; /// # /// # #[derive(Debug, Clone, Copy)] /// # pub enum Message {} diff --git a/native/src/event.rs b/native/src/event.rs index b2550ead..606a71d6 100644 --- a/native/src/event.rs +++ b/native/src/event.rs @@ -1,7 +1,4 @@ -use crate::{ - input::{keyboard, mouse}, - window, -}; +use crate::{keyboard, mouse, window}; /// A user interface event. /// diff --git a/native/src/input.rs b/native/src/input.rs deleted file mode 100644 index 097fa730..00000000 --- a/native/src/input.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! Map your system events into input events that the runtime can understand. -pub mod keyboard; -pub mod mouse; - -mod button_state; - -pub use button_state::ButtonState; diff --git a/native/src/input/button_state.rs b/native/src/input/button_state.rs deleted file mode 100644 index 988043ba..00000000 --- a/native/src/input/button_state.rs +++ /dev/null @@ -1,9 +0,0 @@ -/// The state of a button. -#[derive(Debug, Hash, Ord, PartialOrd, PartialEq, Eq, Clone, Copy)] -pub enum ButtonState { - /// The button is pressed. - Pressed, - - /// The button is __not__ pressed. - Released, -} diff --git a/native/src/input/keyboard.rs b/native/src/input/keyboard.rs deleted file mode 100644 index 928bf492..00000000 --- a/native/src/input/keyboard.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Build keyboard events. -mod event; - -pub use event::Event; -pub use iced_core::keyboard::{KeyCode, ModifiersState}; diff --git a/native/src/keyboard.rs b/native/src/keyboard.rs new file mode 100644 index 00000000..012538e3 --- /dev/null +++ b/native/src/keyboard.rs @@ -0,0 +1,2 @@ +//! Track keyboard events. +pub use iced_core::keyboard::*; diff --git a/native/src/lib.rs b/native/src/lib.rs index 89612391..b67ff2a1 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -9,14 +9,11 @@ //! - Event handling for all the built-in widgets //! - A renderer-agnostic API //! -//! To achieve this, it introduces a bunch of reusable interfaces: +//! To achieve this, it introduces a couple of reusable interfaces: //! //! - A [`Widget`] trait, which is used to implement new widgets: from layout //! requirements to event and drawing logic. //! - A bunch of `Renderer` traits, meant to keep the crate renderer-agnostic. -//! - A [`window::Backend`] trait, leveraging [`raw-window-handle`], which can be -//! implemented by graphical renderers that target _windows_. Window-based -//! shells (like [`iced_winit`]) can use this trait to stay renderer-agnostic. //! //! # Usage //! The strategy to use this crate depends on your particular use case. If you @@ -31,7 +28,6 @@ //! [`druid`]: https://github.com/xi-editor/druid //! [`raw-window-handle`]: https://github.com/rust-windowing/raw-window-handle //! [`Widget`]: widget/trait.Widget.html -//! [`window::Backend`]: window/trait.Backend.html //! [`UserInterface`]: struct.UserInterface.html //! [renderer]: renderer/index.html #![deny(missing_docs)] @@ -39,8 +35,10 @@ #![deny(unused_results)] #![forbid(unsafe_code)] #![forbid(rust_2018_idioms)] -pub mod input; +pub mod keyboard; pub mod layout; +pub mod mouse; +pub mod program; pub mod renderer; pub mod subscription; pub mod widget; @@ -50,10 +48,18 @@ mod clipboard; mod element; mod event; mod hasher; -mod mouse_cursor; mod runtime; mod user_interface; +// We disable debug capabilities on release builds unless the `debug` feature +// is explicitly enabled. +#[cfg(feature = "debug")] +#[path = "debug/basic.rs"] +mod debug; +#[cfg(not(feature = "debug"))] +#[path = "debug/null.rs"] +mod debug; + pub use iced_core::{ Align, Background, Color, Font, HorizontalAlignment, Length, Point, Rectangle, Size, Vector, VerticalAlignment, @@ -64,11 +70,12 @@ pub use iced_futures::{executor, futures, Command}; pub use executor::Executor; pub use clipboard::Clipboard; +pub use debug::Debug; pub use element::Element; pub use event::Event; pub use hasher::Hasher; pub use layout::Layout; -pub use mouse_cursor::MouseCursor; +pub use program::Program; pub use renderer::Renderer; pub use runtime::Runtime; pub use subscription::Subscription; diff --git a/native/src/mouse.rs b/native/src/mouse.rs new file mode 100644 index 00000000..9ee406cf --- /dev/null +++ b/native/src/mouse.rs @@ -0,0 +1,6 @@ +//! Track mouse events. + +pub mod click; + +pub use click::Click; +pub use iced_core::mouse::*; diff --git a/native/src/input/mouse/click.rs b/native/src/mouse/click.rs index d27bc67e..d27bc67e 100644 --- a/native/src/input/mouse/click.rs +++ b/native/src/mouse/click.rs diff --git a/native/src/mouse_cursor.rs b/native/src/mouse_cursor.rs deleted file mode 100644 index 0dad3edc..00000000 --- a/native/src/mouse_cursor.rs +++ /dev/null @@ -1,36 +0,0 @@ -/// The state of the mouse cursor. -#[derive(Debug, Eq, PartialEq, Clone, Copy, PartialOrd, Ord)] -pub enum MouseCursor { - /// The cursor is out of the bounds of the user interface. - OutOfBounds, - - /// The cursor is over a non-interactive widget. - Idle, - - /// The cursor is over a clickable widget. - Pointer, - - /// The cursor is over a busy widget. - Working, - - /// The cursor is over a grabbable widget. - Grab, - - /// The cursor is grabbing a widget. - Grabbing, - - /// The cursor is over a text widget. - Text, - - /// The cursor is resizing a widget horizontally. - ResizingHorizontally, - - /// The cursor is resizing a widget vertically. - ResizingVertically, -} - -impl Default for MouseCursor { - fn default() -> MouseCursor { - MouseCursor::OutOfBounds - } -} diff --git a/native/src/program.rs b/native/src/program.rs new file mode 100644 index 00000000..14afcd84 --- /dev/null +++ b/native/src/program.rs @@ -0,0 +1,39 @@ +//! Build interactive programs using The Elm Architecture. +use crate::{Command, Element, Renderer}; + +mod state; + +pub use state::State; + +/// The core of a user interface application following The Elm Architecture. +pub trait Program: Sized { + /// The graphics backend to use to draw the [`Program`]. + /// + /// [`Program`]: trait.Program.html + type Renderer: Renderer; + + /// The type of __messages__ your [`Program`] will produce. + /// + /// [`Program`]: trait.Program.html + type Message: std::fmt::Debug + Send; + + /// Handles a __message__ and updates the state of the [`Program`]. + /// + /// This is where you define your __update logic__. All the __messages__, + /// produced by either user interactions or commands, will be handled by + /// this method. + /// + /// Any [`Command`] returned will be executed immediately in the + /// background by shells. + /// + /// [`Program`]: trait.Application.html + /// [`Command`]: struct.Command.html + fn update(&mut self, message: Self::Message) -> Command<Self::Message>; + + /// Returns the widgets to display in the [`Program`]. + /// + /// These widgets can produce __messages__ based on user interaction. + /// + /// [`Program`]: trait.Program.html + fn view(&mut self) -> Element<'_, Self::Message, Self::Renderer>; +} diff --git a/native/src/program/state.rs b/native/src/program/state.rs new file mode 100644 index 00000000..fdc42e8b --- /dev/null +++ b/native/src/program/state.rs @@ -0,0 +1,191 @@ +use crate::{ + Cache, Clipboard, Command, Debug, Event, Point, Program, Renderer, Size, + UserInterface, +}; + +/// The execution state of a [`Program`]. It leverages caching, event +/// processing, and rendering primitive storage. +/// +/// [`Program`]: trait.Program.html +#[allow(missing_debug_implementations)] +pub struct State<P> +where + P: Program + 'static, +{ + program: P, + cache: Option<Cache>, + primitive: <P::Renderer as Renderer>::Output, + queued_events: Vec<Event>, + queued_messages: Vec<P::Message>, +} + +impl<P> State<P> +where + P: Program + 'static, +{ + /// Creates a new [`State`] with the provided [`Program`], initializing its + /// primitive with the given logical bounds and renderer. + /// + /// [`State`]: struct.State.html + /// [`Program`]: trait.Program.html + pub fn new( + mut program: P, + bounds: Size, + cursor_position: Point, + renderer: &mut P::Renderer, + debug: &mut Debug, + ) -> Self { + let user_interface = build_user_interface( + &mut program, + Cache::default(), + renderer, + bounds, + debug, + ); + + debug.draw_started(); + let primitive = user_interface.draw(renderer, cursor_position); + debug.draw_finished(); + + let cache = Some(user_interface.into_cache()); + + State { + program, + cache, + primitive, + queued_events: Vec::new(), + queued_messages: Vec::new(), + } + } + + /// Returns a reference to the [`Program`] of the [`State`]. + /// + /// [`Program`]: trait.Program.html + /// [`State`]: struct.State.html + pub fn program(&self) -> &P { + &self.program + } + + /// Returns a reference to the current rendering primitive of the [`State`]. + /// + /// [`State`]: struct.State.html + pub fn primitive(&self) -> &<P::Renderer as Renderer>::Output { + &self.primitive + } + + /// Queues an event in the [`State`] for processing during an [`update`]. + /// + /// [`State`]: struct.State.html + /// [`update`]: #method.update + pub fn queue_event(&mut self, event: Event) { + self.queued_events.push(event); + } + + /// Queues a message in the [`State`] for processing during an [`update`]. + /// + /// [`State`]: struct.State.html + /// [`update`]: #method.update + pub fn queue_message(&mut self, message: P::Message) { + self.queued_messages.push(message); + } + + /// Returns whether the event queue of the [`State`] is empty or not. + /// + /// [`State`]: struct.State.html + pub fn is_queue_empty(&self) -> bool { + self.queued_events.is_empty() && self.queued_messages.is_empty() + } + + /// Processes all the queued events and messages, rebuilding and redrawing + /// the widgets of the linked [`Program`] if necessary. + /// + /// Returns the [`Command`] obtained from [`Program`] after updating it, + /// only if an update was necessary. + /// + /// [`Program`]: trait.Program.html + pub fn update( + &mut self, + bounds: Size, + cursor_position: Point, + clipboard: Option<&dyn Clipboard>, + renderer: &mut P::Renderer, + debug: &mut Debug, + ) -> Option<Command<P::Message>> { + let mut user_interface = build_user_interface( + &mut self.program, + self.cache.take().unwrap(), + renderer, + bounds, + debug, + ); + + debug.event_processing_started(); + let mut messages = user_interface.update( + self.queued_events.drain(..), + cursor_position, + clipboard, + renderer, + ); + messages.extend(self.queued_messages.drain(..)); + debug.event_processing_finished(); + + if messages.is_empty() { + debug.draw_started(); + self.primitive = user_interface.draw(renderer, cursor_position); + debug.draw_finished(); + + self.cache = Some(user_interface.into_cache()); + + None + } else { + // When there are messages, we are forced to rebuild twice + // for now :^) + let temp_cache = user_interface.into_cache(); + + let commands = + Command::batch(messages.into_iter().map(|message| { + debug.log_message(&message); + + debug.update_started(); + let command = self.program.update(message); + debug.update_finished(); + + command + })); + + let user_interface = build_user_interface( + &mut self.program, + temp_cache, + renderer, + bounds, + debug, + ); + + debug.draw_started(); + self.primitive = user_interface.draw(renderer, cursor_position); + debug.draw_finished(); + + self.cache = Some(user_interface.into_cache()); + + Some(commands) + } + } +} + +fn build_user_interface<'a, P: Program>( + program: &'a mut P, + cache: Cache, + renderer: &mut P::Renderer, + size: Size, + debug: &mut Debug, +) -> UserInterface<'a, P::Message, P::Renderer> { + debug.view_started(); + let view = program.view(); + debug.view_finished(); + + debug.layout_started(); + let user_interface = UserInterface::build(view, size, cache, renderer); + debug.layout_finished(); + + user_interface +} diff --git a/native/src/renderer/null.rs b/native/src/renderer/null.rs index 9033a7da..b8b0b996 100644 --- a/native/src/renderer/null.rs +++ b/native/src/renderer/null.rs @@ -47,7 +47,11 @@ impl row::Renderer for Null { } impl text::Renderer for Null { - const DEFAULT_SIZE: u16 = 20; + type Font = Font; + + fn default_size(&self) -> u16 { + 20 + } fn measure( &self, @@ -101,6 +105,7 @@ impl scrollable::Renderer for Null { } impl text_input::Renderer for Null { + type Font = Font; type Style = (); fn default_size(&self) -> u16 { @@ -159,9 +164,8 @@ impl button::Renderer for Null { impl radio::Renderer for Null { type Style = (); - fn default_size(&self) -> u32 { - 20 - } + const DEFAULT_SIZE: u16 = 20; + const DEFAULT_SPACING: u16 = 15; fn draw( &mut self, diff --git a/native/src/user_interface.rs b/native/src/user_interface.rs index 5d9221e9..b9646043 100644 --- a/native/src/user_interface.rs +++ b/native/src/user_interface.rs @@ -1,6 +1,4 @@ -use crate::{ - input::mouse, layout, Clipboard, Element, Event, Layout, Point, Size, -}; +use crate::{layout, Clipboard, Element, Event, Layout, Point, Size}; use std::hash::Hasher; @@ -25,7 +23,6 @@ pub struct UserInterface<'a, Message, Renderer> { root: Element<'a, Message, Renderer>, layout: layout::Node, bounds: Size, - cursor_position: Point, } impl<'a, Message, Renderer> UserInterface<'a, Message, Renderer> @@ -104,7 +101,9 @@ where hasher.finish() }; - let layout = if hash == cache.hash && bounds == cache.bounds { + let layout_is_cached = hash == cache.hash && bounds == cache.bounds; + + let layout = if layout_is_cached { cache.layout } else { renderer.layout(&root, &layout::Limits::new(Size::ZERO, bounds)) @@ -115,7 +114,6 @@ where root, layout, bounds, - cursor_position: cache.cursor_position, } } @@ -132,7 +130,7 @@ where /// completing [the previous example](#example): /// /// ```no_run - /// use iced_native::{UserInterface, Cache, Size}; + /// use iced_native::{UserInterface, Cache, Size, Point}; /// use iced_wgpu::Renderer; /// /// # mod iced_wgpu { @@ -154,6 +152,7 @@ where /// let mut cache = Cache::new(); /// let mut renderer = Renderer::new(); /// let mut window_size = Size::new(1024.0, 768.0); + /// let mut cursor_position = Point::default(); /// /// // Initialize our event storage /// let mut events = Vec::new(); @@ -169,7 +168,12 @@ where /// ); /// /// // Update the user interface - /// let messages = user_interface.update(events.drain(..), None, &renderer); + /// let messages = user_interface.update( + /// events.drain(..), + /// cursor_position, + /// None, + /// &renderer, + /// ); /// /// cache = user_interface.into_cache(); /// @@ -182,20 +186,17 @@ where pub fn update( &mut self, events: impl IntoIterator<Item = Event>, + cursor_position: Point, clipboard: Option<&dyn Clipboard>, renderer: &Renderer, ) -> Vec<Message> { let mut messages = Vec::new(); for event in events { - if let Event::Mouse(mouse::Event::CursorMoved { x, y }) = event { - self.cursor_position = Point::new(x, y); - } - self.root.widget.on_event( event, Layout::new(&self.layout), - self.cursor_position, + cursor_position, &mut messages, renderer, clipboard, @@ -219,7 +220,7 @@ where /// [completing the last example](#example-1): /// /// ```no_run - /// use iced_native::{UserInterface, Cache, Size}; + /// use iced_native::{UserInterface, Cache, Size, Point}; /// use iced_wgpu::Renderer; /// /// # mod iced_wgpu { @@ -241,6 +242,7 @@ where /// let mut cache = 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 events = Vec::new(); /// /// loop { @@ -253,10 +255,15 @@ where /// &mut renderer, /// ); /// - /// let messages = user_interface.update(events.drain(..), None, &renderer); + /// let messages = user_interface.update( + /// events.drain(..), + /// cursor_position, + /// None, + /// &renderer, + /// ); /// /// // Draw the user interface - /// let mouse_cursor = user_interface.draw(&mut renderer); + /// let mouse_cursor = user_interface.draw(&mut renderer, cursor_position); /// /// cache = user_interface.into_cache(); /// @@ -268,12 +275,16 @@ where /// // Flush rendering operations... /// } /// ``` - pub fn draw(&self, renderer: &mut Renderer) -> Renderer::Output { + pub fn draw( + &self, + renderer: &mut Renderer, + cursor_position: Point, + ) -> Renderer::Output { self.root.widget.draw( renderer, &Renderer::Defaults::default(), Layout::new(&self.layout), - self.cursor_position, + cursor_position, ) } @@ -287,7 +298,6 @@ where hash: self.hash, layout: self.layout, bounds: self.bounds, - cursor_position: self.cursor_position, } } } @@ -300,7 +310,6 @@ pub struct Cache { hash: u64, layout: layout::Node, bounds: Size, - cursor_position: Point, } impl Cache { @@ -316,7 +325,6 @@ impl Cache { hash: 0, layout: layout::Node::new(Size::new(0.0, 0.0)), bounds: Size::ZERO, - cursor_position: Point::new(-1.0, -1.0), } } } @@ -329,7 +337,7 @@ impl Default for Cache { impl PartialEq for Cache { fn eq(&self, other: &Cache) -> bool { - self.hash == other.hash && self.cursor_position == other.cursor_position + self.hash == other.hash } } diff --git a/native/src/widget/button.rs b/native/src/widget/button.rs index 3cf4f780..c932da2b 100644 --- a/native/src/widget/button.rs +++ b/native/src/widget/button.rs @@ -5,8 +5,7 @@ //! [`Button`]: struct.Button.html //! [`State`]: struct.State.html use crate::{ - input::{mouse, ButtonState}, - layout, Clipboard, Element, Event, Hasher, Layout, Length, Point, + layout, mouse, Clipboard, Element, Event, Hasher, Layout, Length, Point, Rectangle, Widget, }; use std::hash::Hash; @@ -185,28 +184,24 @@ where _clipboard: Option<&dyn Clipboard>, ) { match event { - Event::Mouse(mouse::Event::Input { - button: mouse::Button::Left, - state, - }) => { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + if self.on_press.is_some() { + let bounds = layout.bounds(); + + self.state.is_pressed = bounds.contains(cursor_position); + } + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { if let Some(on_press) = self.on_press.clone() { let bounds = layout.bounds(); - match state { - ButtonState::Pressed => { - self.state.is_pressed = - bounds.contains(cursor_position); - } - ButtonState::Released => { - let is_clicked = self.state.is_pressed - && bounds.contains(cursor_position); + let is_clicked = self.state.is_pressed + && bounds.contains(cursor_position); - self.state.is_pressed = false; + self.state.is_pressed = false; - if is_clicked { - messages.push(on_press); - } - } + if is_clicked { + messages.push(on_press); } } } diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs index d611993f..44962288 100644 --- a/native/src/widget/checkbox.rs +++ b/native/src/widget/checkbox.rs @@ -2,8 +2,7 @@ use std::hash::Hash; use crate::{ - input::{mouse, ButtonState}, - layout, row, text, Align, Clipboard, Element, Event, Font, Hasher, + layout, mouse, row, text, Align, Clipboard, Element, Event, Hasher, HorizontalAlignment, Layout, Length, Point, Rectangle, Row, Text, VerticalAlignment, Widget, }; @@ -33,7 +32,7 @@ pub struct Checkbox<Message, Renderer: self::Renderer + text::Renderer> { width: Length, size: u16, spacing: u16, - text_size: u16, + text_size: Option<u16>, style: Renderer::Style, } @@ -61,7 +60,7 @@ impl<Message, Renderer: self::Renderer + text::Renderer> width: Length::Shrink, size: <Renderer as self::Renderer>::DEFAULT_SIZE, spacing: Renderer::DEFAULT_SPACING, - text_size: <Renderer as text::Renderer>::DEFAULT_SIZE, + text_size: None, style: Renderer::Style::default(), } } @@ -94,7 +93,7 @@ impl<Message, Renderer: self::Renderer + text::Renderer> /// /// [`Checkbox`]: struct.Checkbox.html pub fn text_size(mut self, text_size: u16) -> Self { - self.text_size = text_size; + self.text_size = Some(text_size); self } @@ -137,7 +136,7 @@ where .push( Text::new(&self.label) .width(self.width) - .size(self.text_size), + .size(self.text_size.unwrap_or(renderer.default_size())), ) .layout(renderer, limits) } @@ -152,10 +151,7 @@ where _clipboard: Option<&dyn Clipboard>, ) { match event { - Event::Mouse(mouse::Event::Input { - button: mouse::Button::Left, - state: ButtonState::Pressed, - }) => { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { let mouse_over = layout.bounds().contains(cursor_position); if mouse_over { @@ -185,8 +181,8 @@ where defaults, label_layout.bounds(), &self.label, - self.text_size, - Font::Default, + self.text_size.unwrap_or(renderer.default_size()), + Default::default(), None, HorizontalAlignment::Left, VerticalAlignment::Center, diff --git a/native/src/widget/image.rs b/native/src/widget/image.rs index 6bd0fd68..132f249d 100644 --- a/native/src/widget/image.rs +++ b/native/src/widget/image.rs @@ -123,6 +123,8 @@ pub struct Handle { impl Handle { /// Creates an image [`Handle`] pointing to the image of the given path. /// + /// Makes an educated guess about the image format by examining the data in the file. + /// /// [`Handle`]: struct.Handle.html pub fn from_path<T: Into<PathBuf>>(path: T) -> Handle { Self::from_data(Data::Path(path.into())) @@ -145,6 +147,8 @@ impl Handle { /// Creates an image [`Handle`] containing the image data directly. /// + /// Makes an educated guess about the image format by examining the given data. + /// /// This is useful if you already have your image loaded in-memory, maybe /// because you downloaded or generated it procedurally. /// diff --git a/native/src/widget/pane_grid.rs b/native/src/widget/pane_grid.rs index f84775ed..2d21a968 100644 --- a/native/src/widget/pane_grid.rs +++ b/native/src/widget/pane_grid.rs @@ -9,6 +9,7 @@ //! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.1/examples/pane_grid //! [`PaneGrid`]: struct.PaneGrid.html mod axis; +mod content; mod direction; mod node; mod pane; @@ -16,15 +17,16 @@ mod split; mod state; pub use axis::Axis; +pub use content::Content; pub use direction::Direction; +pub use node::Node; pub use pane::Pane; pub use split::Split; pub use state::{Focus, State}; use crate::{ - input::{keyboard, mouse, ButtonState}, - layout, Clipboard, Element, Event, Hasher, Layout, Length, Point, Size, - Widget, + keyboard, layout, mouse, Clipboard, Element, Event, Hasher, Layout, Length, + Point, Rectangle, Size, Widget, }; /// A collection of panes distributed using either vertical or horizontal splits @@ -74,7 +76,7 @@ use crate::{ /// }.into() /// }) /// .on_drag(Message::PaneDragged) -/// .on_resize(Message::PaneResized); +/// .on_resize(10, Message::PaneResized); /// ``` /// /// [`PaneGrid`]: struct.PaneGrid.html @@ -89,7 +91,7 @@ pub struct PaneGrid<'a, Message, Renderer> { spacing: u16, modifier_keys: keyboard::ModifiersState, on_drag: Option<Box<dyn Fn(DragEvent) -> Message + 'a>>, - on_resize: Option<Box<dyn Fn(ResizeEvent) -> Message + 'a>>, + on_resize: Option<(u16, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>, on_key_press: Option<Box<dyn Fn(KeyPressEvent) -> Option<Message> + 'a>>, } @@ -175,8 +177,8 @@ impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> { /// Sets the modifier keys of the [`PaneGrid`]. /// - /// The modifier keys will need to be pressed to trigger dragging, resizing, - /// and key events. + /// The modifier keys will need to be pressed to trigger dragging, and key + /// events. /// /// The default modifier key is `Ctrl`. /// @@ -206,14 +208,19 @@ impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> { /// Enables the resize interactions of the [`PaneGrid`], which will /// use the provided function to produce messages. /// - /// Panes can be resized using `Modifier keys + Right click`. + /// The `leeway` describes the amount of space around a split that can be + /// used to grab it. + /// + /// The grabbable area of a split will have a length of `spacing + leeway`, + /// properly centered. In other words, a length of + /// `(spacing + leeway) / 2.0` on either side of the split line. /// /// [`PaneGrid`]: struct.PaneGrid.html - pub fn on_resize<F>(mut self, f: F) -> Self + pub fn on_resize<F>(mut self, leeway: u16, f: F) -> Self where F: 'a + Fn(ResizeEvent) -> Message, { - self.on_resize = Some(Box::new(f)); + self.on_resize = Some((leeway, Box::new(f))); self } @@ -242,13 +249,42 @@ impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> { self } + fn click_pane( + &mut self, + layout: Layout<'_>, + cursor_position: Point, + messages: &mut Vec<Message>, + ) { + let mut clicked_region = + self.elements.iter().zip(layout.children()).filter( + |(_, layout)| layout.bounds().contains(cursor_position), + ); + + if let Some(((pane, _), _)) = clicked_region.next() { + match &self.on_drag { + Some(on_drag) + if self.pressed_modifiers.matches(self.modifier_keys) => + { + self.state.pick_pane(pane); + + messages.push(on_drag(DragEvent::Picked { pane: *pane })); + } + _ => { + self.state.focus(pane); + } + } + } else { + self.state.unfocus(); + } + } + fn trigger_resize( &mut self, layout: Layout<'_>, cursor_position: Point, messages: &mut Vec<Message>, ) { - if let Some(on_resize) = &self.on_resize { + if let Some((_, on_resize)) = &self.on_resize { if let Some((split, _)) = self.state.picked_split() { let bounds = layout.bounds(); @@ -405,40 +441,50 @@ where clipboard: Option<&dyn Clipboard>, ) { match event { - Event::Mouse(mouse::Event::Input { - button: mouse::Button::Left, - state, - }) => match state { - ButtonState::Pressed => { - let mut clicked_region = - self.elements.iter().zip(layout.children()).filter( - |(_, layout)| { - layout.bounds().contains(cursor_position) - }, - ); - - if let Some(((pane, _), _)) = clicked_region.next() { - match &self.on_drag { - Some(on_drag) - if self - .pressed_modifiers - .matches(self.modifier_keys) => - { - self.state.pick_pane(pane); - - messages.push(on_drag(DragEvent::Picked { - pane: *pane, - })); + Event::Mouse(mouse_event) => match mouse_event { + mouse::Event::ButtonPressed(mouse::Button::Left) => { + let bounds = layout.bounds(); + + if bounds.contains(cursor_position) { + match self.on_resize { + Some((leeway, _)) => { + let relative_cursor = Point::new( + cursor_position.x - bounds.x, + cursor_position.y - bounds.y, + ); + + let splits = self.state.splits( + f32::from(self.spacing), + Size::new(bounds.width, bounds.height), + ); + + let clicked_split = hovered_split( + splits.iter(), + f32::from(self.spacing + leeway), + relative_cursor, + ); + + if let Some((split, axis)) = clicked_split { + self.state.pick_split(&split, axis); + } else { + self.click_pane( + layout, + cursor_position, + messages, + ); + } } - _ => { - self.state.focus(pane); + None => { + self.click_pane( + layout, + cursor_position, + messages, + ); } } - } else { - self.state.unfocus(); } } - ButtonState::Released => { + mouse::Event::ButtonReleased(mouse::Button::Left) => { if let Some(pane) = self.state.picked_pane() { self.state.focus(&pane); @@ -463,99 +509,44 @@ where messages.push(on_drag(event)); } + } else if self.state.picked_split().is_some() { + self.state.drop_split(); } } - }, - Event::Mouse(mouse::Event::Input { - button: mouse::Button::Right, - state: ButtonState::Pressed, - }) if self.on_resize.is_some() - && self.state.picked_pane().is_none() - && self.pressed_modifiers.matches(self.modifier_keys) => - { - let bounds = layout.bounds(); - - if bounds.contains(cursor_position) { - let relative_cursor = Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ); - - let splits = self.state.splits( - f32::from(self.spacing), - Size::new(bounds.width, bounds.height), - ); - - let mut sorted_splits: Vec<_> = splits - .iter() - .filter(|(_, (axis, rectangle, _))| match axis { - Axis::Horizontal => { - relative_cursor.x > rectangle.x - && relative_cursor.x - < rectangle.x + rectangle.width - } - Axis::Vertical => { - relative_cursor.y > rectangle.y - && relative_cursor.y - < rectangle.y + rectangle.height - } - }) - .collect(); - - sorted_splits.sort_by_key( - |(_, (axis, rectangle, ratio))| { - let distance = match axis { - Axis::Horizontal => (relative_cursor.y - - (rectangle.y + rectangle.height * ratio)) - .abs(), - Axis::Vertical => (relative_cursor.x - - (rectangle.x + rectangle.width * ratio)) - .abs(), - }; - - distance.round() as u32 - }, - ); - - if let Some((split, (axis, _, _))) = sorted_splits.first() { - self.state.pick_split(split, *axis); - self.trigger_resize(layout, cursor_position, messages); - } + mouse::Event::CursorMoved { .. } => { + self.trigger_resize(layout, cursor_position, messages); } - } - Event::Mouse(mouse::Event::Input { - button: mouse::Button::Right, - state: ButtonState::Released, - }) if self.state.picked_split().is_some() => { - self.state.drop_split(); - } - Event::Mouse(mouse::Event::CursorMoved { .. }) => { - self.trigger_resize(layout, cursor_position, messages); - } - Event::Keyboard(keyboard::Event::Input { - modifiers, - key_code, - state, - }) => { - if let Some(on_key_press) = &self.on_key_press { - // TODO: Discard when event is captured - if state == ButtonState::Pressed { - if let Some(_) = self.state.active_pane() { - if modifiers.matches(self.modifier_keys) { - if let Some(message) = - on_key_press(KeyPressEvent { - key_code, - modifiers, - }) - { - messages.push(message); + _ => {} + }, + Event::Keyboard(keyboard_event) => { + match keyboard_event { + keyboard::Event::KeyPressed { + modifiers, + key_code, + } => { + if let Some(on_key_press) = &self.on_key_press { + // TODO: Discard when event is captured + if let Some(_) = self.state.active_pane() { + if modifiers.matches(self.modifier_keys) { + if let Some(message) = + on_key_press(KeyPressEvent { + key_code, + modifiers, + }) + { + messages.push(message); + } } } } + + *self.pressed_modifiers = modifiers; + } + keyboard::Event::KeyReleased { modifiers, .. } => { + *self.pressed_modifiers = modifiers; } + _ => {} } - - *self.pressed_modifiers = modifiers; } _ => {} } @@ -585,11 +576,37 @@ where layout: Layout<'_>, cursor_position: Point, ) -> Renderer::Output { + let picked_split = self + .state + .picked_split() + .or_else(|| match self.on_resize { + Some((leeway, _)) => { + let bounds = layout.bounds(); + + let relative_cursor = Point::new( + cursor_position.x - bounds.x, + cursor_position.y - bounds.y, + ); + + let splits = self + .state + .splits(f32::from(self.spacing), bounds.size()); + + hovered_split( + splits.iter(), + f32::from(self.spacing + leeway), + relative_cursor, + ) + } + None => None, + }) + .map(|(_, axis)| axis); + renderer.draw( defaults, &self.elements, self.state.picked_pane(), - self.state.picked_split().map(|(_, axis)| axis), + picked_split, layout, cursor_position, ) @@ -653,3 +670,25 @@ where Element::new(pane_grid) } } + +/* + * Helpers + */ +fn hovered_split<'a>( + splits: impl Iterator<Item = (&'a Split, &'a (Axis, Rectangle, f32))>, + spacing: f32, + cursor_position: Point, +) -> Option<(Split, Axis)> { + splits + .filter_map(|(split, (axis, region, ratio))| { + let bounds = + axis.split_line_bounds(*region, *ratio, f32::from(spacing)); + + if bounds.contains(cursor_position) { + Some((*split, *axis)) + } else { + None + } + }) + .next() +} diff --git a/native/src/widget/pane_grid/axis.rs b/native/src/widget/pane_grid/axis.rs index f0e3f362..2320cb7c 100644 --- a/native/src/widget/pane_grid/axis.rs +++ b/native/src/widget/pane_grid/axis.rs @@ -14,41 +14,225 @@ impl Axis { &self, rectangle: &Rectangle, ratio: f32, - halved_spacing: f32, + spacing: f32, ) -> (Rectangle, Rectangle) { match self { Axis::Horizontal => { - let height_top = (rectangle.height * ratio).round(); - let height_bottom = rectangle.height - height_top; + let height_top = + (rectangle.height * ratio - spacing / 2.0).round(); + let height_bottom = rectangle.height - height_top - spacing; ( Rectangle { - height: height_top - halved_spacing, + height: height_top, ..*rectangle }, Rectangle { - y: rectangle.y + height_top + halved_spacing, - height: height_bottom - halved_spacing, + y: rectangle.y + height_top + spacing, + height: height_bottom, ..*rectangle }, ) } Axis::Vertical => { - let width_left = (rectangle.width * ratio).round(); - let width_right = rectangle.width - width_left; + let width_left = + (rectangle.width * ratio - spacing / 2.0).round(); + let width_right = rectangle.width - width_left - spacing; ( Rectangle { - width: width_left - halved_spacing, + width: width_left, ..*rectangle }, Rectangle { - x: rectangle.x + width_left + halved_spacing, - width: width_right - halved_spacing, + x: rectangle.x + width_left + spacing, + width: width_right, ..*rectangle }, ) } } } + + pub(super) fn split_line_bounds( + &self, + rectangle: Rectangle, + ratio: f32, + spacing: f32, + ) -> Rectangle { + match self { + Axis::Horizontal => Rectangle { + x: rectangle.x, + y: (rectangle.y + rectangle.height * ratio - spacing / 2.0) + .round(), + width: rectangle.width, + height: spacing, + }, + Axis::Vertical => Rectangle { + x: (rectangle.x + rectangle.width * ratio - spacing / 2.0) + .round(), + y: rectangle.y, + width: spacing, + height: rectangle.height, + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + enum Case { + Horizontal { + overall_height: f32, + spacing: f32, + top_height: f32, + bottom_y: f32, + bottom_height: f32, + }, + Vertical { + overall_width: f32, + spacing: f32, + left_width: f32, + right_x: f32, + right_width: f32, + }, + } + + #[test] + fn split() { + let cases = vec![ + // Even height, even spacing + Case::Horizontal { + overall_height: 10.0, + spacing: 2.0, + top_height: 4.0, + bottom_y: 6.0, + bottom_height: 4.0, + }, + // Odd height, even spacing + Case::Horizontal { + overall_height: 9.0, + spacing: 2.0, + top_height: 4.0, + bottom_y: 6.0, + bottom_height: 3.0, + }, + // Even height, odd spacing + Case::Horizontal { + overall_height: 10.0, + spacing: 1.0, + top_height: 5.0, + bottom_y: 6.0, + bottom_height: 4.0, + }, + // Odd height, odd spacing + Case::Horizontal { + overall_height: 9.0, + spacing: 1.0, + top_height: 4.0, + bottom_y: 5.0, + bottom_height: 4.0, + }, + // Even width, even spacing + Case::Vertical { + overall_width: 10.0, + spacing: 2.0, + left_width: 4.0, + right_x: 6.0, + right_width: 4.0, + }, + // Odd width, even spacing + Case::Vertical { + overall_width: 9.0, + spacing: 2.0, + left_width: 4.0, + right_x: 6.0, + right_width: 3.0, + }, + // Even width, odd spacing + Case::Vertical { + overall_width: 10.0, + spacing: 1.0, + left_width: 5.0, + right_x: 6.0, + right_width: 4.0, + }, + // Odd width, odd spacing + Case::Vertical { + overall_width: 9.0, + spacing: 1.0, + left_width: 4.0, + right_x: 5.0, + right_width: 4.0, + }, + ]; + for case in cases { + match case { + Case::Horizontal { + overall_height, + spacing, + top_height, + bottom_y, + bottom_height, + } => { + let a = Axis::Horizontal; + let r = Rectangle { + x: 0.0, + y: 0.0, + width: 10.0, + height: overall_height, + }; + let (top, bottom) = a.split(&r, 0.5, spacing); + assert_eq!( + top, + Rectangle { + height: top_height, + ..r + } + ); + assert_eq!( + bottom, + Rectangle { + y: bottom_y, + height: bottom_height, + ..r + } + ); + } + Case::Vertical { + overall_width, + spacing, + left_width, + right_x, + right_width, + } => { + let a = Axis::Vertical; + let r = Rectangle { + x: 0.0, + y: 0.0, + width: overall_width, + height: 10.0, + }; + let (left, right) = a.split(&r, 0.5, spacing); + assert_eq!( + left, + Rectangle { + width: left_width, + ..r + } + ); + assert_eq!( + right, + Rectangle { + x: right_x, + width: right_width, + ..r + } + ); + } + } + } + } } diff --git a/native/src/widget/pane_grid/content.rs b/native/src/widget/pane_grid/content.rs new file mode 100644 index 00000000..8822083e --- /dev/null +++ b/native/src/widget/pane_grid/content.rs @@ -0,0 +1,30 @@ +use crate::pane_grid::Axis; + +/// The content of a [`PaneGrid`]. +/// +/// [`PaneGrid`]: struct.PaneGrid.html +#[derive(Debug, Clone)] +pub enum Content<T> { + /// A split of the available space. + Split { + /// The direction of the split. + axis: Axis, + + /// The ratio of the split in [0.0, 1.0]. + ratio: f32, + + /// The left/top [`Content`] of the split. + /// + /// [`Content`]: enum.Node.html + a: Box<Content<T>>, + + /// The right/bottom [`Content`] of the split. + /// + /// [`Content`]: enum.Node.html + b: Box<Content<T>>, + }, + /// A [`Pane`]. + /// + /// [`Pane`]: struct.Pane.html + Pane(T), +} diff --git a/native/src/widget/pane_grid/node.rs b/native/src/widget/pane_grid/node.rs index 4d5970b8..b13c5e26 100644 --- a/native/src/widget/pane_grid/node.rs +++ b/native/src/widget/pane_grid/node.rs @@ -5,20 +5,98 @@ use crate::{ use std::collections::HashMap; -#[derive(Debug, Clone, Hash)] +/// A layout node of a [`PaneGrid`]. +/// +/// [`PaneGrid`]: struct.PaneGrid.html +#[derive(Debug, Clone)] pub enum Node { + /// The region of this [`Node`] is split into two. + /// + /// [`Node`]: enum.Node.html Split { + /// The [`Split`] of this [`Node`]. + /// + /// [`Split`]: struct.Split.html + /// [`Node`]: enum.Node.html id: Split, + + /// The direction of the split. axis: Axis, - ratio: u32, + + /// The ratio of the split in [0.0, 1.0]. + ratio: f32, + + /// The left/top [`Node`] of the split. + /// + /// [`Node`]: enum.Node.html a: Box<Node>, + + /// The right/bottom [`Node`] of the split. + /// + /// [`Node`]: enum.Node.html b: Box<Node>, }, + /// The region of this [`Node`] is taken by a [`Pane`]. + /// + /// [`Pane`]: struct.Pane.html Pane(Pane), } impl Node { - pub fn find(&mut self, pane: &Pane) -> Option<&mut Node> { + /// Returns the rectangular region for each [`Pane`] in the [`Node`] given + /// the spacing between panes and the total available space. + /// + /// [`Pane`]: struct.Pane.html + /// [`Node`]: enum.Node.html + pub fn regions( + &self, + spacing: f32, + size: Size, + ) -> HashMap<Pane, Rectangle> { + let mut regions = HashMap::new(); + + self.compute_regions( + spacing, + &Rectangle { + x: 0.0, + y: 0.0, + width: size.width, + height: size.height, + }, + &mut regions, + ); + + regions + } + + /// Returns the axis, rectangular region, and ratio for each [`Split`] in + /// the [`Node`] given the spacing between panes and the total available + /// space. + /// + /// [`Split`]: struct.Split.html + /// [`Node`]: enum.Node.html + pub fn splits( + &self, + spacing: f32, + size: Size, + ) -> HashMap<Split, (Axis, Rectangle, f32)> { + let mut splits = HashMap::new(); + + self.compute_splits( + spacing, + &Rectangle { + x: 0.0, + y: 0.0, + width: size.width, + height: size.height, + }, + &mut splits, + ); + + splits + } + + pub(crate) fn find(&mut self, pane: &Pane) -> Option<&mut Node> { match self { Node::Split { a, b, .. } => { a.find(pane).or_else(move || b.find(pane)) @@ -33,17 +111,17 @@ impl Node { } } - pub fn split(&mut self, id: Split, axis: Axis, new_pane: Pane) { + pub(crate) fn split(&mut self, id: Split, axis: Axis, new_pane: Pane) { *self = Node::Split { id, axis, - ratio: 500_000, + ratio: 0.5, a: Box::new(self.clone()), b: Box::new(Node::Pane(new_pane)), }; } - pub fn update(&mut self, f: &impl Fn(&mut Node)) { + pub(crate) fn update(&mut self, f: &impl Fn(&mut Node)) { match self { Node::Split { a, b, .. } => { a.update(f); @@ -55,13 +133,13 @@ impl Node { f(self); } - pub fn resize(&mut self, split: &Split, percentage: f32) -> bool { + pub(crate) fn resize(&mut self, split: &Split, percentage: f32) -> bool { match self { Node::Split { id, ratio, a, b, .. } => { if id == split { - *ratio = (percentage * 1_000_000.0).round() as u32; + *ratio = percentage; true } else if a.resize(split, percentage) { @@ -74,7 +152,7 @@ impl Node { } } - pub fn remove(&mut self, pane: &Pane) -> Option<Pane> { + pub(crate) fn remove(&mut self, pane: &Pane) -> Option<Pane> { match self { Node::Split { a, b, .. } => { if a.pane() == Some(*pane) { @@ -91,56 +169,14 @@ impl Node { } } - pub fn regions( - &self, - spacing: f32, - size: Size, - ) -> HashMap<Pane, Rectangle> { - let mut regions = HashMap::new(); - - self.compute_regions( - spacing / 2.0, - &Rectangle { - x: 0.0, - y: 0.0, - width: size.width, - height: size.height, - }, - &mut regions, - ); - - regions - } - - pub fn splits( - &self, - spacing: f32, - size: Size, - ) -> HashMap<Split, (Axis, Rectangle, f32)> { - let mut splits = HashMap::new(); - - self.compute_splits( - spacing / 2.0, - &Rectangle { - x: 0.0, - y: 0.0, - width: size.width, - height: size.height, - }, - &mut splits, - ); - - splits - } - - pub fn pane(&self) -> Option<Pane> { + fn pane(&self) -> Option<Pane> { match self { Node::Split { .. } => None, Node::Pane(pane) => Some(*pane), } } - pub fn first_pane(&self) -> Pane { + fn first_pane(&self) -> Pane { match self { Node::Split { a, .. } => a.first_pane(), Node::Pane(pane) => *pane, @@ -149,7 +185,7 @@ impl Node { fn compute_regions( &self, - halved_spacing: f32, + spacing: f32, current: &Rectangle, regions: &mut HashMap<Pane, Rectangle>, ) { @@ -157,12 +193,10 @@ impl Node { Node::Split { axis, ratio, a, b, .. } => { - let ratio = *ratio as f32 / 1_000_000.0; - let (region_a, region_b) = - axis.split(current, ratio, halved_spacing); + let (region_a, region_b) = axis.split(current, *ratio, spacing); - a.compute_regions(halved_spacing, ®ion_a, regions); - b.compute_regions(halved_spacing, ®ion_b, regions); + a.compute_regions(spacing, ®ion_a, regions); + b.compute_regions(spacing, ®ion_b, regions); } Node::Pane(pane) => { let _ = regions.insert(*pane, *current); @@ -172,7 +206,7 @@ impl Node { fn compute_splits( &self, - halved_spacing: f32, + spacing: f32, current: &Rectangle, splits: &mut HashMap<Split, (Axis, Rectangle, f32)>, ) { @@ -184,16 +218,37 @@ impl Node { b, id, } => { - let ratio = *ratio as f32 / 1_000_000.0; - let (region_a, region_b) = - axis.split(current, ratio, halved_spacing); + let (region_a, region_b) = axis.split(current, *ratio, spacing); - let _ = splits.insert(*id, (*axis, *current, ratio)); + let _ = splits.insert(*id, (*axis, *current, *ratio)); - a.compute_splits(halved_spacing, ®ion_a, splits); - b.compute_splits(halved_spacing, ®ion_b, splits); + a.compute_splits(spacing, ®ion_a, splits); + b.compute_splits(spacing, ®ion_b, splits); } Node::Pane(_) => {} } } } + +impl std::hash::Hash for Node { + fn hash<H: std::hash::Hasher>(&self, state: &mut H) { + match self { + Node::Split { + id, + axis, + ratio, + a, + b, + } => { + id.hash(state); + axis.hash(state); + ((ratio * 100_000.0) as u32).hash(state); + a.hash(state); + b.hash(state); + } + Node::Pane(pane) => { + pane.hash(state); + } + } + } +} diff --git a/native/src/widget/pane_grid/state.rs b/native/src/widget/pane_grid/state.rs index 0a8b8419..4b13fb8e 100644 --- a/native/src/widget/pane_grid/state.rs +++ b/native/src/widget/pane_grid/state.rs @@ -1,6 +1,6 @@ use crate::{ - input::keyboard, - pane_grid::{node::Node, Axis, Direction, Pane, Split}, + keyboard, + pane_grid::{Axis, Content, Direction, Node, Pane, Split}, Hasher, Point, Rectangle, Size, }; @@ -21,7 +21,7 @@ use std::collections::HashMap; /// [`Pane`]: struct.Pane.html /// [`Split`]: struct.Split.html /// [`State`]: struct.State.html -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct State<T> { pub(super) panes: HashMap<Pane, T>, pub(super) internal: Internal, @@ -53,23 +53,28 @@ impl<T> State<T> { /// [`State`]: struct.State.html /// [`Pane`]: struct.Pane.html pub fn new(first_pane_state: T) -> (Self, Pane) { - let first_pane = Pane(0); + (Self::with_content(Content::Pane(first_pane_state)), Pane(0)) + } + /// Creates a new [`State`] with the given [`Content`]. + /// + /// [`State`]: struct.State.html + /// [`Content`]: enum.Content.html + pub fn with_content(content: impl Into<Content<T>>) -> Self { let mut panes = HashMap::new(); - let _ = panes.insert(first_pane, first_pane_state); - - ( - State { - panes, - internal: Internal { - layout: Node::Pane(first_pane), - last_id: 0, - action: Action::Idle { focus: None }, - }, - modifiers: keyboard::ModifiersState::default(), + + let (layout, last_id) = + Self::distribute_content(&mut panes, content.into(), 0); + + State { + panes, + internal: Internal { + layout, + last_id, + action: Action::Idle { focus: None }, }, - first_pane, - ) + modifiers: keyboard::ModifiersState::default(), + } } /// Returns the total amount of panes in the [`State`]. @@ -82,6 +87,14 @@ impl<T> State<T> { /// Returns the internal state of the given [`Pane`], if it exists. /// /// [`Pane`]: struct.Pane.html + pub fn get(&self, pane: &Pane) -> Option<&T> { + self.panes.get(pane) + } + + /// Returns the internal state of the given [`Pane`] with mutability, if it + /// exists. + /// + /// [`Pane`]: struct.Pane.html pub fn get_mut(&mut self, pane: &Pane) -> Option<&mut T> { self.panes.get_mut(pane) } @@ -102,6 +115,13 @@ impl<T> State<T> { self.panes.iter_mut() } + /// Returns the layout of the [`State`]. + /// + /// [`State`]: struct.State.html + pub fn layout(&self) -> &Node { + &self.internal.layout + } + /// Returns the active [`Pane`] of the [`State`], if there is one. /// /// A [`Pane`] is active if it is focused and is __not__ being dragged. @@ -176,7 +196,12 @@ impl<T> State<T> { /// /// [`Pane`]: struct.Pane.html /// [`Axis`]: enum.Axis.html - pub fn split(&mut self, axis: Axis, pane: &Pane, state: T) -> Option<Pane> { + pub fn split( + &mut self, + axis: Axis, + pane: &Pane, + state: T, + ) -> Option<(Pane, Split)> { let node = self.internal.layout.find(pane)?; let new_pane = { @@ -196,7 +221,7 @@ impl<T> State<T> { let _ = self.panes.insert(new_pane, state); self.focus(&new_pane); - Some(new_pane) + Some((new_pane, new_split)) } /// Swaps the position of the provided panes in the [`State`]. @@ -246,9 +271,39 @@ impl<T> State<T> { None } } + + fn distribute_content( + panes: &mut HashMap<Pane, T>, + content: Content<T>, + next_id: usize, + ) -> (Node, usize) { + match content { + Content::Split { axis, ratio, a, b } => { + let (a, next_id) = Self::distribute_content(panes, *a, next_id); + let (b, next_id) = Self::distribute_content(panes, *b, next_id); + + ( + Node::Split { + id: Split(next_id), + axis, + ratio, + a: Box::new(a), + b: Box::new(b), + }, + next_id + 1, + ) + } + Content::Pane(state) => { + let id = Pane(next_id); + let _ = panes.insert(id, state); + + (Node::Pane(id), next_id + 1) + } + } + } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Internal { layout: Node, last_id: usize, diff --git a/native/src/widget/radio.rs b/native/src/widget/radio.rs index 0ec621bf..5b8d00e9 100644 --- a/native/src/widget/radio.rs +++ b/native/src/widget/radio.rs @@ -1,7 +1,6 @@ //! Create choices using radio buttons. use crate::{ - input::{mouse, ButtonState}, - layout, row, text, Align, Clipboard, Element, Event, Font, Hasher, + layout, mouse, row, text, Align, Clipboard, Element, Event, Hasher, HorizontalAlignment, Layout, Length, Point, Rectangle, Row, Text, VerticalAlignment, Widget, }; @@ -35,14 +34,20 @@ use std::hash::Hash; /// ///  #[allow(missing_debug_implementations)] -pub struct Radio<Message, Renderer: self::Renderer> { +pub struct Radio<Message, Renderer: self::Renderer + text::Renderer> { is_selected: bool, on_click: Message, label: String, + width: Length, + size: u16, + spacing: u16, + text_size: Option<u16>, style: Renderer::Style, } -impl<Message, Renderer: self::Renderer> Radio<Message, Renderer> { +impl<Message, Renderer: self::Renderer + text::Renderer> + Radio<Message, Renderer> +{ /// Creates a new [`Radio`] button. /// /// It expects: @@ -67,10 +72,46 @@ impl<Message, Renderer: self::Renderer> Radio<Message, Renderer> { is_selected: Some(value) == selected, on_click: f(value), label: label.into(), + width: Length::Shrink, + size: <Renderer as self::Renderer>::DEFAULT_SIZE, + spacing: Renderer::DEFAULT_SPACING, //15 + text_size: None, style: Renderer::Style::default(), } } + /// Sets the size of the [`Radio`] button. + /// + /// [`Radio`]: struct.Radio.html + pub fn size(mut self, size: u16) -> Self { + self.size = size; + self + } + + /// Sets the width of the [`Radio`] button. + /// + /// [`Radio`]: struct.Radio.html + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the spacing between the [`Radio`] button and the text. + /// + /// [`Radio`]: struct.Radio.html + pub fn spacing(mut self, spacing: u16) -> Self { + self.spacing = spacing; + self + } + + /// Sets the text size of the [`Radio`] button. + /// + /// [`Radio`]: struct.Radio.html + pub fn text_size(mut self, text_size: u16) -> Self { + self.text_size = Some(text_size); + self + } + /// Sets the style of the [`Radio`] button. /// /// [`Radio`]: struct.Radio.html @@ -86,7 +127,7 @@ where Message: Clone, { fn width(&self) -> Length { - Length::Fill + self.width } fn height(&self) -> Length { @@ -98,18 +139,20 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let size = self::Renderer::default_size(renderer); - Row::<(), Renderer>::new() - .width(Length::Fill) - .spacing(15) + .width(self.width) + .spacing(self.spacing) .align_items(Align::Center) .push( Row::new() - .width(Length::Units(size as u16)) - .height(Length::Units(size as u16)), + .width(Length::Units(self.size)) + .height(Length::Units(self.size)), + ) + .push( + Text::new(&self.label) + .width(self.width) + .size(self.text_size.unwrap_or(renderer.default_size())), ) - .push(Text::new(&self.label)) .layout(renderer, limits) } @@ -123,10 +166,7 @@ where _clipboard: Option<&dyn Clipboard>, ) { match event { - Event::Mouse(mouse::Event::Input { - button: mouse::Button::Left, - state: ButtonState::Pressed, - }) => { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { if layout.bounds().contains(cursor_position) { messages.push(self.on_click.clone()); } @@ -154,8 +194,8 @@ where defaults, label_layout.bounds(), &self.label, - <Renderer as text::Renderer>::DEFAULT_SIZE, - Font::Default, + self.text_size.unwrap_or(renderer.default_size()), + Default::default(), None, HorizontalAlignment::Left, VerticalAlignment::Center, @@ -192,10 +232,15 @@ pub trait Renderer: crate::Renderer { /// The style supported by this renderer. type Style: Default; - /// Returns the default size of a [`Radio`] button. + /// The default size of a [`Radio`] button. + /// + /// [`Radio`]: struct.Radio.html + const DEFAULT_SIZE: u16; + + /// The default spacing of a [`Radio`] button. /// /// [`Radio`]: struct.Radio.html - fn default_size(&self) -> u32; + const DEFAULT_SPACING: u16; /// Draws a [`Radio`] button. /// diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index 393095a4..3c8e5e5b 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -1,9 +1,7 @@ //! Navigate an endless amount of content with a scrollbar. use crate::{ - column, - input::{mouse, ButtonState}, - layout, Align, Clipboard, Column, Element, Event, Hasher, Layout, Length, - Point, Rectangle, Size, Widget, + column, layout, mouse, Align, Clipboard, Column, Element, Event, Hasher, + Layout, Length, Point, Rectangle, Size, Widget, }; use std::{f32, hash::Hash, u32}; @@ -188,10 +186,9 @@ where if self.state.is_scroller_grabbed() { match event { - Event::Mouse(mouse::Event::Input { - button: mouse::Button::Left, - state: ButtonState::Released, - }) => { + Event::Mouse(mouse::Event::ButtonReleased( + mouse::Button::Left, + )) => { self.state.scroller_grabbed_at = None; } Event::Mouse(mouse::Event::CursorMoved { .. }) => { @@ -212,10 +209,9 @@ where } } else if is_mouse_over_scrollbar { match event { - Event::Mouse(mouse::Event::Input { - button: mouse::Button::Left, - state: ButtonState::Pressed, - }) => { + Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + )) => { if let Some(scrollbar) = scrollbar { if let Some(scroller_grabbed_at) = scrollbar.grab_scroller(cursor_position) diff --git a/native/src/widget/slider.rs b/native/src/widget/slider.rs index 1feb7825..70f2b6ac 100644 --- a/native/src/widget/slider.rs +++ b/native/src/widget/slider.rs @@ -5,8 +5,7 @@ //! [`Slider`]: struct.Slider.html //! [`State`]: struct.State.html use crate::{ - input::{mouse, ButtonState}, - layout, Clipboard, Element, Event, Hasher, Layout, Length, Point, + layout, mouse, Clipboard, Element, Event, Hasher, Layout, Length, Point, Rectangle, Size, Widget, }; @@ -17,13 +16,16 @@ use std::{hash::Hash, ops::RangeInclusive}; /// /// A [`Slider`] will try to fill the horizontal space of its container. /// +/// The [`Slider`] range of numeric values is generic and its step size defaults +/// to 1 unit. +/// /// [`Slider`]: struct.Slider.html /// /// # Example /// ``` /// # use iced_native::{slider, renderer::Null}; /// # -/// # pub type Slider<'a, Message> = iced_native::Slider<'a, Message, Null>; +/// # pub type Slider<'a, T, Message> = iced_native::Slider<'a, T, Message, Null>; /// pub enum Message { /// SliderChanged(f32), /// } @@ -36,16 +38,22 @@ use std::{hash::Hash, ops::RangeInclusive}; /// ///  #[allow(missing_debug_implementations)] -pub struct Slider<'a, Message, Renderer: self::Renderer> { +pub struct Slider<'a, T, Message, Renderer: self::Renderer> { state: &'a mut State, - range: RangeInclusive<f32>, - value: f32, - on_change: Box<dyn Fn(f32) -> Message>, + range: RangeInclusive<T>, + step: T, + value: T, + on_change: Box<dyn Fn(T) -> Message>, + on_release: Option<Message>, width: Length, style: Renderer::Style, } -impl<'a, Message, Renderer: self::Renderer> Slider<'a, Message, Renderer> { +impl<'a, T, Message, Renderer> Slider<'a, T, Message, Renderer> +where + T: Copy + From<u8> + std::cmp::PartialOrd, + Renderer: self::Renderer, +{ /// Creates a new [`Slider`]. /// /// It expects: @@ -60,23 +68,50 @@ impl<'a, Message, Renderer: self::Renderer> Slider<'a, Message, Renderer> { /// [`State`]: struct.State.html pub fn new<F>( state: &'a mut State, - range: RangeInclusive<f32>, - value: f32, + range: RangeInclusive<T>, + value: T, on_change: F, ) -> Self where - F: 'static + Fn(f32) -> Message, + F: 'static + Fn(T) -> Message, { + let value = if value >= *range.start() { + value + } else { + *range.start() + }; + + let value = if value <= *range.end() { + value + } else { + *range.end() + }; + Slider { state, - value: value.max(*range.start()).min(*range.end()), + value, range, + step: T::from(1), on_change: Box::new(on_change), + on_release: None, width: Length::Fill, style: Renderer::Style::default(), } } + /// Sets the release message of the [`Slider`]. + /// This is called when the mouse is released from the slider. + /// + /// Typically, the user's interaction with the slider is finished when this message is produced. + /// This is useful if you need to spawn a long-running task from the slider's result, where + /// the default on_change message could create too many events. + /// + /// [`Slider`]: struct.Slider.html + pub fn on_release(mut self, on_release: Message) -> Self { + self.on_release = Some(on_release); + self + } + /// Sets the width of the [`Slider`]. /// /// [`Slider`]: struct.Slider.html @@ -92,6 +127,14 @@ impl<'a, Message, Renderer: self::Renderer> Slider<'a, Message, Renderer> { self.style = style.into(); self } + + /// Sets the step size of the [`Slider`]. + /// + /// [`Slider`]: struct.Slider.html + pub fn step(mut self, step: T) -> Self { + self.step = step; + self + } } /// The local state of a [`Slider`]. @@ -111,10 +154,12 @@ impl State { } } -impl<'a, Message, Renderer> Widget<Message, Renderer> - for Slider<'a, Message, Renderer> +impl<'a, T, Message, Renderer> Widget<Message, Renderer> + for Slider<'a, T, Message, Renderer> where + T: Copy + Into<f64> + num_traits::FromPrimitive, Renderer: self::Renderer, + Message: Clone, { fn width(&self) -> Length { self.width @@ -149,40 +194,50 @@ where ) { let mut change = || { let bounds = layout.bounds(); - if cursor_position.x <= bounds.x { messages.push((self.on_change)(*self.range.start())); } else if cursor_position.x >= bounds.x + bounds.width { messages.push((self.on_change)(*self.range.end())); } else { - let percent = (cursor_position.x - bounds.x) / bounds.width; - let value = (self.range.end() - self.range.start()) * percent - + self.range.start(); + let step = self.step.into(); + let start = (*self.range.start()).into(); + let end = (*self.range.end()).into(); + + let percent = f64::from(cursor_position.x - bounds.x) + / f64::from(bounds.width); + + let steps = (percent * (end - start) / step).round(); + let value = steps * step + start; - messages.push((self.on_change)(value)); + if let Some(value) = T::from_f64(value) { + messages.push((self.on_change)(value)); + } } }; match event { - Event::Mouse(mouse::Event::Input { - button: mouse::Button::Left, - state, - }) => match state { - ButtonState::Pressed => { + Event::Mouse(mouse_event) => match mouse_event { + mouse::Event::ButtonPressed(mouse::Button::Left) => { if layout.bounds().contains(cursor_position) { change(); self.state.is_dragging = true; } } - ButtonState::Released => { - self.state.is_dragging = false; + mouse::Event::ButtonReleased(mouse::Button::Left) => { + if self.state.is_dragging { + if let Some(on_release) = self.on_release.clone() { + messages.push(on_release); + } + self.state.is_dragging = false; + } } - }, - Event::Mouse(mouse::Event::CursorMoved { .. }) => { - if self.state.is_dragging { - change(); + mouse::Event::CursorMoved { .. } => { + if self.state.is_dragging { + change(); + } } - } + _ => {} + }, _ => {} } } @@ -194,11 +249,14 @@ where layout: Layout<'_>, cursor_position: Point, ) -> Renderer::Output { + let start = *self.range.start(); + let end = *self.range.end(); + renderer.draw( layout.bounds(), cursor_position, - self.range.clone(), - self.value, + start.into() as f32..=end.into() as f32, + self.value.into() as f32, self.state.is_dragging, &self.style, ) @@ -251,14 +309,15 @@ pub trait Renderer: crate::Renderer { ) -> Self::Output; } -impl<'a, Message, Renderer> From<Slider<'a, Message, Renderer>> +impl<'a, T, Message, Renderer> From<Slider<'a, T, Message, Renderer>> for Element<'a, Message, Renderer> where + T: 'a + Copy + Into<f64> + num_traits::FromPrimitive, Renderer: 'a + self::Renderer, - Message: 'a, + Message: 'a + Clone, { fn from( - slider: Slider<'a, Message, Renderer>, + slider: Slider<'a, T, Message, Renderer>, ) -> Element<'a, Message, Renderer> { Element::new(slider) } diff --git a/native/src/widget/text.rs b/native/src/widget/text.rs index dc7c33ec..48a69e34 100644 --- a/native/src/widget/text.rs +++ b/native/src/widget/text.rs @@ -1,7 +1,7 @@ //! Write some text for your users to read. use crate::{ - layout, Color, Element, Font, Hasher, HorizontalAlignment, Layout, Length, - Point, Rectangle, Size, VerticalAlignment, Widget, + layout, Color, Element, Hasher, HorizontalAlignment, Layout, Length, Point, + Rectangle, Size, VerticalAlignment, Widget, }; use std::hash::Hash; @@ -11,7 +11,7 @@ use std::hash::Hash; /// # Example /// /// ``` -/// # use iced_native::Text; +/// # type Text = iced_native::Text<iced_native::renderer::Null>; /// # /// Text::new("I <3 iced!") /// .color([0.0, 0.0, 1.0]) @@ -19,19 +19,19 @@ use std::hash::Hash; /// ``` /// ///  -#[derive(Debug, Clone)] -pub struct Text { +#[derive(Debug)] +pub struct Text<Renderer: self::Renderer> { content: String, size: Option<u16>, color: Option<Color>, - font: Font, + font: Renderer::Font, width: Length, height: Length, horizontal_alignment: HorizontalAlignment, vertical_alignment: VerticalAlignment, } -impl Text { +impl<Renderer: self::Renderer> Text<Renderer> { /// Create a new fragment of [`Text`] with the given contents. /// /// [`Text`]: struct.Text.html @@ -40,7 +40,7 @@ impl Text { content: label.into(), size: None, color: None, - font: Font::Default, + font: Default::default(), width: Length::Shrink, height: Length::Shrink, horizontal_alignment: HorizontalAlignment::Left, @@ -69,8 +69,8 @@ impl Text { /// /// [`Text`]: struct.Text.html /// [`Font`]: ../../struct.Font.html - pub fn font(mut self, font: Font) -> Self { - self.font = font; + pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self { + self.font = font.into(); self } @@ -112,7 +112,7 @@ impl Text { } } -impl<Message, Renderer> Widget<Message, Renderer> for Text +impl<Message, Renderer> Widget<Message, Renderer> for Text<Renderer> where Renderer: self::Renderer, { @@ -131,7 +131,7 @@ where ) -> layout::Node { let limits = limits.width(self.width).height(self.height); - let size = self.size.unwrap_or(Renderer::DEFAULT_SIZE); + let size = self.size.unwrap_or(renderer.default_size()); let bounds = limits.max(); @@ -154,7 +154,7 @@ where defaults, layout.bounds(), &self.content, - self.size.unwrap_or(Renderer::DEFAULT_SIZE), + self.size.unwrap_or(renderer.default_size()), self.font, self.color, self.horizontal_alignment, @@ -163,7 +163,8 @@ where } fn hash_layout(&self, state: &mut Hasher) { - std::any::TypeId::of::<Text>().hash(state); + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); self.content.hash(state); self.size.hash(state); @@ -181,10 +182,15 @@ where /// [renderer]: ../../renderer/index.html /// [`UserInterface`]: ../../struct.UserInterface.html pub trait Renderer: crate::Renderer { - /// The default size of [`Text`]. + /// The font type used for [`Text`]. /// /// [`Text`]: struct.Text.html - const DEFAULT_SIZE: u16; + type Font: Default + Copy; + + /// Returns the default size of [`Text`]. + /// + /// [`Text`]: struct.Text.html + fn default_size(&self) -> u16; /// Measures the [`Text`] in the given bounds and returns the minimum /// boundaries that can fit the contents. @@ -194,7 +200,7 @@ pub trait Renderer: crate::Renderer { &self, content: &str, size: u16, - font: Font, + font: Self::Font, bounds: Size, ) -> (f32, f32); @@ -217,18 +223,34 @@ pub trait Renderer: crate::Renderer { bounds: Rectangle, content: &str, size: u16, - font: Font, + font: Self::Font, color: Option<Color>, horizontal_alignment: HorizontalAlignment, vertical_alignment: VerticalAlignment, ) -> Self::Output; } -impl<'a, Message, Renderer> From<Text> for Element<'a, Message, Renderer> +impl<'a, Message, Renderer> From<Text<Renderer>> + for Element<'a, Message, Renderer> where - Renderer: self::Renderer, + Renderer: self::Renderer + 'a, { - fn from(text: Text) -> Element<'a, Message, Renderer> { + fn from(text: Text<Renderer>) -> Element<'a, Message, Renderer> { Element::new(text) } } + +impl<Renderer: self::Renderer> Clone for Text<Renderer> { + fn clone(&self) -> Self { + Self { + content: self.content.clone(), + size: self.size, + color: self.color, + font: self.font, + width: self.width, + height: self.height, + horizontal_alignment: self.horizontal_alignment, + vertical_alignment: self.vertical_alignment, + } + } +} diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index 7d1a7415..24085606 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -15,13 +15,10 @@ pub use value::Value; use editor::Editor; use crate::{ - input::{ - keyboard, - mouse::{self, click}, - ButtonState, - }, - layout, Clipboard, Element, Event, Font, Hasher, Layout, Length, Point, - Rectangle, Size, Widget, + keyboard, layout, + mouse::{self, click}, + Clipboard, Element, Event, Hasher, Layout, Length, Point, Rectangle, Size, + Widget, }; use std::u32; @@ -56,7 +53,7 @@ pub struct TextInput<'a, Message, Renderer: self::Renderer> { placeholder: String, value: Value, is_secure: bool, - font: Font, + font: Renderer::Font, width: Length, max_width: u32, padding: u16, @@ -91,7 +88,7 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { placeholder: String::from(placeholder), value: Value::new(value), is_secure: false, - font: Font::Default, + font: Default::default(), width: Length::Fill, max_width: u32::MAX, padding: 0, @@ -114,7 +111,7 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { /// /// [`Text`]: struct.Text.html /// [`Font`]: ../../struct.Font.html - pub fn font(mut self, font: Font) -> Self { + pub fn font(mut self, font: Renderer::Font) -> Self { self.font = font; self } @@ -212,10 +209,7 @@ where clipboard: Option<&dyn Clipboard>, ) { match event { - Event::Mouse(mouse::Event::Input { - button: mouse::Button::Left, - state: ButtonState::Pressed, - }) => { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { let is_clicked = layout.bounds().contains(cursor_position); if is_clicked { @@ -280,10 +274,7 @@ where self.state.is_dragging = is_clicked; self.state.is_focused = is_clicked; } - Event::Mouse(mouse::Event::Input { - button: mouse::Button::Left, - state: ButtonState::Released, - }) => { + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { self.state.is_dragging = false; } Event::Mouse(mouse::Event::CursorMoved { x, .. }) => { @@ -327,9 +318,8 @@ where let message = (self.on_change)(editor.contents()); messages.push(message); } - Event::Keyboard(keyboard::Event::Input { + Event::Keyboard(keyboard::Event::KeyPressed { key_code, - state: ButtonState::Pressed, modifiers, }) if self.state.is_focused => match key_code { keyboard::KeyCode::Enter => { @@ -473,10 +463,8 @@ where } _ => {} }, - Event::Keyboard(keyboard::Event::Input { - key_code, - state: ButtonState::Released, - .. + Event::Keyboard(keyboard::Event::KeyReleased { + key_code, .. }) => match key_code { keyboard::KeyCode::V => { self.state.is_pasting = None; @@ -544,6 +532,11 @@ where /// [`TextInput`]: struct.TextInput.html /// [renderer]: ../../renderer/index.html pub trait Renderer: crate::Renderer + Sized { + /// The font type used for [`TextInput`]. + /// + /// [`TextInput`]: struct.TextInput.html + type Font: Default + Copy; + /// The style supported by this renderer. type Style: Default; @@ -555,7 +548,7 @@ pub trait Renderer: crate::Renderer + Sized { /// Returns the width of the value of the [`TextInput`]. /// /// [`TextInput`]: struct.TextInput.html - fn measure_value(&self, value: &str, size: u16, font: Font) -> f32; + fn measure_value(&self, value: &str, size: u16, font: Self::Font) -> f32; /// Returns the current horizontal offset of the value of the /// [`TextInput`]. @@ -568,7 +561,7 @@ pub trait Renderer: crate::Renderer + Sized { fn offset( &self, text_bounds: Rectangle, - font: Font, + font: Self::Font, size: u16, value: &Value, state: &State, @@ -592,7 +585,7 @@ pub trait Renderer: crate::Renderer + Sized { bounds: Rectangle, text_bounds: Rectangle, cursor_position: Point, - font: Font, + font: Self::Font, size: u16, placeholder: &str, value: &Value, @@ -607,7 +600,7 @@ pub trait Renderer: crate::Renderer + Sized { fn find_cursor_position( &self, text_bounds: Rectangle, - font: Font, + font: Self::Font, size: Option<u16>, value: &Value, state: &State, @@ -690,13 +683,37 @@ impl State { pub fn cursor(&self) -> Cursor { self.cursor } + + /// Moves the [`Cursor`] of the [`TextInput`] to the front of the input text. + /// + /// [`Cursor`]: struct.Cursor.html + /// [`TextInput`]: struct.TextInput.html + pub fn move_cursor_to_front(&mut self) { + self.cursor.move_to(0); + } + + /// Moves the [`Cursor`] of the [`TextInput`] to the end of the input text. + /// + /// [`Cursor`]: struct.Cursor.html + /// [`TextInput`]: struct.TextInput.html + pub fn move_cursor_to_end(&mut self) { + self.cursor.move_to(usize::MAX); + } + + /// Moves the [`Cursor`] of the [`TextInput`] to an arbitrary location. + /// + /// [`Cursor`]: struct.Cursor.html + /// [`TextInput`]: struct.TextInput.html + pub fn move_cursor_to(&mut self, position: usize) { + self.cursor.move_to(position); + } } // TODO: Reduce allocations fn find_cursor_position<Renderer: self::Renderer>( renderer: &Renderer, value: &Value, - font: Font, + font: Renderer::Font, size: u16, target: f32, start: usize, @@ -749,7 +766,7 @@ fn find_cursor_position<Renderer: self::Renderer>( } mod platform { - use crate::input::keyboard; + use crate::keyboard; pub fn is_jump_modifier_pressed( modifiers: keyboard::ModifiersState, diff --git a/native/src/window.rs b/native/src/window.rs index 4dcae62f..220bb3be 100644 --- a/native/src/window.rs +++ b/native/src/window.rs @@ -1,6 +1,4 @@ //! Build window-based GUI applications. -mod backend; mod event; -pub use backend::Backend; pub use event::Event; diff --git a/src/application.rs b/src/application.rs index 689332f1..47b89dbd 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,4 +1,6 @@ -use crate::{window, Command, Element, Executor, Settings, Subscription}; +use crate::{ + window, Color, Command, Element, Executor, Settings, Subscription, +}; /// An interactive cross-platform application. /// @@ -174,6 +176,31 @@ pub trait Application: Sized { window::Mode::Windowed } + /// Returns the background color of the [`Application`]. + /// + /// By default, it returns [`Color::WHITE`]. + /// + /// [`Application`]: trait.Application.html + /// [`Color::WHITE`]: struct.Color.html#const.WHITE + fn background_color(&self) -> Color { + Color::WHITE + } + + /// Returns the scale factor of the [`Application`]. + /// + /// It can be used to dynamically control the size of the UI at runtime + /// (i.e. zooming). + /// + /// For instance, a scale factor of `2.0` will make widgets twice as big, + /// while a scale factor of `0.5` will shrink them to half their size. + /// + /// By default, it returns `1.0`. + /// + /// [`Application`]: trait.Application.html + fn scale_factor(&self) -> f64 { + 1.0 + } + /// Runs the [`Application`]. /// /// On native platforms, this method will take control of the current thread @@ -188,20 +215,22 @@ pub trait Application: Sized { { #[cfg(not(target_arch = "wasm32"))] { - let wgpu_settings = iced_wgpu::Settings { + let renderer_settings = crate::renderer::Settings { default_font: settings.default_font, + default_text_size: settings.default_text_size, antialiasing: if settings.antialiasing { - Some(iced_wgpu::settings::Antialiasing::MSAAx4) + Some(crate::renderer::settings::Antialiasing::MSAAx4) } else { None }, - ..iced_wgpu::Settings::default() + ..crate::renderer::Settings::default() }; - <Instance<Self> as iced_winit::Application>::run( - settings.into(), - wgpu_settings, - ); + crate::runtime::application::run::< + Instance<Self>, + Self::Executor, + crate::renderer::window::Compositor, + >(settings.into(), renderer_settings); } #[cfg(target_arch = "wasm32")] @@ -212,15 +241,29 @@ pub trait Application: Sized { struct Instance<A: Application>(A); #[cfg(not(target_arch = "wasm32"))] -impl<A> iced_winit::Application for Instance<A> +impl<A> iced_winit::Program for Instance<A> where A: Application, { - type Backend = iced_wgpu::window::Backend; - type Executor = A::Executor; - type Flags = A::Flags; + type Renderer = crate::renderer::Renderer; type Message = A::Message; + fn update(&mut self, message: Self::Message) -> Command<Self::Message> { + self.0.update(message) + } + + fn view(&mut self) -> Element<'_, Self::Message> { + self.0.view() + } +} + +#[cfg(not(target_arch = "wasm32"))] +impl<A> crate::runtime::Application for Instance<A> +where + A: Application, +{ + type Flags = A::Flags; + fn new(flags: Self::Flags) -> (Self, Command<A::Message>) { let (app, command) = A::new(flags); @@ -238,16 +281,16 @@ where } } - fn update(&mut self, message: Self::Message) -> Command<Self::Message> { - self.0.update(message) - } - fn subscription(&self) -> Subscription<Self::Message> { self.0.subscription() } - fn view(&mut self) -> Element<'_, Self::Message> { - self.0.view() + fn background_color(&self) -> Color { + self.0.background_color() + } + + fn scale_factor(&self) -> f64 { + self.0.scale_factor() } } diff --git a/src/element.rs b/src/element.rs index e5356fb6..6f47c701 100644 --- a/src/element.rs +++ b/src/element.rs @@ -3,7 +3,7 @@ /// This is an alias of an `iced_native` element with a default `Renderer`. #[cfg(not(target_arch = "wasm32"))] pub type Element<'a, Message> = - iced_winit::Element<'a, Message, iced_wgpu::Renderer>; + crate::runtime::Element<'a, Message, crate::renderer::Renderer>; #[cfg(target_arch = "wasm32")] pub use iced_web::Element; diff --git a/src/keyboard.rs b/src/keyboard.rs index 181dd974..0b3e894d 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -1,6 +1,2 @@ //! Listen and react to keyboard events. -#[cfg(not(target_arch = "wasm32"))] -pub use iced_winit::input::keyboard::{KeyCode, ModifiersState}; - -#[cfg(target_arch = "wasm32")] -pub use iced_web::keyboard::{KeyCode, ModifiersState}; +pub use crate::runtime::keyboard::{Event, KeyCode, ModifiersState}; @@ -185,10 +185,41 @@ mod sandbox; pub mod executor; pub mod keyboard; +pub mod mouse; pub mod settings; pub mod widget; pub mod window; +#[cfg(all( + any(feature = "tokio", feature = "async-std"), + not(target_arch = "wasm32") +))] +#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio", feature = "async-std"))))] +pub mod time; + +#[cfg(all( + not(target_arch = "wasm32"), + not(feature = "glow"), + feature = "wgpu" +))] +use iced_winit as runtime; + +#[cfg(all(not(target_arch = "wasm32"), feature = "glow"))] +use iced_glutin as runtime; + +#[cfg(all( + not(target_arch = "wasm32"), + not(feature = "glow"), + feature = "wgpu" +))] +use iced_wgpu as renderer; + +#[cfg(all(not(target_arch = "wasm32"), feature = "glow"))] +use iced_glow as renderer; + +#[cfg(target_arch = "wasm32")] +use iced_web as runtime; + #[doc(no_inline)] pub use widget::*; @@ -198,13 +229,7 @@ pub use executor::Executor; pub use sandbox::Sandbox; pub use settings::Settings; -#[cfg(not(target_arch = "wasm32"))] -use iced_winit as runtime; - -#[cfg(target_arch = "wasm32")] -use iced_web as runtime; - pub use runtime::{ futures, Align, Background, Color, Command, Font, HorizontalAlignment, - Length, Point, Size, Subscription, Vector, VerticalAlignment, + Length, Point, Rectangle, Size, Subscription, Vector, VerticalAlignment, }; diff --git a/src/mouse.rs b/src/mouse.rs new file mode 100644 index 00000000..d61ed09a --- /dev/null +++ b/src/mouse.rs @@ -0,0 +1,2 @@ +//! Listen and react to mouse events. +pub use crate::runtime::mouse::{Button, Event, Interaction, ScrollDelta}; diff --git a/src/sandbox.rs b/src/sandbox.rs index c6fa45d0..6a73eab0 100644 --- a/src/sandbox.rs +++ b/src/sandbox.rs @@ -1,4 +1,6 @@ -use crate::{executor, Application, Command, Element, Settings, Subscription}; +use crate::{ + executor, Application, Color, Command, Element, Settings, Subscription, +}; /// A sandboxed [`Application`]. /// @@ -124,6 +126,31 @@ pub trait Sandbox { /// [`Sandbox`]: trait.Sandbox.html fn view(&mut self) -> Element<'_, Self::Message>; + /// Returns the background color of the [`Sandbox`]. + /// + /// By default, it returns [`Color::WHITE`]. + /// + /// [`Sandbox`]: trait.Sandbox.html + /// [`Color::WHITE`]: struct.Color.html#const.WHITE + fn background_color(&self) -> Color { + Color::WHITE + } + + /// Returns the scale factor of the [`Sandbox`]. + /// + /// It can be used to dynamically control the size of the UI at runtime + /// (i.e. zooming). + /// + /// For instance, a scale factor of `2.0` will make widgets twice as big, + /// while a scale factor of `0.5` will shrink them to half their size. + /// + /// By default, it returns `1.0`. + /// + /// [`Sandbox`]: trait.Sandbox.html + fn scale_factor(&self) -> f64 { + 1.0 + } + /// Runs the [`Sandbox`]. /// /// On native platforms, this method will take control of the current thread @@ -169,4 +196,12 @@ where fn view(&mut self) -> Element<'_, T::Message> { T::view(self) } + + fn background_color(&self) -> Color { + T::background_color(self) + } + + fn scale_factor(&self) -> f64 { + T::scale_factor(self) + } } diff --git a/src/settings.rs b/src/settings.rs index 01ad0ee0..d7ff4cab 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -2,7 +2,7 @@ use crate::window; /// The settings of an application. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Settings<Flags> { /// The window settings. /// @@ -22,6 +22,11 @@ pub struct Settings<Flags> { // TODO: Add `name` for web compatibility pub default_font: Option<&'static [u8]>, + /// The text size that will be used by default. + /// + /// The default value is 20. + pub default_text_size: u16, + /// If set to true, the renderer will try to perform antialiasing for some /// primitives. /// @@ -39,12 +44,28 @@ impl<Flags> Settings<Flags> { /// /// [`Application`]: ../trait.Application.html pub fn with_flags(flags: Flags) -> Self { + let default_settings = Settings::<()>::default(); + Self { flags, - // not using ..Default::default() struct update syntax since it is more permissive to - // allow initializing with flags without trait bound on Default + antialiasing: default_settings.antialiasing, + default_font: default_settings.default_font, + default_text_size: default_settings.default_text_size, + window: default_settings.window, + } + } +} + +impl<Flags> Default for Settings<Flags> +where + Flags: Default, +{ + fn default() -> Self { + Self { + flags: Default::default(), antialiasing: Default::default(), default_font: Default::default(), + default_text_size: 20, window: Default::default(), } } @@ -54,12 +75,7 @@ impl<Flags> Settings<Flags> { impl<Flags> From<Settings<Flags>> for iced_winit::Settings<Flags> { fn from(settings: Settings<Flags>) -> iced_winit::Settings<Flags> { iced_winit::Settings { - window: iced_winit::settings::Window { - size: settings.window.size, - resizable: settings.window.resizable, - decorations: settings.window.decorations, - platform_specific: Default::default(), - }, + window: settings.window.into(), flags: settings.flags, } } diff --git a/src/time.rs b/src/time.rs new file mode 100644 index 00000000..cd442461 --- /dev/null +++ b/src/time.rs @@ -0,0 +1,14 @@ +//! Listen and react to time. +use crate::Subscription; + +/// Returns a [`Subscription`] that produces messages at a set interval. +/// +/// The first message is produced after a `duration`, and then continues to +/// produce more messages every `duration` after that. +/// +/// [`Subscription`]: ../subscription/struct.Subscription.html +pub fn every( + duration: std::time::Duration, +) -> Subscription<std::time::Instant> { + iced_futures::time::every(duration) +} diff --git a/src/widget.rs b/src/widget.rs index 03e3192b..3e4d4788 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -18,29 +18,30 @@ //! [`text_input::State`]: text_input/struct.State.html #[cfg(not(target_arch = "wasm32"))] mod platform { - pub use iced_wgpu::widget::{ + pub use crate::renderer::widget::{ button, checkbox, container, pane_grid, progress_bar, radio, - scrollable, slider, text_input, + scrollable, slider, text_input, Column, Row, Space, Text, }; - #[cfg(feature = "canvas")] - #[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] - pub use iced_wgpu::widget::canvas; + #[cfg(any(feature = "canvas", feature = "glow_canvas"))] + #[cfg_attr( + docsrs, + doc(cfg(any(feature = "canvas", feature = "glow_canvas"))) + )] + pub use crate::renderer::widget::canvas; #[cfg_attr(docsrs, doc(cfg(feature = "image")))] pub mod image { //! Display images in your user interface. - pub use iced_winit::image::{Handle, Image}; + pub use crate::runtime::image::{Handle, Image}; } #[cfg_attr(docsrs, doc(cfg(feature = "svg")))] pub mod svg { //! Display vector graphics in your user interface. - pub use iced_winit::svg::{Handle, Svg}; + pub use crate::runtime::svg::{Handle, Svg}; } - pub use iced_winit::{Space, Text}; - #[doc(no_inline)] pub use { button::Button, checkbox::Checkbox, container::Container, image::Image, @@ -52,18 +53,6 @@ mod platform { #[cfg(feature = "canvas")] #[doc(no_inline)] pub use canvas::Canvas; - - /// A container that distributes its contents vertically. - /// - /// This is an alias of an `iced_native` column with a default `Renderer`. - pub type Column<'a, Message> = - iced_winit::Column<'a, Message, iced_wgpu::Renderer>; - - /// A container that distributes its contents horizontally. - /// - /// This is an alias of an `iced_native` row with a default `Renderer`. - pub type Row<'a, Message> = - iced_winit::Row<'a, Message, iced_wgpu::Renderer>; } #[cfg(target_arch = "wasm32")] diff --git a/src/window/settings.rs b/src/window/settings.rs index a31d2af2..eb997899 100644 --- a/src/window/settings.rs +++ b/src/window/settings.rs @@ -1,9 +1,15 @@ /// The window settings of an application. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Settings { - /// The size of the window. + /// The initial size of the window. pub size: (u32, u32), + /// The minimum size of the window. + pub min_size: Option<(u32, u32)>, + + /// The maximum size of the window. + pub max_size: Option<(u32, u32)>, + /// Whether the window should be resizable or not. pub resizable: bool, @@ -15,8 +21,24 @@ impl Default for Settings { fn default() -> Settings { Settings { size: (1024, 768), + min_size: None, + max_size: None, resizable: true, decorations: true, } } } + +#[cfg(not(target_arch = "wasm32"))] +impl From<Settings> for iced_winit::settings::Window { + fn from(settings: Settings) -> Self { + Self { + size: settings.size, + min_size: settings.min_size, + max_size: settings.max_size, + resizable: settings.resizable, + decorations: settings.decorations, + platform_specific: Default::default(), + } + } +} diff --git a/style/src/lib.rs b/style/src/lib.rs index 2c5977b5..72d83aec 100644 --- a/style/src/lib.rs +++ b/style/src/lib.rs @@ -2,6 +2,8 @@ //! //! It contains a set of styles and stylesheets for most of the built-in //! widgets. +pub use iced_core::{Background, Color}; + pub mod button; pub mod checkbox; pub mod container; diff --git a/web/Cargo.toml b/web/Cargo.toml index 12d3865e..88c3102f 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -19,6 +19,7 @@ dodrio = "0.1.0" wasm-bindgen = "0.2.51" wasm-bindgen-futures = "0.4" url = "2.0" +num-traits = "0.2" [dependencies.iced_core] version = "0.2" diff --git a/web/src/lib.rs b/web/src/lib.rs index 395c0a25..53b54b7e 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -74,8 +74,8 @@ pub use dodrio; pub use element::Element; pub use hasher::Hasher; pub use iced_core::{ - keyboard, Align, Background, Color, Font, HorizontalAlignment, Length, - Point, Size, Vector, VerticalAlignment, + keyboard, mouse, Align, Background, Color, Font, HorizontalAlignment, + Length, Point, Rectangle, Size, Vector, VerticalAlignment, }; pub use iced_futures::{executor, futures, Command}; pub use subscription::Subscription; diff --git a/web/src/widget/slider.rs b/web/src/widget/slider.rs index 5aa6439e..a0d9df00 100644 --- a/web/src/widget/slider.rs +++ b/web/src/widget/slider.rs @@ -16,6 +16,9 @@ use std::{ops::RangeInclusive, rc::Rc}; /// /// A [`Slider`] will try to fill the horizontal space of its container. /// +/// The [`Slider`] range of numeric values is generic and its step size defaults +/// to 1 unit. +/// /// [`Slider`]: struct.Slider.html /// /// # Example @@ -34,16 +37,20 @@ use std::{ops::RangeInclusive, rc::Rc}; /// ///  #[allow(missing_debug_implementations)] -pub struct Slider<'a, Message> { +pub struct Slider<'a, T, Message> { _state: &'a mut State, - range: RangeInclusive<f32>, - value: f32, - on_change: Rc<Box<dyn Fn(f32) -> Message>>, + range: RangeInclusive<T>, + step: T, + value: T, + on_change: Rc<Box<dyn Fn(T) -> Message>>, width: Length, style: Box<dyn StyleSheet>, } -impl<'a, Message> Slider<'a, Message> { +impl<'a, T, Message> Slider<'a, T, Message> +where + T: Copy + From<u8> + std::cmp::PartialOrd, +{ /// Creates a new [`Slider`]. /// /// It expects: @@ -58,17 +65,30 @@ impl<'a, Message> Slider<'a, Message> { /// [`State`]: struct.State.html pub fn new<F>( state: &'a mut State, - range: RangeInclusive<f32>, - value: f32, + range: RangeInclusive<T>, + value: T, on_change: F, ) -> Self where - F: 'static + Fn(f32) -> Message, + F: 'static + Fn(T) -> Message, { + let value = if value >= *range.start() { + value + } else { + *range.start() + }; + + let value = if value <= *range.end() { + value + } else { + *range.end() + }; + Slider { _state: state, - value: value.max(*range.start()).min(*range.end()), + value, range, + step: T::from(1), on_change: Rc::new(Box::new(on_change)), width: Length::Fill, style: Default::default(), @@ -90,10 +110,19 @@ impl<'a, Message> Slider<'a, Message> { self.style = style.into(); self } + + /// Sets the step size of the [`Slider`]. + /// + /// [`Slider`]: struct.Slider.html + pub fn step(mut self, step: T) -> Self { + self.step = step; + self + } } -impl<'a, Message> Widget<Message> for Slider<'a, Message> +impl<'a, T, Message> Widget<Message> for Slider<'a, T, Message> where + T: 'static + Copy + Into<f64> + num_traits::FromPrimitive, Message: 'static, { fn node<'b>( @@ -107,18 +136,18 @@ where let (start, end) = self.range.clone().into_inner(); - let min = bumpalo::format!(in bump, "{}", start); - let max = bumpalo::format!(in bump, "{}", end); - let value = bumpalo::format!(in bump, "{}", self.value); + let min = bumpalo::format!(in bump, "{}", start.into()); + let max = bumpalo::format!(in bump, "{}", end.into()); + let value = bumpalo::format!(in bump, "{}", self.value.into()); + let step = bumpalo::format!(in bump, "{}", self.step.into()); let on_change = self.on_change.clone(); let event_bus = bus.clone(); - // TODO: Make `step` configurable // TODO: Styling input(bump) .attr("type", "range") - .attr("step", "0.01") + .attr("step", step.into_bump_str()) .attr("min", min.into_bump_str()) .attr("max", max.into_bump_str()) .attr("value", value.into_bump_str()) @@ -131,19 +160,22 @@ where Some(slider) => slider, }; - if let Ok(value) = slider.value().parse::<f32>() { - event_bus.publish(on_change(value)); + if let Ok(value) = slider.value().parse::<f64>() { + if let Some(value) = T::from_f64(value) { + event_bus.publish(on_change(value)); + } } }) .finish() } } -impl<'a, Message> From<Slider<'a, Message>> for Element<'a, Message> +impl<'a, T, Message> From<Slider<'a, T, Message>> for Element<'a, Message> where + T: 'static + Copy + Into<f64> + num_traits::FromPrimitive, Message: 'static, { - fn from(slider: Slider<'a, Message>) -> Element<'a, Message> { + fn from(slider: Slider<'a, T, Message>) -> Element<'a, Message> { Element::new(slider) } } diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 0794b970..db3104c4 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_wgpu" -version = "0.2.1" +version = "0.2.2" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2018" description = "A wgpu renderer for Iced" @@ -9,40 +9,41 @@ repository = "https://github.com/hecrj/iced" [features] svg = ["resvg"] -canvas = ["lyon"] +canvas = ["iced_graphics/canvas"] +default_system_font = ["iced_graphics/font-source"] [dependencies] wgpu = "0.5" -wgpu_glyph = "0.8" +wgpu_glyph = "0.9" +glyph_brush = "0.7" zerocopy = "0.3" -glyph_brush = "0.6" +bytemuck = "1.2" raw-window-handle = "0.3" glam = "0.8" -font-kit = "0.4" log = "0.4" -guillotiere = "0.4" +guillotiere = "0.5" +# Pin `gfx-memory` until https://github.com/gfx-rs/wgpu-rs/issues/261 is +# resolved +gfx-memory = "=0.1.1" [dependencies.iced_native] version = "0.2" path = "../native" -[dependencies.iced_style] +[dependencies.iced_graphics] version = "0.1" -path = "../style" +path = "../graphics" +features = ["font-fallback", "font-icons"] [dependencies.image] -version = "0.22" +version = "0.23" optional = true [dependencies.resvg] -version = "0.8" +version = "0.9" features = ["raqote-backend"] optional = true -[dependencies.lyon] -version = "0.15" -optional = true - [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] all-features = true diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs new file mode 100644 index 00000000..a25f42f7 --- /dev/null +++ b/wgpu/src/backend.rs @@ -0,0 +1,282 @@ +use crate::quad; +use crate::text; +use crate::triangle; +use crate::{Settings, Transformation}; +use iced_graphics::backend; +use iced_graphics::font; +use iced_graphics::layer::Layer; +use iced_graphics::{Primitive, Viewport}; +use iced_native::mouse; +use iced_native::{Font, HorizontalAlignment, Size, VerticalAlignment}; + +#[cfg(any(feature = "image", feature = "svg"))] +use crate::image; + +/// A [`wgpu`] graphics backend for [`iced`]. +/// +/// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs +/// [`iced`]: https://github.com/hecrj/iced +#[derive(Debug)] +pub struct Backend { + quad_pipeline: quad::Pipeline, + text_pipeline: text::Pipeline, + triangle_pipeline: triangle::Pipeline, + + #[cfg(any(feature = "image", feature = "svg"))] + image_pipeline: image::Pipeline, + + default_text_size: u16, +} + +impl Backend { + /// Creates a new [`Backend`]. + /// + /// [`Backend`]: struct.Backend.html + pub fn new(device: &wgpu::Device, settings: Settings) -> Self { + let text_pipeline = + text::Pipeline::new(device, settings.format, settings.default_font); + let quad_pipeline = quad::Pipeline::new(device, settings.format); + let triangle_pipeline = triangle::Pipeline::new( + device, + settings.format, + settings.antialiasing, + ); + + #[cfg(any(feature = "image", feature = "svg"))] + let image_pipeline = image::Pipeline::new(device, settings.format); + + Self { + quad_pipeline, + text_pipeline, + triangle_pipeline, + + #[cfg(any(feature = "image", feature = "svg"))] + image_pipeline, + + default_text_size: settings.default_text_size, + } + } + + /// Draws the provided primitives in the given `TextureView`. + /// + /// The text provided as overlay will be rendered on top of the primitives. + /// This is useful for rendering debug information. + pub fn draw<T: AsRef<str>>( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + frame: &wgpu::TextureView, + viewport: &Viewport, + (primitive, mouse_interaction): &(Primitive, mouse::Interaction), + overlay_text: &[T], + ) -> mouse::Interaction { + log::debug!("Drawing"); + + let target_size = viewport.physical_size(); + let scale_factor = viewport.scale_factor() as f32; + let transformation = viewport.projection(); + + let mut layers = Layer::generate(primitive, viewport); + layers.push(Layer::overlay(overlay_text, viewport)); + + for layer in layers { + self.flush( + device, + scale_factor, + transformation, + &layer, + encoder, + &frame, + target_size.width, + target_size.height, + ); + } + + #[cfg(any(feature = "image", feature = "svg"))] + self.image_pipeline.trim_cache(); + + *mouse_interaction + } + + fn flush( + &mut self, + device: &wgpu::Device, + scale_factor: f32, + transformation: Transformation, + layer: &Layer<'_>, + encoder: &mut wgpu::CommandEncoder, + target: &wgpu::TextureView, + target_width: u32, + target_height: u32, + ) { + let bounds = (layer.bounds * scale_factor).snap(); + + if !layer.quads.is_empty() { + self.quad_pipeline.draw( + device, + encoder, + &layer.quads, + transformation, + scale_factor, + bounds, + target, + ); + } + + if !layer.meshes.is_empty() { + let scaled = transformation + * Transformation::scale(scale_factor, scale_factor); + + self.triangle_pipeline.draw( + device, + encoder, + target, + target_width, + target_height, + scaled, + scale_factor, + &layer.meshes, + ); + } + + #[cfg(any(feature = "image", feature = "svg"))] + { + if !layer.images.is_empty() { + let scaled = transformation + * Transformation::scale(scale_factor, scale_factor); + + self.image_pipeline.draw( + device, + encoder, + &layer.images, + scaled, + bounds, + target, + scale_factor, + ); + } + } + + if !layer.text.is_empty() { + for text in layer.text.iter() { + // Target physical coordinates directly to avoid blurry text + let text = wgpu_glyph::Section { + // TODO: We `round` here to avoid rerasterizing text when + // its position changes slightly. This can make text feel a + // bit "jumpy". We may be able to do better once we improve + // our text rendering/caching pipeline. + screen_position: ( + (text.bounds.x * scale_factor).round(), + (text.bounds.y * scale_factor).round(), + ), + // TODO: Fix precision issues with some scale factors. + // + // The `ceil` here can cause some words to render on the + // same line when they should not. + // + // Ideally, `wgpu_glyph` should be able to compute layout + // using logical positions, and then apply the proper + // scaling when rendering. This would ensure that both + // measuring and rendering follow the same layout rules. + bounds: ( + (text.bounds.width * scale_factor).ceil(), + (text.bounds.height * scale_factor).ceil(), + ), + text: vec![wgpu_glyph::Text { + text: text.content, + scale: wgpu_glyph::ab_glyph::PxScale { + x: text.size * scale_factor, + y: text.size * scale_factor, + }, + font_id: self.text_pipeline.find_font(text.font), + extra: wgpu_glyph::Extra { + color: text.color, + z: 0.0, + }, + }], + layout: wgpu_glyph::Layout::default() + .h_align(match text.horizontal_alignment { + HorizontalAlignment::Left => { + wgpu_glyph::HorizontalAlign::Left + } + HorizontalAlignment::Center => { + wgpu_glyph::HorizontalAlign::Center + } + HorizontalAlignment::Right => { + wgpu_glyph::HorizontalAlign::Right + } + }) + .v_align(match text.vertical_alignment { + VerticalAlignment::Top => { + wgpu_glyph::VerticalAlign::Top + } + VerticalAlignment::Center => { + wgpu_glyph::VerticalAlign::Center + } + VerticalAlignment::Bottom => { + wgpu_glyph::VerticalAlign::Bottom + } + }), + ..Default::default() + }; + + self.text_pipeline.queue(text); + } + + self.text_pipeline.draw_queued( + device, + encoder, + target, + transformation, + wgpu_glyph::Region { + x: bounds.x, + y: bounds.y, + width: bounds.width, + height: bounds.height, + }, + ); + } + } +} + +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::ICONS; + const CHECKMARK_ICON: char = font::CHECKMARK_ICON; + + fn default_size(&self) -> u16 { + self.default_text_size + } + + fn measure( + &self, + contents: &str, + size: f32, + font: Font, + bounds: Size, + ) -> (f32, f32) { + self.text_pipeline.measure(contents, size, font, bounds) + } +} + +#[cfg(feature = "image")] +impl backend::Image for Backend { + fn dimensions(&self, handle: &iced_native::image::Handle) -> (u32, u32) { + self.image_pipeline.dimensions(handle) + } +} + +#[cfg(feature = "svg")] +impl backend::Svg for Backend { + fn viewport_dimensions( + &self, + handle: &iced_native::svg::Handle, + ) -> (u32, u32) { + self.image_pipeline.viewport_dimensions(handle) + } +} diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs index ea5dc09d..49f1d29c 100644 --- a/wgpu/src/image.rs +++ b/wgpu/src/image.rs @@ -9,6 +9,7 @@ mod vector; use crate::Transformation; use atlas::Atlas; +use iced_graphics::layer; use iced_native::Rectangle; use std::cell::RefCell; use std::mem; @@ -282,7 +283,7 @@ impl Pipeline { &mut self, device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, - images: &[Image], + images: &[layer::Image], transformation: Transformation, bounds: Rectangle<u32>, target: &wgpu::TextureView, @@ -297,31 +298,48 @@ impl Pipeline { let mut vector_cache = self.vector_cache.borrow_mut(); for image in images { - match &image.handle { + match &image { #[cfg(feature = "image")] - Handle::Raster(handle) => { + layer::Image::Raster { handle, bounds } => { if let Some(atlas_entry) = raster_cache.upload( handle, device, encoder, &mut self.texture_atlas, ) { - add_instances(image, atlas_entry, instances); + add_instances( + [bounds.x, bounds.y], + [bounds.width, bounds.height], + atlas_entry, + instances, + ); } } + #[cfg(not(feature = "image"))] + layer::Image::Raster { .. } => {} + #[cfg(feature = "svg")] - Handle::Vector(handle) => { + layer::Image::Vector { handle, bounds } => { + let size = [bounds.width, bounds.height]; + if let Some(atlas_entry) = vector_cache.upload( handle, - image.size, + size, _scale, device, encoder, &mut self.texture_atlas, ) { - add_instances(image, atlas_entry, instances); + add_instances( + [bounds.x, bounds.y], + size, + atlas_entry, + instances, + ); } } + #[cfg(not(feature = "svg"))] + layer::Image::Vector { .. } => {} } } @@ -437,20 +455,6 @@ impl Pipeline { } } -pub struct Image { - pub handle: Handle, - pub position: [f32; 2], - pub size: [f32; 2], -} - -pub enum Handle { - #[cfg(feature = "image")] - Raster(image::Handle), - - #[cfg(feature = "svg")] - Vector(svg::Handle), -} - #[repr(C)] #[derive(Clone, Copy, AsBytes)] pub struct Vertex { @@ -495,22 +499,23 @@ struct Uniforms { } fn add_instances( - image: &Image, + image_position: [f32; 2], + image_size: [f32; 2], entry: &atlas::Entry, instances: &mut Vec<Instance>, ) { match entry { atlas::Entry::Contiguous(allocation) => { - add_instance(image.position, image.size, allocation, instances); + add_instance(image_position, image_size, allocation, instances); } atlas::Entry::Fragmented { fragments, size } => { - let scaling_x = image.size[0] / size.0 as f32; - let scaling_y = image.size[1] / size.1 as f32; + let scaling_x = image_size[0] / size.0 as f32; + let scaling_y = image_size[1] / size.1 as f32; for fragment in fragments { let allocation = &fragment.allocation; - let [x, y] = image.position; + let [x, y] = image_position; let (fragment_x, fragment_y) = fragment.position; let (fragment_width, fragment_height) = allocation.size(); diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 799c1f34..e51a225c 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -27,34 +27,31 @@ #![forbid(rust_2018_idioms)] #![cfg_attr(docsrs, feature(doc_cfg))] -pub mod defaults; pub mod settings; pub mod triangle; pub mod widget; pub mod window; -mod primitive; +mod backend; mod quad; -mod renderer; -mod target; mod text; -mod transformation; -mod viewport; +pub use iced_graphics::{Antialiasing, Color, Defaults, Primitive, Viewport}; pub use wgpu; -pub use defaults::Defaults; -pub use primitive::Primitive; -pub use renderer::Renderer; +pub use backend::Backend; pub use settings::Settings; -pub use target::Target; -pub use viewport::Viewport; #[doc(no_inline)] pub use widget::*; -pub(crate) use quad::Quad; -pub(crate) use transformation::Transformation; +pub(crate) use iced_graphics::Transformation; #[cfg(any(feature = "image", feature = "svg"))] mod image; + +/// A [`wgpu`] graphics renderer for [`iced`]. +/// +/// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs +/// [`iced`]: https://github.com/hecrj/iced +pub type Renderer = iced_graphics::Renderer<Backend>; diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs index 0c2d2244..0b62f44f 100644 --- a/wgpu/src/quad.rs +++ b/wgpu/src/quad.rs @@ -1,4 +1,5 @@ use crate::Transformation; +use iced_graphics::layer; use iced_native::Rectangle; use std::mem; @@ -107,7 +108,7 @@ impl Pipeline { }], }, wgpu::VertexBufferDescriptor { - stride: mem::size_of::<Quad>() as u64, + stride: mem::size_of::<layer::Quad>() as u64, step_mode: wgpu::InputStepMode::Instance, attributes: &[ wgpu::VertexAttributeDescriptor { @@ -161,7 +162,7 @@ impl Pipeline { let instances = device.create_buffer(&wgpu::BufferDescriptor { label: None, - size: mem::size_of::<Quad>() as u64 * Quad::MAX as u64, + size: mem::size_of::<layer::Quad>() as u64 * MAX_INSTANCES as u64, usage: wgpu::BufferUsage::VERTEX | wgpu::BufferUsage::COPY_DST, }); @@ -179,7 +180,7 @@ impl Pipeline { &mut self, device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, - instances: &[Quad], + instances: &[layer::Quad], transformation: Transformation, scale: f32, bounds: Rectangle<u32>, @@ -204,11 +205,11 @@ impl Pipeline { let total = instances.len(); while i < total { - let end = (i + Quad::MAX).min(total); + let end = (i + MAX_INSTANCES).min(total); let amount = end - i; let instance_buffer = device.create_buffer_with_data( - &instances[i..end].as_bytes(), + bytemuck::cast_slice(&instances[i..end]), wgpu::BufferUsage::COPY_SRC, ); @@ -217,7 +218,7 @@ impl Pipeline { 0, &self.instances, 0, - (mem::size_of::<Quad>() * amount) as u64, + (mem::size_of::<layer::Quad>() * amount) as u64, ); { @@ -260,7 +261,7 @@ impl Pipeline { ); } - i += Quad::MAX; + i += MAX_INSTANCES; } } } @@ -288,20 +289,7 @@ const QUAD_VERTS: [Vertex; 4] = [ }, ]; -#[repr(C)] -#[derive(Debug, Clone, Copy, AsBytes)] -pub struct Quad { - pub position: [f32; 2], - pub scale: [f32; 2], - pub color: [f32; 4], - pub border_color: [f32; 4], - pub border_radius: f32, - pub border_width: f32, -} - -impl Quad { - const MAX: usize = 100_000; -} +const MAX_INSTANCES: usize = 100_000; #[repr(C)] #[derive(Debug, Clone, Copy, AsBytes)] diff --git a/wgpu/src/renderer.rs b/wgpu/src/renderer.rs deleted file mode 100644 index 481b310c..00000000 --- a/wgpu/src/renderer.rs +++ /dev/null @@ -1,492 +0,0 @@ -use crate::{ - quad, text, triangle, Defaults, Primitive, Quad, Settings, Target, - Transformation, -}; - -#[cfg(any(feature = "image", feature = "svg"))] -use crate::image::{self, Image}; - -use iced_native::{ - layout, Background, Color, Layout, MouseCursor, Point, Rectangle, Vector, - Widget, -}; - -mod widget; - -/// A [`wgpu`] renderer. -/// -/// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs -#[derive(Debug)] -pub struct Renderer { - quad_pipeline: quad::Pipeline, - text_pipeline: text::Pipeline, - triangle_pipeline: triangle::Pipeline, - - #[cfg(any(feature = "image", feature = "svg"))] - image_pipeline: image::Pipeline, -} - -struct Layer<'a> { - bounds: Rectangle<u32>, - quads: Vec<Quad>, - meshes: Vec<(Point, &'a triangle::Mesh2D)>, - text: Vec<wgpu_glyph::Section<'a>>, - - #[cfg(any(feature = "image", feature = "svg"))] - images: Vec<Image>, -} - -impl<'a> Layer<'a> { - pub fn new(bounds: Rectangle<u32>) -> Self { - Self { - bounds, - quads: Vec::new(), - text: Vec::new(), - meshes: Vec::new(), - - #[cfg(any(feature = "image", feature = "svg"))] - images: Vec::new(), - } - } -} - -impl Renderer { - /// Creates a new [`Renderer`]. - /// - /// [`Renderer`]: struct.Renderer.html - pub fn new(device: &wgpu::Device, settings: Settings) -> Self { - let text_pipeline = - text::Pipeline::new(device, settings.format, settings.default_font); - let quad_pipeline = quad::Pipeline::new(device, settings.format); - let triangle_pipeline = triangle::Pipeline::new( - device, - settings.format, - settings.antialiasing, - ); - - #[cfg(any(feature = "image", feature = "svg"))] - let image_pipeline = image::Pipeline::new(device, settings.format); - - Self { - quad_pipeline, - text_pipeline, - triangle_pipeline, - - #[cfg(any(feature = "image", feature = "svg"))] - image_pipeline, - } - } - - /// Draws the provided primitives in the given [`Target`]. - /// - /// The text provided as overlay will be renderer on top of the primitives. - /// This is useful for rendering debug information. - /// - /// [`Target`]: struct.Target.html - pub fn draw<T: AsRef<str>>( - &mut self, - device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - target: Target<'_>, - (primitive, mouse_cursor): &(Primitive, MouseCursor), - scale_factor: f64, - overlay: &[T], - ) -> MouseCursor { - log::debug!("Drawing"); - - let (width, height) = target.viewport.dimensions(); - let scale_factor = scale_factor as f32; - let transformation = target.viewport.transformation(); - - let mut layers = Vec::new(); - - layers.push(Layer::new(Rectangle { - x: 0, - y: 0, - width, - height, - })); - - self.draw_primitive(Vector::new(0.0, 0.0), primitive, &mut layers); - self.draw_overlay(overlay, &mut layers); - - for layer in layers { - self.flush( - device, - scale_factor, - transformation, - &layer, - encoder, - target.texture, - width, - height, - ); - } - - #[cfg(any(feature = "image", feature = "svg"))] - self.image_pipeline.trim_cache(); - - *mouse_cursor - } - - fn draw_primitive<'a>( - &mut self, - translation: Vector, - primitive: &'a Primitive, - layers: &mut Vec<Layer<'a>>, - ) { - match primitive { - Primitive::None => {} - Primitive::Group { primitives } => { - // TODO: Inspect a bit and regroup (?) - for primitive in primitives { - self.draw_primitive(translation, primitive, layers) - } - } - Primitive::Text { - content, - bounds, - size, - color, - font, - horizontal_alignment, - vertical_alignment, - } => { - let layer = layers.last_mut().unwrap(); - - layer.text.push(wgpu_glyph::Section { - text: &content, - screen_position: ( - bounds.x + translation.x, - bounds.y + translation.y, - ), - bounds: (bounds.width, bounds.height), - scale: wgpu_glyph::Scale { x: *size, y: *size }, - color: color.into_linear(), - font_id: self.text_pipeline.find_font(*font), - layout: wgpu_glyph::Layout::default() - .h_align(match horizontal_alignment { - iced_native::HorizontalAlignment::Left => { - wgpu_glyph::HorizontalAlign::Left - } - iced_native::HorizontalAlignment::Center => { - wgpu_glyph::HorizontalAlign::Center - } - iced_native::HorizontalAlignment::Right => { - wgpu_glyph::HorizontalAlign::Right - } - }) - .v_align(match vertical_alignment { - iced_native::VerticalAlignment::Top => { - wgpu_glyph::VerticalAlign::Top - } - iced_native::VerticalAlignment::Center => { - wgpu_glyph::VerticalAlign::Center - } - iced_native::VerticalAlignment::Bottom => { - wgpu_glyph::VerticalAlign::Bottom - } - }), - ..Default::default() - }) - } - Primitive::Quad { - bounds, - background, - border_radius, - border_width, - border_color, - } => { - let layer = layers.last_mut().unwrap(); - - // TODO: Move some of these computations to the GPU (?) - layer.quads.push(Quad { - position: [ - bounds.x + translation.x, - bounds.y + translation.y, - ], - scale: [bounds.width, bounds.height], - color: match background { - Background::Color(color) => color.into_linear(), - }, - border_radius: *border_radius as f32, - border_width: *border_width as f32, - border_color: border_color.into_linear(), - }); - } - Primitive::Mesh2D { origin, buffers } => { - let layer = layers.last_mut().unwrap(); - - layer.meshes.push((*origin + translation, buffers)); - } - Primitive::Clip { - bounds, - offset, - content, - } => { - let layer = layers.last_mut().unwrap(); - - let layer_bounds: Rectangle<f32> = layer.bounds.into(); - - let clip = Rectangle { - x: bounds.x + translation.x, - y: bounds.y + translation.y, - ..*bounds - }; - - // Only draw visible content - if let Some(clip_bounds) = layer_bounds.intersection(&clip) { - let clip_layer = Layer::new(clip_bounds.into()); - let new_layer = Layer::new(layer.bounds); - - layers.push(clip_layer); - self.draw_primitive( - translation - - Vector::new(offset.x as f32, offset.y as f32), - content, - layers, - ); - layers.push(new_layer); - } - } - - Primitive::Cached { origin, cache } => { - self.draw_primitive( - translation + Vector::new(origin.x, origin.y), - &cache, - layers, - ); - } - - #[cfg(feature = "image")] - Primitive::Image { handle, bounds } => { - let layer = layers.last_mut().unwrap(); - - layer.images.push(Image { - handle: image::Handle::Raster(handle.clone()), - position: [ - bounds.x + translation.x, - bounds.y + translation.y, - ], - size: [bounds.width, bounds.height], - }); - } - #[cfg(not(feature = "image"))] - Primitive::Image { .. } => {} - - #[cfg(feature = "svg")] - Primitive::Svg { handle, bounds } => { - let layer = layers.last_mut().unwrap(); - - layer.images.push(Image { - handle: image::Handle::Vector(handle.clone()), - position: [ - bounds.x + translation.x, - bounds.y + translation.y, - ], - size: [bounds.width, bounds.height], - }); - } - #[cfg(not(feature = "svg"))] - Primitive::Svg { .. } => {} - } - } - - fn draw_overlay<'a, T: AsRef<str>>( - &mut self, - lines: &'a [T], - layers: &mut Vec<Layer<'a>>, - ) { - let first = layers.first().unwrap(); - let mut overlay = Layer::new(first.bounds); - - let font_id = self.text_pipeline.overlay_font(); - let scale = wgpu_glyph::Scale { x: 20.0, y: 20.0 }; - - for (i, line) in lines.iter().enumerate() { - overlay.text.push(wgpu_glyph::Section { - text: line.as_ref(), - screen_position: (11.0, 11.0 + 25.0 * i as f32), - color: [0.9, 0.9, 0.9, 1.0], - scale, - font_id, - ..wgpu_glyph::Section::default() - }); - - overlay.text.push(wgpu_glyph::Section { - text: line.as_ref(), - screen_position: (10.0, 10.0 + 25.0 * i as f32), - color: [0.0, 0.0, 0.0, 1.0], - scale, - font_id, - ..wgpu_glyph::Section::default() - }); - } - - layers.push(overlay); - } - - fn flush( - &mut self, - device: &wgpu::Device, - scale_factor: f32, - transformation: Transformation, - layer: &Layer<'_>, - encoder: &mut wgpu::CommandEncoder, - target: &wgpu::TextureView, - target_width: u32, - target_height: u32, - ) { - let bounds = layer.bounds * scale_factor; - - if !layer.meshes.is_empty() { - let scaled = transformation - * Transformation::scale(scale_factor, scale_factor); - - self.triangle_pipeline.draw( - device, - encoder, - target, - target_width, - target_height, - scaled, - &layer.meshes, - bounds, - ); - } - - if !layer.quads.is_empty() { - self.quad_pipeline.draw( - device, - encoder, - &layer.quads, - transformation, - scale_factor, - bounds, - target, - ); - } - - #[cfg(any(feature = "image", feature = "svg"))] - { - if !layer.images.is_empty() { - let scaled = transformation - * Transformation::scale(scale_factor, scale_factor); - - self.image_pipeline.draw( - device, - encoder, - &layer.images, - scaled, - bounds, - target, - scale_factor, - ); - } - } - - if !layer.text.is_empty() { - for text in layer.text.iter() { - // Target physical coordinates directly to avoid blurry text - let text = wgpu_glyph::Section { - // TODO: We `round` here to avoid rerasterizing text when - // its position changes slightly. This can make text feel a - // bit "jumpy". We may be able to do better once we improve - // our text rendering/caching pipeline. - screen_position: ( - (text.screen_position.0 * scale_factor).round(), - (text.screen_position.1 * scale_factor).round(), - ), - // TODO: Fix precision issues with some scale factors. - // - // The `ceil` here can cause some words to render on the - // same line when they should not. - // - // Ideally, `wgpu_glyph` should be able to compute layout - // using logical positions, and then apply the proper - // scaling when rendering. This would ensure that both - // measuring and rendering follow the same layout rules. - bounds: ( - (text.bounds.0 * scale_factor).ceil(), - (text.bounds.1 * scale_factor).ceil(), - ), - scale: wgpu_glyph::Scale { - x: text.scale.x * scale_factor, - y: text.scale.y * scale_factor, - }, - ..*text - }; - - self.text_pipeline.queue(text); - } - - self.text_pipeline.draw_queued( - device, - encoder, - target, - transformation, - wgpu_glyph::Region { - x: bounds.x, - y: bounds.y, - width: bounds.width, - height: bounds.height, - }, - ); - } - } -} - -impl iced_native::Renderer for Renderer { - type Output = (Primitive, MouseCursor); - type Defaults = Defaults; - - fn layout<'a, Message>( - &mut self, - element: &iced_native::Element<'a, Message, Self>, - limits: &iced_native::layout::Limits, - ) -> iced_native::layout::Node { - let node = element.layout(self, limits); - - self.text_pipeline.clear_measurement_cache(); - - node - } -} - -impl layout::Debugger for Renderer { - fn explain<Message>( - &mut self, - defaults: &Defaults, - widget: &dyn Widget<Message, Self>, - layout: Layout<'_>, - cursor_position: Point, - color: Color, - ) -> Self::Output { - let mut primitives = Vec::new(); - let (primitive, cursor) = - widget.draw(self, defaults, layout, cursor_position); - - explain_layout(layout, color, &mut primitives); - primitives.push(primitive); - - (Primitive::Group { primitives }, cursor) - } -} - -fn explain_layout( - layout: Layout<'_>, - color: Color, - primitives: &mut Vec<Primitive>, -) { - primitives.push(Primitive::Quad { - bounds: layout.bounds(), - background: Background::Color(Color::TRANSPARENT), - border_radius: 0, - border_width: 1, - border_color: [0.6, 0.6, 0.6, 0.5].into(), - }); - - for child in layout.children() { - explain_layout(child, color, primitives); - } -} diff --git a/wgpu/src/renderer/widget.rs b/wgpu/src/renderer/widget.rs deleted file mode 100644 index 37421fbe..00000000 --- a/wgpu/src/renderer/widget.rs +++ /dev/null @@ -1,19 +0,0 @@ -mod button; -mod checkbox; -mod column; -mod container; -mod pane_grid; -mod progress_bar; -mod radio; -mod row; -mod scrollable; -mod slider; -mod space; -mod text; -mod text_input; - -#[cfg(feature = "svg")] -mod svg; - -#[cfg(feature = "image")] -mod image; diff --git a/wgpu/src/renderer/widget/column.rs b/wgpu/src/renderer/widget/column.rs deleted file mode 100644 index 95a7463a..00000000 --- a/wgpu/src/renderer/widget/column.rs +++ /dev/null @@ -1,34 +0,0 @@ -use crate::{Primitive, Renderer}; -use iced_native::{column, Element, Layout, MouseCursor, Point}; - -impl column::Renderer for Renderer { - fn draw<Message>( - &mut self, - defaults: &Self::Defaults, - content: &[Element<'_, Message, Self>], - layout: Layout<'_>, - cursor_position: Point, - ) -> Self::Output { - let mut mouse_cursor = MouseCursor::OutOfBounds; - - ( - Primitive::Group { - primitives: content - .iter() - .zip(layout.children()) - .map(|(child, layout)| { - let (primitive, new_mouse_cursor) = - child.draw(self, defaults, layout, cursor_position); - - if new_mouse_cursor > mouse_cursor { - mouse_cursor = new_mouse_cursor; - } - - primitive - }) - .collect(), - }, - mouse_cursor, - ) - } -} diff --git a/wgpu/src/renderer/widget/image.rs b/wgpu/src/renderer/widget/image.rs deleted file mode 100644 index 70dc5d97..00000000 --- a/wgpu/src/renderer/widget/image.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::{Primitive, Renderer}; -use iced_native::{image, Layout, MouseCursor}; - -impl image::Renderer for Renderer { - fn dimensions(&self, handle: &image::Handle) -> (u32, u32) { - self.image_pipeline.dimensions(handle) - } - - fn draw( - &mut self, - handle: image::Handle, - layout: Layout<'_>, - ) -> Self::Output { - ( - Primitive::Image { - handle, - bounds: layout.bounds(), - }, - MouseCursor::OutOfBounds, - ) - } -} diff --git a/wgpu/src/renderer/widget/row.rs b/wgpu/src/renderer/widget/row.rs deleted file mode 100644 index bd9f1a04..00000000 --- a/wgpu/src/renderer/widget/row.rs +++ /dev/null @@ -1,34 +0,0 @@ -use crate::{Primitive, Renderer}; -use iced_native::{row, Element, Layout, MouseCursor, Point}; - -impl row::Renderer for Renderer { - fn draw<Message>( - &mut self, - defaults: &Self::Defaults, - children: &[Element<'_, Message, Self>], - layout: Layout<'_>, - cursor_position: Point, - ) -> Self::Output { - let mut mouse_cursor = MouseCursor::OutOfBounds; - - ( - Primitive::Group { - primitives: children - .iter() - .zip(layout.children()) - .map(|(child, layout)| { - let (primitive, new_mouse_cursor) = - child.draw(self, defaults, layout, cursor_position); - - if new_mouse_cursor > mouse_cursor { - mouse_cursor = new_mouse_cursor; - } - - primitive - }) - .collect(), - }, - mouse_cursor, - ) - } -} diff --git a/wgpu/src/renderer/widget/space.rs b/wgpu/src/renderer/widget/space.rs deleted file mode 100644 index 28e05437..00000000 --- a/wgpu/src/renderer/widget/space.rs +++ /dev/null @@ -1,8 +0,0 @@ -use crate::{Primitive, Renderer}; -use iced_native::{space, MouseCursor, Rectangle}; - -impl space::Renderer for Renderer { - fn draw(&mut self, _bounds: Rectangle) -> Self::Output { - (Primitive::None, MouseCursor::OutOfBounds) - } -} diff --git a/wgpu/src/settings.rs b/wgpu/src/settings.rs index 5ee245b6..bc146c4c 100644 --- a/wgpu/src/settings.rs +++ b/wgpu/src/settings.rs @@ -1,6 +1,5 @@ -//! Configure a [`Renderer`]. -//! -//! [`Renderer`]: struct.Renderer.html +//! Configure a renderer. +pub use crate::Antialiasing; /// The settings of a [`Renderer`]. /// @@ -17,6 +16,11 @@ pub struct Settings { /// If `None` is provided, a default system font will be chosen. pub default_font: Option<&'static [u8]>, + /// The default size of text. + /// + /// By default, it will be set to 20. + pub default_text_size: u16, + /// The antialiasing strategy that will be used for triangle primitives. pub antialiasing: Option<Antialiasing>, } @@ -26,31 +30,8 @@ impl Default for Settings { Settings { format: wgpu::TextureFormat::Bgra8UnormSrgb, default_font: None, + default_text_size: 20, antialiasing: None, } } } - -/// An antialiasing strategy. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Antialiasing { - /// Multisample AA with 2 samples - MSAAx2, - /// Multisample AA with 4 samples - MSAAx4, - /// Multisample AA with 8 samples - MSAAx8, - /// Multisample AA with 16 samples - MSAAx16, -} - -impl Antialiasing { - pub(crate) fn sample_count(self) -> u32 { - match self { - Antialiasing::MSAAx2 => 2, - Antialiasing::MSAAx4 => 4, - Antialiasing::MSAAx8 => 8, - Antialiasing::MSAAx16 => 16, - } - } -} diff --git a/wgpu/src/target.rs b/wgpu/src/target.rs deleted file mode 100644 index 1e72c0c3..00000000 --- a/wgpu/src/target.rs +++ /dev/null @@ -1,14 +0,0 @@ -use crate::Viewport; - -/// A rendering target. -#[derive(Debug)] -pub struct Target<'a> { - /// The texture where graphics will be rendered. - pub texture: &'a wgpu::TextureView, - - /// The viewport of the target. - /// - /// Most of the time, you will want this to match the dimensions of the - /// texture. - pub viewport: &'a Viewport, -} diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index f4521e72..a7123d39 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -1,24 +1,13 @@ -mod font; - use crate::Transformation; - +use iced_graphics::font; use std::{cell::RefCell, collections::HashMap}; - -pub const BUILTIN_ICONS: iced_native::Font = iced_native::Font::External { - name: "iced_wgpu icons", - bytes: include_bytes!("text/icons.ttf"), -}; - -pub const CHECKMARK_ICON: char = '\u{F00C}'; - -const FALLBACK_FONT: &[u8] = include_bytes!("../fonts/Lato-Regular.ttf"); +use wgpu_glyph::ab_glyph; #[derive(Debug)] pub struct Pipeline { - draw_brush: RefCell<wgpu_glyph::GlyphBrush<'static, ()>>, + draw_brush: RefCell<wgpu_glyph::GlyphBrush<()>>, draw_font_map: RefCell<HashMap<String, wgpu_glyph::FontId>>, - - measure_brush: RefCell<glyph_brush::GlyphBrush<'static, ()>>, + measure_brush: RefCell<glyph_brush::GlyphBrush<()>>, } impl Pipeline { @@ -27,51 +16,48 @@ impl Pipeline { format: wgpu::TextureFormat, default_font: Option<&[u8]>, ) -> Self { - // TODO: Font customization - let font_source = font::Source::new(); + let default_font = default_font.map(|slice| slice.to_vec()); - let default_font = - default_font.map(|slice| slice.to_vec()).unwrap_or_else(|| { - font_source + // TODO: Font customization + #[cfg(feature = "default_system_font")] + let default_font = { + default_font.or_else(|| { + font::Source::new() .load(&[font::Family::SansSerif, font::Family::Serif]) - .unwrap_or_else(|_| FALLBACK_FONT.to_vec()) - }); - - let load_glyph_brush = |font: Vec<u8>| { - let builder = - wgpu_glyph::GlyphBrushBuilder::using_fonts_bytes(vec![ - font.clone() - ])?; - - Ok(( - builder, - glyph_brush::GlyphBrushBuilder::using_font_bytes(font).build(), - )) + .ok() + }) }; - let (brush_builder, measure_brush) = load_glyph_brush(default_font) - .unwrap_or_else(|_: wgpu_glyph::rusttype::Error| { - log::warn!("System font failed to load. Falling back to embedded font..."); + let default_font = + default_font.unwrap_or_else(|| font::FALLBACK.to_vec()); - load_glyph_brush(FALLBACK_FONT.to_vec()).expect("Load fallback font") + let font = ab_glyph::FontArc::try_from_vec(default_font) + .unwrap_or_else(|_| { + log::warn!( + "System font failed to load. Falling back to \ + embedded font..." + ); + + ab_glyph::FontArc::try_from_slice(font::FALLBACK) + .expect("Load fallback font") }); - let draw_brush = brush_builder - .initial_cache_size((2048, 2048)) - .build(device, format); + let draw_brush = + wgpu_glyph::GlyphBrushBuilder::using_font(font.clone()) + .initial_cache_size((2048, 2048)) + .draw_cache_multithread(false) // TODO: Expose as a configuration flag + .build(device, format); + + let measure_brush = + glyph_brush::GlyphBrushBuilder::using_font(font).build(); Pipeline { draw_brush: RefCell::new(draw_brush), draw_font_map: RefCell::new(HashMap::new()), - measure_brush: RefCell::new(measure_brush), } } - pub fn overlay_font(&self) -> wgpu_glyph::FontId { - wgpu_glyph::FontId(0) - } - pub fn queue(&mut self, section: wgpu_glyph::Section<'_>) { self.draw_brush.borrow_mut().queue(section); } @@ -108,10 +94,13 @@ impl Pipeline { let wgpu_glyph::FontId(font_id) = self.find_font(font); let section = wgpu_glyph::Section { - text: content, - scale: wgpu_glyph::Scale { x: size, y: size }, bounds: (bounds.width, bounds.height), - font_id: wgpu_glyph::FontId(font_id), + text: vec![wgpu_glyph::Text { + text: content, + scale: size.into(), + font_id: wgpu_glyph::FontId(font_id), + extra: wgpu_glyph::Extra::default(), + }], ..Default::default() }; @@ -124,21 +113,7 @@ impl Pipeline { } } - pub fn space_width(&self, size: f32) -> f32 { - use wgpu_glyph::GlyphCruncher; - - let glyph_brush = self.measure_brush.borrow(); - - // TODO: Select appropriate font - let font = &glyph_brush.fonts()[0]; - - font.glyph(' ') - .scaled(wgpu_glyph::Scale { x: size, y: size }) - .h_metrics() - .advance_width - } - - pub fn clear_measurement_cache(&mut self) { + pub fn trim_measurement_cache(&mut self) { // TODO: We should probably use a `GlyphCalculator` for this. However, // it uses a lifetimed `GlyphCalculatorGuard` with side-effects on drop. // This makes stuff quite inconvenient. A manual method for trimming the @@ -170,11 +145,12 @@ impl Pipeline { return *font_id; } - // TODO: Find a way to share font data - let _ = self.measure_brush.borrow_mut().add_font_bytes(bytes); + let font = ab_glyph::FontArc::try_from_slice(bytes) + .expect("Load font"); + + let _ = self.measure_brush.borrow_mut().add_font(font.clone()); - let font_id = - self.draw_brush.borrow_mut().add_font_bytes(bytes); + let font_id = self.draw_brush.borrow_mut().add_font(font); let _ = self .draw_font_map diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index 86c74fcd..2744c67a 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -1,19 +1,22 @@ //! Draw meshes of triangles. use crate::{settings, Transformation}; -use iced_native::{Point, Rectangle}; +use iced_graphics::layer; use std::mem; use zerocopy::AsBytes; +pub use iced_graphics::triangle::{Mesh2D, Vertex2D}; + mod msaa; const UNIFORM_BUFFER_SIZE: usize = 100; -const VERTEX_BUFFER_SIZE: usize = 100_000; -const INDEX_BUFFER_SIZE: usize = 100_000; +const VERTEX_BUFFER_SIZE: usize = 10_000; +const INDEX_BUFFER_SIZE: usize = 10_000; #[derive(Debug)] pub(crate) struct Pipeline { pipeline: wgpu::RenderPipeline, blit: Option<msaa::Blit>, + constants_layout: wgpu::BindGroupLayout, constants: wgpu::BindGroup, uniforms_buffer: Buffer<Uniforms>, vertex_buffer: Buffer<Vertex2D>, @@ -48,8 +51,10 @@ impl<T> Buffer<T> { } } - pub fn ensure_capacity(&mut self, device: &wgpu::Device, size: usize) { - if self.size < size { + pub fn expand(&mut self, device: &wgpu::Device, size: usize) -> bool { + let needs_resize = self.size < size; + + if needs_resize { self.raw = device.create_buffer(&wgpu::BufferDescriptor { label: None, size: (std::mem::size_of::<T>() * size) as u64, @@ -58,6 +63,8 @@ impl<T> Buffer<T> { self.size = size; } + + needs_resize } } @@ -67,7 +74,7 @@ impl Pipeline { format: wgpu::TextureFormat, antialiasing: Option<settings::Antialiasing>, ) -> Pipeline { - let constant_layout = + let constants_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: None, bindings: &[wgpu::BindGroupLayoutEntry { @@ -86,7 +93,7 @@ impl Pipeline { let constant_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: None, - layout: &constant_layout, + layout: &constants_layout, bindings: &[wgpu::Binding { binding: 0, resource: wgpu::BindingResource::Buffer { @@ -98,7 +105,7 @@ impl Pipeline { let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - bind_group_layouts: &[&constant_layout], + bind_group_layouts: &[&constants_layout], }); let vs = include_bytes!("shader/triangle.vert.spv"); @@ -168,9 +175,9 @@ impl Pipeline { ], }], }, - sample_count: antialiasing - .map(|a| a.sample_count()) - .unwrap_or(1), + sample_count: u32::from( + antialiasing.map(|a| a.sample_count()).unwrap_or(1), + ), sample_mask: !0, alpha_to_coverage_enabled: false, }); @@ -178,6 +185,7 @@ impl Pipeline { Pipeline { pipeline, blit: antialiasing.map(|a| msaa::Blit::new(device, format, a)), + constants_layout, constants: constant_bind_group, uniforms_buffer: constants_buffer, vertex_buffer: Buffer::new( @@ -201,24 +209,42 @@ impl Pipeline { target_width: u32, target_height: u32, transformation: Transformation, - meshes: &[(Point, &Mesh2D)], - bounds: Rectangle<u32>, + scale_factor: f32, + meshes: &[layer::Mesh<'_>], ) { // This looks a bit crazy, but we are just counting how many vertices // and indices we will need to handle. // TODO: Improve readability let (total_vertices, total_indices) = meshes .iter() - .map(|(_, mesh)| (mesh.vertices.len(), mesh.indices.len())) + .map(|layer::Mesh { buffers, .. }| { + (buffers.vertices.len(), buffers.indices.len()) + }) .fold((0, 0), |(total_v, total_i), (v, i)| { (total_v + v, total_i + i) }); // Then we ensure the current buffers are big enough, resizing if // necessary - self.uniforms_buffer.ensure_capacity(device, meshes.len()); - self.vertex_buffer.ensure_capacity(device, total_vertices); - self.index_buffer.ensure_capacity(device, total_indices); + let _ = self.vertex_buffer.expand(device, total_vertices); + let _ = self.index_buffer.expand(device, total_indices); + + // If the uniforms buffer is resized, then we need to recreate its + // bind group. + if self.uniforms_buffer.expand(device, meshes.len()) { + self.constants = + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: None, + layout: &self.constants_layout, + bindings: &[wgpu::Binding { + binding: 0, + resource: wgpu::BindingResource::Buffer { + buffer: &self.uniforms_buffer.raw, + range: 0..std::mem::size_of::<Uniforms>() as u64, + }, + }], + }); + } let mut uniforms: Vec<Uniforms> = Vec::with_capacity(meshes.len()); let mut offsets: Vec<( @@ -230,20 +256,18 @@ impl Pipeline { let mut last_index = 0; // We upload everything upfront - for (origin, mesh) in meshes { - let transform = Uniforms { - transform: (transformation - * Transformation::translate(origin.x, origin.y)) - .into(), - }; + for mesh in meshes { + let transform = (transformation + * Transformation::translate(mesh.origin.x, mesh.origin.y)) + .into(); let vertex_buffer = device.create_buffer_with_data( - mesh.vertices.as_bytes(), + bytemuck::cast_slice(&mesh.buffers.vertices), wgpu::BufferUsage::COPY_SRC, ); let index_buffer = device.create_buffer_with_data( - mesh.indices.as_bytes(), + mesh.buffers.indices.as_bytes(), wgpu::BufferUsage::COPY_SRC, ); @@ -252,7 +276,8 @@ impl Pipeline { 0, &self.vertex_buffer.raw, (std::mem::size_of::<Vertex2D>() * last_vertex) as u64, - (std::mem::size_of::<Vertex2D>() * mesh.vertices.len()) as u64, + (std::mem::size_of::<Vertex2D>() * mesh.buffers.vertices.len()) + as u64, ); encoder.copy_buffer_to_buffer( @@ -260,18 +285,19 @@ impl Pipeline { 0, &self.index_buffer.raw, (std::mem::size_of::<u32>() * last_index) as u64, - (std::mem::size_of::<u32>() * mesh.indices.len()) as u64, + (std::mem::size_of::<u32>() * mesh.buffers.indices.len()) + as u64, ); uniforms.push(transform); offsets.push(( last_vertex as u64, last_index as u64, - mesh.indices.len(), + mesh.buffers.indices.len(), )); - last_vertex += mesh.vertices.len(); - last_index += mesh.indices.len(); + last_vertex += mesh.buffers.vertices.len(); + last_index += mesh.buffers.indices.len(); } let uniforms_buffer = device.create_buffer_with_data( @@ -318,16 +344,19 @@ impl Pipeline { }); render_pass.set_pipeline(&self.pipeline); - render_pass.set_scissor_rect( - bounds.x, - bounds.y, - bounds.width, - bounds.height, - ); for (i, (vertex_offset, index_offset, indices)) in offsets.into_iter().enumerate() { + let clip_bounds = (meshes[i].clip_bounds * scale_factor).snap(); + + render_pass.set_scissor_rect( + clip_bounds.x, + clip_bounds.y, + clip_bounds.width, + clip_bounds.height, + ); + render_pass.set_bind_group( 0, &self.constants, @@ -361,35 +390,28 @@ impl Pipeline { #[derive(Debug, Clone, Copy, AsBytes)] struct Uniforms { transform: [f32; 16], + // We need to align this to 256 bytes to please `wgpu`... + // TODO: Be smarter and stop wasting memory! + _padding_a: [f32; 32], + _padding_b: [f32; 16], } impl Default for Uniforms { fn default() -> Self { Self { transform: *Transformation::identity().as_ref(), + _padding_a: [0.0; 32], + _padding_b: [0.0; 16], } } } -/// A two-dimensional vertex with some color in __linear__ RGBA. -#[repr(C)] -#[derive(Copy, Clone, Debug, AsBytes)] -pub struct Vertex2D { - /// The vertex position - pub position: [f32; 2], - /// The vertex color in __linear__ RGBA. - pub color: [f32; 4], -} - -/// A set of [`Vertex2D`] and indices representing a list of triangles. -/// -/// [`Vertex2D`]: struct.Vertex2D.html -#[derive(Clone, Debug)] -pub struct Mesh2D { - /// The vertices of the mesh - pub vertices: Vec<Vertex2D>, - /// The list of vertex indices that defines the triangles of the mesh. - /// - /// Therefore, this list should always have a length that is a multiple of 3. - pub indices: Vec<u32>, +impl From<Transformation> for Uniforms { + fn from(transformation: Transformation) -> Uniforms { + Self { + transform: transformation.into(), + _padding_a: [0.0; 32], + _padding_b: [0.0; 16], + } + } } diff --git a/wgpu/src/viewport.rs b/wgpu/src/viewport.rs deleted file mode 100644 index 66242468..00000000 --- a/wgpu/src/viewport.rs +++ /dev/null @@ -1,29 +0,0 @@ -use crate::Transformation; - -/// A viewing region for displaying computer graphics. -#[derive(Debug)] -pub struct Viewport { - width: u32, - height: u32, - transformation: Transformation, -} - -impl Viewport { - /// Creates a new [`Viewport`] with the given dimensions. - pub fn new(width: u32, height: u32) -> Viewport { - Viewport { - width, - height, - transformation: Transformation::orthographic(width, height), - } - } - - /// Returns the dimensions of the [`Viewport`]. - pub fn dimensions(&self) -> (u32, u32) { - (self.width, self.height) - } - - pub(crate) fn transformation(&self) -> Transformation { - self.transformation - } -} diff --git a/wgpu/src/widget.rs b/wgpu/src/widget.rs index c3a47dff..d17b7a5d 100644 --- a/wgpu/src/widget.rs +++ b/wgpu/src/widget.rs @@ -7,6 +7,8 @@ //! ``` //! use iced_wgpu::{button, Button}; //! ``` +use crate::Renderer; + pub mod button; pub mod checkbox; pub mod container; @@ -43,3 +45,14 @@ pub mod canvas; #[cfg(feature = "canvas")] #[doc(no_inline)] pub use canvas::Canvas; + +pub use iced_native::Space; + +/// A container that distributes its contents vertically. +pub type Column<'a, Message> = iced_native::Column<'a, Message, Renderer>; + +/// A container that distributes its contents horizontally. +pub type Row<'a, Message> = iced_native::Row<'a, Message, Renderer>; + +/// A paragraph of text. +pub type Text = iced_native::Text<Renderer>; diff --git a/wgpu/src/widget/button.rs b/wgpu/src/widget/button.rs index b738c55e..fee7a7f8 100644 --- a/wgpu/src/widget/button.rs +++ b/wgpu/src/widget/button.rs @@ -6,8 +6,8 @@ //! [`State`]: struct.State.html use crate::Renderer; +pub use iced_graphics::button::{Style, StyleSheet}; pub use iced_native::button::State; -pub use iced_style::button::{Style, StyleSheet}; /// A widget that produces a message when clicked. /// diff --git a/wgpu/src/widget/canvas.rs b/wgpu/src/widget/canvas.rs index 325f90ce..bef34857 100644 --- a/wgpu/src/widget/canvas.rs +++ b/wgpu/src/widget/canvas.rs @@ -6,196 +6,4 @@ //! //! [`Canvas`]: struct.Canvas.html //! [`Frame`]: struct.Frame.html -use crate::{Defaults, Primitive, Renderer}; - -use iced_native::{ - layout, Element, Hasher, Layout, Length, MouseCursor, Point, Size, Widget, -}; -use std::hash::Hash; - -pub mod layer; -pub mod path; - -mod drawable; -mod fill; -mod frame; -mod stroke; -mod text; - -pub use drawable::Drawable; -pub use fill::Fill; -pub use frame::Frame; -pub use layer::Layer; -pub use path::Path; -pub use stroke::{LineCap, LineJoin, Stroke}; -pub use text::Text; - -/// A widget capable of drawing 2D graphics. -/// -/// A [`Canvas`] may contain multiple layers. A [`Layer`] is drawn using the -/// painter's algorithm. In other words, layers will be drawn on top of each -/// other in the same order they are pushed into the [`Canvas`]. -/// -/// [`Canvas`]: struct.Canvas.html -/// [`Layer`]: layer/trait.Layer.html -/// -/// # Examples -/// The repository has a couple of [examples] showcasing how to use a -/// [`Canvas`]: -/// -/// - [`clock`], an application that uses the [`Canvas`] widget to draw a clock -/// and its hands to display the current time. -/// - [`solar_system`], an animated solar system drawn using the [`Canvas`] widget -/// and showcasing how to compose different transforms. -/// -/// [examples]: https://github.com/hecrj/iced/tree/0.1/examples -/// [`clock`]: https://github.com/hecrj/iced/tree/0.1/examples/clock -/// [`solar_system`]: https://github.com/hecrj/iced/tree/0.1/examples/solar_system -/// -/// ## Drawing a simple circle -/// If you want to get a quick overview, here's how we can draw a simple circle: -/// -/// ```no_run -/// # mod iced { -/// # pub use iced_wgpu::canvas; -/// # pub use iced_native::Color; -/// # } -/// use iced::canvas::{self, layer, Canvas, Drawable, Fill, Frame, Path}; -/// use iced::Color; -/// -/// // First, we define the data we need for drawing -/// #[derive(Debug)] -/// struct Circle { -/// radius: f32, -/// } -/// -/// // Then, we implement the `Drawable` trait -/// impl Drawable for Circle { -/// fn draw(&self, frame: &mut Frame) { -/// // We create a `Path` representing a simple circle -/// let circle = Path::new(|p| p.circle(frame.center(), self.radius)); -/// -/// // And fill it with some color -/// frame.fill(&circle, Fill::Color(Color::BLACK)); -/// } -/// } -/// -/// // We can use a `Cache` to avoid unnecessary re-tessellation -/// let cache: layer::Cache<Circle> = layer::Cache::new(); -/// -/// // Finally, we simply provide the data to our `Cache` and push the resulting -/// // layer into a `Canvas` -/// let canvas = Canvas::new() -/// .push(cache.with(&Circle { radius: 50.0 })); -/// ``` -#[derive(Debug)] -pub struct Canvas<'a> { - width: Length, - height: Length, - layers: Vec<Box<dyn Layer + 'a>>, -} - -impl<'a> Canvas<'a> { - const DEFAULT_SIZE: u16 = 100; - - /// Creates a new [`Canvas`] with no layers. - /// - /// [`Canvas`]: struct.Canvas.html - pub fn new() -> Self { - Canvas { - width: Length::Units(Self::DEFAULT_SIZE), - height: Length::Units(Self::DEFAULT_SIZE), - layers: Vec::new(), - } - } - - /// Sets the width of the [`Canvas`]. - /// - /// [`Canvas`]: struct.Canvas.html - pub fn width(mut self, width: Length) -> Self { - self.width = width; - self - } - - /// Sets the height of the [`Canvas`]. - /// - /// [`Canvas`]: struct.Canvas.html - pub fn height(mut self, height: Length) -> Self { - self.height = height; - self - } - - /// Adds a [`Layer`] to the [`Canvas`]. - /// - /// It will be drawn on top of previous layers. - /// - /// [`Layer`]: layer/trait.Layer.html - /// [`Canvas`]: struct.Canvas.html - pub fn push(mut self, layer: impl Layer + 'a) -> Self { - self.layers.push(Box::new(layer)); - self - } -} - -impl<'a, Message> Widget<Message, Renderer> for Canvas<'a> { - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height - } - - fn layout( - &self, - _renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - let size = limits.resolve(Size::ZERO); - - layout::Node::new(size) - } - - fn draw( - &self, - _renderer: &mut Renderer, - _defaults: &Defaults, - layout: Layout<'_>, - _cursor_position: Point, - ) -> (Primitive, MouseCursor) { - let bounds = layout.bounds(); - let origin = Point::new(bounds.x, bounds.y); - let size = Size::new(bounds.width, bounds.height); - - ( - Primitive::Group { - primitives: self - .layers - .iter() - .map(|layer| Primitive::Cached { - origin, - cache: layer.draw(size), - }) - .collect(), - }, - MouseCursor::Idle, - ) - } - - fn hash_layout(&self, state: &mut Hasher) { - std::any::TypeId::of::<Canvas<'static>>().hash(state); - - self.width.hash(state); - self.height.hash(state); - } -} - -impl<'a, Message> From<Canvas<'a>> for Element<'a, Message, Renderer> -where - Message: 'static, -{ - fn from(canvas: Canvas<'a>) -> Element<'a, Message, Renderer> { - Element::new(canvas) - } -} +pub use iced_graphics::canvas::*; diff --git a/wgpu/src/widget/canvas/drawable.rs b/wgpu/src/widget/canvas/drawable.rs deleted file mode 100644 index 6c74071c..00000000 --- a/wgpu/src/widget/canvas/drawable.rs +++ /dev/null @@ -1,12 +0,0 @@ -use crate::canvas::Frame; - -/// A type that can be drawn on a [`Frame`]. -/// -/// [`Frame`]: struct.Frame.html -pub trait Drawable { - /// Draws the [`Drawable`] on the given [`Frame`]. - /// - /// [`Drawable`]: trait.Drawable.html - /// [`Frame`]: struct.Frame.html - fn draw(&self, frame: &mut Frame); -} diff --git a/wgpu/src/widget/canvas/fill.rs b/wgpu/src/widget/canvas/fill.rs deleted file mode 100644 index a2010e45..00000000 --- a/wgpu/src/widget/canvas/fill.rs +++ /dev/null @@ -1,20 +0,0 @@ -use iced_native::Color; - -/// The style used to fill geometry. -#[derive(Debug, Clone, Copy)] -pub enum Fill { - /// Fill with a color. - Color(Color), -} - -impl Default for Fill { - fn default() -> Fill { - Fill::Color(Color::BLACK) - } -} - -impl From<Color> for Fill { - fn from(color: Color) -> Fill { - Fill::Color(color) - } -} diff --git a/wgpu/src/widget/canvas/layer.rs b/wgpu/src/widget/canvas/layer.rs deleted file mode 100644 index a46b7fb1..00000000 --- a/wgpu/src/widget/canvas/layer.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! Produce, store, and reuse geometry. -mod cache; - -pub use cache::Cache; - -use crate::Primitive; -use iced_native::Size; - -use std::sync::Arc; - -/// A layer that can be presented at a [`Canvas`]. -/// -/// [`Canvas`]: ../struct.Canvas.html -pub trait Layer: std::fmt::Debug { - /// Draws the [`Layer`] in the given bounds and produces a [`Primitive`] as - /// a result. - /// - /// The [`Layer`] may choose to store the produced [`Primitive`] locally and - /// only recompute it when the bounds change, its contents change, or is - /// otherwise explicitly cleared by other means. - /// - /// [`Layer`]: trait.Layer.html - /// [`Primitive`]: ../../../enum.Primitive.html - fn draw(&self, bounds: Size) -> Arc<Primitive>; -} diff --git a/wgpu/src/widget/canvas/layer/cache.rs b/wgpu/src/widget/canvas/layer/cache.rs deleted file mode 100644 index 4f8c2bec..00000000 --- a/wgpu/src/widget/canvas/layer/cache.rs +++ /dev/null @@ -1,128 +0,0 @@ -use crate::{ - canvas::{Drawable, Frame, Layer}, - Primitive, -}; - -use iced_native::Size; -use std::{cell::RefCell, marker::PhantomData, sync::Arc}; - -enum State { - Empty, - Filled { - bounds: Size, - primitive: Arc<Primitive>, - }, -} - -impl Default for State { - fn default() -> Self { - State::Empty - } -} -/// A simple cache that stores generated geometry to avoid recomputation. -/// -/// A [`Cache`] will not redraw its geometry unless the dimensions of its layer -/// change or it is explicitly cleared. -/// -/// [`Layer`]: ../trait.Layer.html -/// [`Cache`]: struct.Cache.html -#[derive(Debug)] -pub struct Cache<T: Drawable> { - input: PhantomData<T>, - state: RefCell<State>, -} - -impl<T> Default for Cache<T> -where - T: Drawable, -{ - fn default() -> Self { - Self { - input: PhantomData, - state: Default::default(), - } - } -} - -impl<T> Cache<T> -where - T: Drawable + std::fmt::Debug, -{ - /// Creates a new empty [`Cache`]. - /// - /// [`Cache`]: struct.Cache.html - pub fn new() -> Self { - Cache { - input: PhantomData, - state: Default::default(), - } - } - - /// Clears the cache, forcing a redraw the next time it is used. - /// - /// [`Cached`]: struct.Cached.html - pub fn clear(&mut self) { - *self.state.borrow_mut() = State::Empty; - } - - /// Binds the [`Cache`] with some data, producing a [`Layer`] that can be - /// added to a [`Canvas`]. - /// - /// [`Cache`]: struct.Cache.html - /// [`Layer`]: ../trait.Layer.html - /// [`Canvas`]: ../../struct.Canvas.html - pub fn with<'a>(&'a self, input: &'a T) -> impl Layer + 'a { - Bind { - cache: self, - input: input, - } - } -} - -#[derive(Debug)] -struct Bind<'a, T: Drawable> { - cache: &'a Cache<T>, - input: &'a T, -} - -impl<'a, T> Layer for Bind<'a, T> -where - T: Drawable + std::fmt::Debug, -{ - fn draw(&self, current_bounds: Size) -> Arc<Primitive> { - use std::ops::Deref; - - if let State::Filled { bounds, primitive } = - self.cache.state.borrow().deref() - { - if *bounds == current_bounds { - return primitive.clone(); - } - } - - let mut frame = Frame::new(current_bounds.width, current_bounds.height); - self.input.draw(&mut frame); - - let primitive = Arc::new(frame.into_primitive()); - - *self.cache.state.borrow_mut() = State::Filled { - bounds: current_bounds, - primitive: primitive.clone(), - }; - - primitive - } -} - -impl std::fmt::Debug for State { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - State::Empty => write!(f, "Empty"), - State::Filled { primitive, bounds } => f - .debug_struct("Filled") - .field("primitive", primitive) - .field("bounds", bounds) - .finish(), - } - } -} diff --git a/wgpu/src/widget/checkbox.rs b/wgpu/src/widget/checkbox.rs index da0d7a84..d27d77cc 100644 --- a/wgpu/src/widget/checkbox.rs +++ b/wgpu/src/widget/checkbox.rs @@ -1,7 +1,7 @@ //! Show toggle controls using checkboxes. use crate::Renderer; -pub use iced_style::checkbox::{Style, StyleSheet}; +pub use iced_graphics::checkbox::{Style, StyleSheet}; /// A box that can be checked. /// diff --git a/wgpu/src/widget/container.rs b/wgpu/src/widget/container.rs index 9a93a246..bc26cef2 100644 --- a/wgpu/src/widget/container.rs +++ b/wgpu/src/widget/container.rs @@ -1,7 +1,7 @@ //! Decorate content and apply alignment. use crate::Renderer; -pub use iced_style::container::{Style, StyleSheet}; +pub use iced_graphics::container::{Style, StyleSheet}; /// An element decorating some content. /// diff --git a/wgpu/src/widget/pane_grid.rs b/wgpu/src/widget/pane_grid.rs index 578e8960..6f437df7 100644 --- a/wgpu/src/widget/pane_grid.rs +++ b/wgpu/src/widget/pane_grid.rs @@ -11,8 +11,8 @@ use crate::Renderer; pub use iced_native::pane_grid::{ - Axis, Direction, DragEvent, Focus, KeyPressEvent, Pane, ResizeEvent, Split, - State, + Axis, Content, Direction, DragEvent, Focus, KeyPressEvent, Node, Pane, + ResizeEvent, Split, State, }; /// A collection of panes distributed using either vertical or horizontal splits diff --git a/wgpu/src/widget/progress_bar.rs b/wgpu/src/widget/progress_bar.rs index 770bcea8..a636a3a6 100644 --- a/wgpu/src/widget/progress_bar.rs +++ b/wgpu/src/widget/progress_bar.rs @@ -6,7 +6,7 @@ //! [`ProgressBar`]: type.ProgressBar.html use crate::Renderer; -pub use iced_style::progress_bar::{Style, StyleSheet}; +pub use iced_graphics::progress_bar::{Style, StyleSheet}; /// A bar that displays progress. /// diff --git a/wgpu/src/widget/radio.rs b/wgpu/src/widget/radio.rs index 6e5cf042..0b843d1f 100644 --- a/wgpu/src/widget/radio.rs +++ b/wgpu/src/widget/radio.rs @@ -1,7 +1,7 @@ //! Create choices using radio buttons. use crate::Renderer; -pub use iced_style::radio::{Style, StyleSheet}; +pub use iced_graphics::radio::{Style, StyleSheet}; /// A circular button representing a choice. /// diff --git a/wgpu/src/widget/scrollable.rs b/wgpu/src/widget/scrollable.rs index 1d236105..fabb4318 100644 --- a/wgpu/src/widget/scrollable.rs +++ b/wgpu/src/widget/scrollable.rs @@ -1,8 +1,8 @@ //! Navigate an endless amount of content with a scrollbar. use crate::Renderer; +pub use iced_graphics::scrollable::{Scrollbar, Scroller, StyleSheet}; pub use iced_native::scrollable::State; -pub use iced_style::scrollable::{Scrollbar, Scroller, StyleSheet}; /// A widget that can vertically display an infinite amount of content /// with a scrollbar. diff --git a/wgpu/src/widget/slider.rs b/wgpu/src/widget/slider.rs index 4e47978f..3a8c2595 100644 --- a/wgpu/src/widget/slider.rs +++ b/wgpu/src/widget/slider.rs @@ -6,11 +6,11 @@ //! [`State`]: struct.State.html use crate::Renderer; +pub use iced_graphics::slider::{Handle, HandleShape, Style, StyleSheet}; pub use iced_native::slider::State; -pub use iced_style::slider::{Handle, HandleShape, Style, StyleSheet}; /// An horizontal bar and a handle that selects a single value from a range of /// values. /// /// This is an alias of an `iced_native` slider with an `iced_wgpu::Renderer`. -pub type Slider<'a, Message> = iced_native::Slider<'a, Message, Renderer>; +pub type Slider<'a, T, Message> = iced_native::Slider<'a, T, Message, Renderer>; diff --git a/wgpu/src/widget/text_input.rs b/wgpu/src/widget/text_input.rs index 260fe3a6..1da3fbe6 100644 --- a/wgpu/src/widget/text_input.rs +++ b/wgpu/src/widget/text_input.rs @@ -6,8 +6,8 @@ //! [`State`]: struct.State.html use crate::Renderer; +pub use iced_graphics::text_input::{Style, StyleSheet}; pub use iced_native::text_input::State; -pub use iced_style::text_input::{Style, StyleSheet}; /// A field that can be filled with text. /// diff --git a/wgpu/src/window.rs b/wgpu/src/window.rs index b7adad82..aac5fb9e 100644 --- a/wgpu/src/window.rs +++ b/wgpu/src/window.rs @@ -1,6 +1,4 @@ //! Display rendering results on windows. -mod backend; -mod swap_chain; +mod compositor; -pub use backend::Backend; -pub use swap_chain::SwapChain; +pub use compositor::Compositor; diff --git a/wgpu/src/window/backend.rs b/wgpu/src/window/backend.rs deleted file mode 100644 index e1b77700..00000000 --- a/wgpu/src/window/backend.rs +++ /dev/null @@ -1,120 +0,0 @@ -use crate::{window::SwapChain, Renderer, Settings, Target}; - -use iced_native::{futures, MouseCursor}; -use raw_window_handle::HasRawWindowHandle; - -/// A window graphics backend for iced powered by `wgpu`. -#[derive(Debug)] -pub struct Backend { - device: wgpu::Device, - queue: wgpu::Queue, - format: wgpu::TextureFormat, -} - -impl iced_native::window::Backend for Backend { - type Settings = Settings; - type Renderer = Renderer; - type Surface = wgpu::Surface; - type SwapChain = SwapChain; - - fn new(settings: Self::Settings) -> (Backend, Renderer) { - let (mut device, queue) = futures::executor::block_on(async { - let adapter = wgpu::Adapter::request( - &wgpu::RequestAdapterOptions { - power_preference: if settings.antialiasing.is_none() { - wgpu::PowerPreference::Default - } else { - wgpu::PowerPreference::HighPerformance - }, - compatible_surface: None, - }, - wgpu::BackendBit::all(), - ) - .await - .expect("Request adapter"); - - adapter - .request_device(&wgpu::DeviceDescriptor { - extensions: wgpu::Extensions { - anisotropic_filtering: false, - }, - limits: wgpu::Limits { max_bind_groups: 2 }, - }) - .await - }); - - let renderer = Renderer::new(&mut device, settings); - - ( - Backend { - device, - queue, - format: settings.format, - }, - renderer, - ) - } - - fn create_surface<W: HasRawWindowHandle>( - &mut self, - window: &W, - ) -> wgpu::Surface { - wgpu::Surface::create(window) - } - - fn create_swap_chain( - &mut self, - surface: &Self::Surface, - width: u32, - height: u32, - ) -> SwapChain { - SwapChain::new(&self.device, surface, self.format, width, height) - } - - fn draw<T: AsRef<str>>( - &mut self, - renderer: &mut Self::Renderer, - swap_chain: &mut SwapChain, - output: &<Self::Renderer as iced_native::Renderer>::Output, - scale_factor: f64, - overlay: &[T], - ) -> MouseCursor { - let (frame, viewport) = swap_chain.next_frame().expect("Next frame"); - - let mut encoder = self.device.create_command_encoder( - &wgpu::CommandEncoderDescriptor { label: None }, - ); - - let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor { - attachment: &frame.view, - resolve_target: None, - load_op: wgpu::LoadOp::Clear, - store_op: wgpu::StoreOp::Store, - clear_color: wgpu::Color { - r: 1.0, - g: 1.0, - b: 1.0, - a: 1.0, - }, - }], - depth_stencil_attachment: None, - }); - - let mouse_cursor = renderer.draw( - &mut self.device, - &mut encoder, - Target { - texture: &frame.view, - viewport, - }, - output, - scale_factor, - overlay, - ); - - self.queue.submit(&[encoder.finish()]); - - mouse_cursor - } -} diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs new file mode 100644 index 00000000..5bdd34bc --- /dev/null +++ b/wgpu/src/window/compositor.rs @@ -0,0 +1,149 @@ +use crate::{Backend, Color, Renderer, Settings}; + +use iced_graphics::Viewport; +use iced_native::{futures, mouse}; +use raw_window_handle::HasRawWindowHandle; + +/// A window graphics backend for iced powered by `wgpu`. +#[derive(Debug)] +pub struct Compositor { + settings: Settings, + device: wgpu::Device, + queue: wgpu::Queue, +} + +impl Compositor { + /// Requests a new [`Compositor`] with the given [`Settings`]. + /// + /// Returns `None` if no compatible graphics adapter could be found. + /// + /// [`Compositor`]: struct.Compositor.html + /// [`Settings`]: struct.Settings.html + pub async fn request(settings: Settings) -> Option<Self> { + let adapter = wgpu::Adapter::request( + &wgpu::RequestAdapterOptions { + power_preference: if settings.antialiasing.is_none() { + wgpu::PowerPreference::Default + } else { + wgpu::PowerPreference::HighPerformance + }, + compatible_surface: None, + }, + wgpu::BackendBit::PRIMARY, + ) + .await?; + + let (device, queue) = adapter + .request_device(&wgpu::DeviceDescriptor { + extensions: wgpu::Extensions { + anisotropic_filtering: false, + }, + limits: wgpu::Limits { max_bind_groups: 2 }, + }) + .await; + + Some(Compositor { + settings, + device, + queue, + }) + } + + /// Creates a new rendering [`Backend`] for this [`Compositor`]. + /// + /// [`Compositor`]: struct.Compositor.html + /// [`Backend`]: struct.Backend.html + pub fn create_backend(&self) -> Backend { + Backend::new(&self.device, self.settings) + } +} + +impl iced_graphics::window::Compositor for Compositor { + type Settings = Settings; + type Renderer = Renderer; + type Surface = wgpu::Surface; + type SwapChain = wgpu::SwapChain; + + fn new(settings: Self::Settings) -> (Self, Renderer) { + let compositor = futures::executor::block_on(Self::request(settings)) + .expect("Could not find a suitable graphics adapter"); + + let backend = compositor.create_backend(); + + (compositor, Renderer::new(backend)) + } + + fn create_surface<W: HasRawWindowHandle>( + &mut self, + window: &W, + ) -> wgpu::Surface { + wgpu::Surface::create(window) + } + + fn create_swap_chain( + &mut self, + surface: &Self::Surface, + width: u32, + height: u32, + ) -> Self::SwapChain { + self.device.create_swap_chain( + surface, + &wgpu::SwapChainDescriptor { + usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT, + format: self.settings.format, + width, + height, + present_mode: wgpu::PresentMode::Mailbox, + }, + ) + } + + fn draw<T: AsRef<str>>( + &mut self, + renderer: &mut Self::Renderer, + swap_chain: &mut Self::SwapChain, + viewport: &Viewport, + background_color: Color, + output: &<Self::Renderer as iced_native::Renderer>::Output, + overlay: &[T], + ) -> mouse::Interaction { + let frame = swap_chain.get_next_texture().expect("Next frame"); + + let mut encoder = self.device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { label: None }, + ); + + let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor { + attachment: &frame.view, + resolve_target: None, + load_op: wgpu::LoadOp::Clear, + store_op: wgpu::StoreOp::Store, + clear_color: { + let [r, g, b, a] = background_color.into_linear(); + + wgpu::Color { + r: f64::from(r), + g: f64::from(g), + b: f64::from(b), + a: f64::from(a), + } + }, + }], + depth_stencil_attachment: None, + }); + + let mouse_interaction = renderer.backend_mut().draw( + &mut self.device, + &mut encoder, + &frame.view, + viewport, + output, + overlay, + ); + + self.queue.submit(&[encoder.finish()]); + + mouse_interaction + } +} diff --git a/wgpu/src/window/swap_chain.rs b/wgpu/src/window/swap_chain.rs deleted file mode 100644 index 72e58a50..00000000 --- a/wgpu/src/window/swap_chain.rs +++ /dev/null @@ -1,61 +0,0 @@ -use crate::Viewport; - -/// The rendering target of a window. -/// -/// It represents a series of virtual framebuffers with a scale factor. -#[derive(Debug)] -pub struct SwapChain { - raw: wgpu::SwapChain, - viewport: Viewport, -} - -impl SwapChain {} - -impl SwapChain { - /// Creates a new [`SwapChain`] for the given surface. - /// - /// [`SwapChain`]: struct.SwapChain.html - pub fn new( - device: &wgpu::Device, - surface: &wgpu::Surface, - format: wgpu::TextureFormat, - width: u32, - height: u32, - ) -> SwapChain { - SwapChain { - raw: new_swap_chain(surface, format, width, height, device), - viewport: Viewport::new(width, height), - } - } - - /// Returns the next frame of the [`SwapChain`] alongside its [`Viewport`]. - /// - /// [`SwapChain`]: struct.SwapChain.html - /// [`Viewport`]: ../struct.Viewport.html - pub fn next_frame( - &mut self, - ) -> Result<(wgpu::SwapChainOutput, &Viewport), wgpu::TimeOut> { - let viewport = &self.viewport; - - self.raw.get_next_texture().map(|output| (output, viewport)) - } -} - -fn new_swap_chain( - surface: &wgpu::Surface, - format: wgpu::TextureFormat, - width: u32, - height: u32, - device: &wgpu::Device, -) -> wgpu::SwapChain { - device.create_swap_chain( - &surface, - &wgpu::SwapChainDescriptor { - usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT, - format, - width, - height, - present_mode: wgpu::PresentMode::Mailbox, - }, - ) -} diff --git a/winit/Cargo.toml b/winit/Cargo.toml index b6662451..7fe83b96 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -11,7 +11,7 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"] categories = ["gui"] [features] -debug = [] +debug = ["iced_native/debug"] [dependencies] winit = "0.22" @@ -22,5 +22,9 @@ log = "0.4" version = "0.2" path = "../native" +[dependencies.iced_graphics] +version = "0.1" +path = "../graphics" + [target.'cfg(target_os = "windows")'.dependencies.winapi] version = "0.3.6" diff --git a/winit/src/application.rs b/winit/src/application.rs index b974711c..5b93c8af 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -1,8 +1,11 @@ +//! Create interactive, native cross-platform applications. use crate::{ - conversion, size::Size, window, Cache, Clipboard, Command, Debug, Element, - Executor, Mode, MouseCursor, Proxy, Runtime, Settings, Subscription, - UserInterface, + conversion, mouse, Clipboard, Color, Command, Debug, Executor, Mode, Proxy, + Runtime, Settings, Size, Subscription, }; +use iced_graphics::window; +use iced_graphics::Viewport; +use iced_native::program::{self, Program}; /// An interactive, native cross-platform application. /// @@ -15,22 +18,9 @@ use crate::{ /// /// When using an [`Application`] with the `debug` feature enabled, a debug view /// can be toggled by pressing `F12`. -pub trait Application: Sized { - /// The graphics backend to use to draw the [`Application`]. - /// - /// [`Application`]: trait.Application.html - type Backend: window::Backend; - - /// The [`Executor`] that will run commands and subscriptions. - /// - /// [`Executor`]: trait.Executor.html - type Executor: Executor; - - /// The type of __messages__ your [`Application`] will produce. - /// - /// [`Application`]: trait.Application.html - type Message: std::fmt::Debug + Send; - +/// +/// [`Application`]: trait.Application.html +pub trait Application: Program { /// The data needed to initialize your [`Application`]. /// /// [`Application`]: trait.Application.html @@ -48,7 +38,7 @@ pub trait Application: Sized { /// /// [`Application`]: trait.Application.html /// [`run`]: #method.run.html - /// [`Settings`]: struct.Settings.html + /// [`Settings`]: ../settings/struct.Settings.html fn new(flags: Self::Flags) -> (Self, Command<Self::Message>); /// Returns the current title of the [`Application`]. @@ -59,18 +49,6 @@ pub trait Application: Sized { /// [`Application`]: trait.Application.html fn title(&self) -> String; - /// Handles a __message__ and updates the state of the [`Application`]. - /// - /// This is where you define your __update logic__. All the __messages__, - /// produced by either user interactions or commands, will be handled by - /// this method. - /// - /// Any [`Command`] returned will be executed immediately in the background. - /// - /// [`Application`]: trait.Application.html - /// [`Command`]: struct.Command.html - fn update(&mut self, message: Self::Message) -> Command<Self::Message>; - /// Returns the event `Subscription` for the current state of the /// application. /// @@ -78,16 +56,11 @@ pub trait Application: Sized { /// [`update`](#tymethod.update). /// /// A `Subscription` will be kept alive as long as you keep returning it! - fn subscription(&self) -> Subscription<Self::Message>; - - /// Returns the widgets to display in the [`Application`]. - /// - /// These widgets can produce __messages__ based on user interaction. /// - /// [`Application`]: trait.Application.html - fn view( - &mut self, - ) -> Element<'_, Self::Message, <Self::Backend as window::Backend>::Renderer>; + /// By default, it returns an empty subscription. + fn subscription(&self) -> Subscription<Self::Message> { + Subscription::none() + } /// Returns the current [`Application`] mode. /// @@ -101,333 +74,323 @@ pub trait Application: Sized { Mode::Windowed } - /// Runs the [`Application`] with the provided [`Settings`]. + /// Returns the background [`Color`] of the [`Application`]. /// - /// On native platforms, this method will take control of the current thread - /// and __will NOT return__. - /// - /// It should probably be that last thing you call in your `main` function. + /// By default, it returns [`Color::WHITE`]. /// + /// [`Color`]: struct.Color.html /// [`Application`]: trait.Application.html - /// [`Settings`]: struct.Settings.html - fn run( - settings: Settings<Self::Flags>, - backend_settings: <Self::Backend as window::Backend>::Settings, - ) where - Self: 'static, - { - use window::Backend as _; - use winit::{ - event::{self, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - window::WindowBuilder, - }; - - let mut debug = Debug::new(); - - debug.startup_started(); - let event_loop = EventLoop::with_user_event(); - let mut external_messages = Vec::new(); - - let mut runtime = { - let executor = Self::Executor::new().expect("Create executor"); - - Runtime::new(executor, Proxy::new(event_loop.create_proxy())) - }; - - let flags = settings.flags; - let (mut application, init_command) = - runtime.enter(|| Self::new(flags)); - runtime.spawn(init_command); - - let subscription = application.subscription(); - runtime.track(subscription); - - let mut title = application.title(); - let mut mode = application.mode(); - - let window = { - let mut window_builder = WindowBuilder::new(); - - let (width, height) = settings.window.size; - - window_builder = window_builder - .with_title(&title) - .with_inner_size(winit::dpi::LogicalSize { width, height }) - .with_resizable(settings.window.resizable) - .with_decorations(settings.window.decorations) - .with_fullscreen(conversion::fullscreen( - event_loop.primary_monitor(), - mode, - )); - - #[cfg(target_os = "windows")] - { - use winit::platform::windows::WindowBuilderExtWindows; - - if let Some(parent) = settings.window.platform_specific.parent { - window_builder = window_builder.with_parent_window(parent); - } - } - - window_builder.build(&event_loop).expect("Open window") - }; - - let mut size = Size::new(window.inner_size(), window.scale_factor()); - let mut resized = false; - - let clipboard = Clipboard::new(&window); - let (mut backend, mut renderer) = Self::Backend::new(backend_settings); - - let surface = backend.create_surface(&window); + /// [`Color::WHITE`]: struct.Color.html#const.WHITE + fn background_color(&self) -> Color { + Color::WHITE + } - let mut swap_chain = { - let physical_size = size.physical(); + /// Returns the scale factor of the [`Application`]. + /// + /// It can be used to dynamically control the size of the UI at runtime + /// (i.e. zooming). + /// + /// For instance, a scale factor of `2.0` will make widgets twice as big, + /// while a scale factor of `0.5` will shrink them to half their size. + /// + /// By default, it returns `1.0`. + /// + /// [`Application`]: trait.Application.html + fn scale_factor(&self) -> f64 { + 1.0 + } +} - backend.create_swap_chain( - &surface, - physical_size.width, - physical_size.height, - ) - }; +/// Runs an [`Application`] with an executor, compositor, and the provided +/// settings. +/// +/// [`Application`]: trait.Application.html +pub fn run<A, E, C>( + settings: Settings<A::Flags>, + compositor_settings: C::Settings, +) where + A: Application + 'static, + E: Executor + 'static, + C: window::Compositor<Renderer = A::Renderer> + 'static, +{ + use winit::{ + event, + event_loop::{ControlFlow, EventLoop}, + }; + + let mut debug = Debug::new(); + debug.startup_started(); + + let event_loop = EventLoop::with_user_event(); + let mut runtime = { + let executor = E::new().expect("Create executor"); + let proxy = Proxy::new(event_loop.create_proxy()); + + Runtime::new(executor, proxy) + }; + + let flags = settings.flags; + let (application, init_command) = runtime.enter(|| A::new(flags)); + runtime.spawn(init_command); + + let subscription = application.subscription(); + runtime.track(subscription); + + let mut title = application.title(); + let mut mode = application.mode(); + let mut background_color = application.background_color(); + let mut scale_factor = application.scale_factor(); + + let window = settings + .window + .into_builder(&title, mode, event_loop.primary_monitor()) + .build(&event_loop) + .expect("Open window"); + + let clipboard = Clipboard::new(&window); + let mut cursor_position = winit::dpi::PhysicalPosition::new(-1.0, -1.0); + let mut mouse_interaction = mouse::Interaction::default(); + let mut modifiers = winit::event::ModifiersState::default(); + + let physical_size = window.inner_size(); + let mut viewport = Viewport::with_physical_size( + Size::new(physical_size.width, physical_size.height), + window.scale_factor() * scale_factor, + ); + let mut resized = false; - let user_interface = build_user_interface( - &mut application, - Cache::default(), - &mut renderer, - size.logical(), - &mut debug, - ); + let (mut compositor, mut renderer) = C::new(compositor_settings); - debug.draw_started(); - let mut primitive = user_interface.draw(&mut renderer); - debug.draw_finished(); + let surface = compositor.create_surface(&window); - let mut cache = Some(user_interface.into_cache()); - let mut events = Vec::new(); - let mut mouse_cursor = MouseCursor::OutOfBounds; - let mut modifiers = winit::event::ModifiersState::default(); - debug.startup_finished(); + let mut swap_chain = compositor.create_swap_chain( + &surface, + physical_size.width, + physical_size.height, + ); - window.request_redraw(); + let mut state = program::State::new( + application, + viewport.logical_size(), + conversion::cursor_position(cursor_position, viewport.scale_factor()), + &mut renderer, + &mut debug, + ); + debug.startup_finished(); - event_loop.run(move |event, _, control_flow| match event { - event::Event::MainEventsCleared => { - if events.is_empty() && external_messages.is_empty() { - return; - } + event_loop.run(move |event, _, control_flow| match event { + event::Event::MainEventsCleared => { + if state.is_queue_empty() { + return; + } - // TODO: We should be able to keep a user interface alive - // between events once we remove state references. - // - // This will allow us to rebuild it only when a message is - // handled. - let mut user_interface = build_user_interface( - &mut application, - cache.take().unwrap(), + let command = runtime.enter(|| { + state.update( + viewport.logical_size(), + conversion::cursor_position( + cursor_position, + viewport.scale_factor(), + ), + clipboard.as_ref().map(|c| c as _), &mut renderer, - size.logical(), &mut debug, - ); + ) + }); - debug.event_processing_started(); - events - .iter() - .cloned() - .for_each(|event| runtime.broadcast(event)); - - let mut messages = user_interface.update( - events.drain(..), - clipboard - .as_ref() - .map(|c| c as &dyn iced_native::Clipboard), - &renderer, - ); - messages.extend(external_messages.drain(..)); - debug.event_processing_finished(); + // If the application was updated + if let Some(command) = command { + runtime.spawn(command); - if messages.is_empty() { - debug.draw_started(); - primitive = user_interface.draw(&mut renderer); - debug.draw_finished(); + let program = state.program(); - cache = Some(user_interface.into_cache()); - } else { - // When there are messages, we are forced to rebuild twice - // for now :^) - let temp_cache = user_interface.into_cache(); + // Update subscriptions + let subscription = program.subscription(); + runtime.track(subscription); - for message in messages { - log::debug!("Updating"); + // Update window title + let new_title = program.title(); - debug.log_message(&message); + if title != new_title { + window.set_title(&new_title); - debug.update_started(); - let command = - runtime.enter(|| application.update(message)); - runtime.spawn(command); - debug.update_finished(); - } + title = new_title; + } - let subscription = application.subscription(); - runtime.track(subscription); + // Update window mode + let new_mode = program.mode(); - // Update window title - let new_title = application.title(); + if mode != new_mode { + window.set_fullscreen(conversion::fullscreen( + window.current_monitor(), + new_mode, + )); - if title != new_title { - window.set_title(&new_title); + mode = new_mode; + } - title = new_title; - } + // Update background color + background_color = program.background_color(); - // Update window mode - let new_mode = application.mode(); + // Update scale factor + let new_scale_factor = program.scale_factor(); - if mode != new_mode { - window.set_fullscreen(conversion::fullscreen( - window.current_monitor(), - new_mode, - )); + if scale_factor != new_scale_factor { + let size = window.inner_size(); - mode = new_mode; - } + viewport = Viewport::with_physical_size( + Size::new(size.width, size.height), + window.scale_factor() * new_scale_factor, + ); - let user_interface = build_user_interface( - &mut application, - temp_cache, + // We relayout the UI with the new logical size. + // The queue is empty, therefore this will never produce + // a `Command`. + // + // TODO: Properly queue `WindowResized` + let _ = state.update( + viewport.logical_size(), + conversion::cursor_position( + cursor_position, + viewport.scale_factor(), + ), + clipboard.as_ref().map(|c| c as _), &mut renderer, - size.logical(), &mut debug, ); - debug.draw_started(); - primitive = user_interface.draw(&mut renderer); - debug.draw_finished(); - - cache = Some(user_interface.into_cache()); + scale_factor = new_scale_factor; } - - window.request_redraw(); } - event::Event::UserEvent(message) => { - external_messages.push(message); - } - event::Event::RedrawRequested(_) => { - debug.render_started(); - - if resized { - let physical_size = size.physical(); - swap_chain = backend.create_swap_chain( - &surface, - physical_size.width, - physical_size.height, - ); - - resized = false; - } - - let new_mouse_cursor = backend.draw( - &mut renderer, - &mut swap_chain, - &primitive, - size.scale_factor(), - &debug.overlay(), + window.request_redraw(); + } + event::Event::UserEvent(message) => { + state.queue_message(message); + } + event::Event::RedrawRequested(_) => { + debug.render_started(); + + if resized { + let physical_size = viewport.physical_size(); + + swap_chain = compositor.create_swap_chain( + &surface, + physical_size.width, + physical_size.height, ); - debug.render_finished(); + resized = false; + } - if new_mouse_cursor != mouse_cursor { - window.set_cursor_icon(conversion::mouse_cursor( - new_mouse_cursor, - )); + let new_mouse_interaction = compositor.draw( + &mut renderer, + &mut swap_chain, + &viewport, + background_color, + state.primitive(), + &debug.overlay(), + ); - mouse_cursor = new_mouse_cursor; - } + debug.render_finished(); - // TODO: Handle animations! - // Maybe we can use `ControlFlow::WaitUntil` for this. - } - event::Event::WindowEvent { - event: window_event, - .. - } => { - match window_event { - WindowEvent::Resized(new_size) => { - size = Size::new(new_size, window.scale_factor()); - resized = true; - } - WindowEvent::CloseRequested => { - *control_flow = ControlFlow::Exit; - } - WindowEvent::ModifiersChanged(new_modifiers) => { - modifiers = new_modifiers; - } - #[cfg(target_os = "macos")] - WindowEvent::KeyboardInput { - input: - winit::event::KeyboardInput { - virtual_keycode: - Some(winit::event::VirtualKeyCode::Q), - state: winit::event::ElementState::Pressed, - .. - }, - .. - } if modifiers.logo() => { - *control_flow = ControlFlow::Exit; - } - #[cfg(feature = "debug")] - WindowEvent::KeyboardInput { - input: - winit::event::KeyboardInput { - virtual_keycode: - Some(winit::event::VirtualKeyCode::F12), - state: winit::event::ElementState::Pressed, - .. - }, - .. - } => debug.toggle(), - _ => {} - } + if new_mouse_interaction != mouse_interaction { + window.set_cursor_icon(conversion::mouse_interaction( + new_mouse_interaction, + )); - if let Some(event) = conversion::window_event( - &window_event, - size.scale_factor(), - modifiers, - ) { - events.push(event); - } + mouse_interaction = new_mouse_interaction; } - _ => { - *control_flow = ControlFlow::Wait; + + // TODO: Handle animations! + // Maybe we can use `ControlFlow::WaitUntil` for this. + } + event::Event::WindowEvent { + event: window_event, + .. + } => { + handle_window_event( + &window_event, + &window, + scale_factor, + control_flow, + &mut cursor_position, + &mut modifiers, + &mut viewport, + &mut resized, + &mut debug, + ); + + if let Some(event) = conversion::window_event( + &window_event, + viewport.scale_factor(), + modifiers, + ) { + state.queue_event(event.clone()); + runtime.broadcast(event); } - }) - } + } + _ => { + *control_flow = ControlFlow::Wait; + } + }) } -fn build_user_interface<'a, A: Application>( - application: &'a mut A, - cache: Cache, - renderer: &mut <A::Backend as window::Backend>::Renderer, - size: winit::dpi::LogicalSize<f64>, - debug: &mut Debug, -) -> UserInterface<'a, A::Message, <A::Backend as window::Backend>::Renderer> { - debug.view_started(); - let view = application.view(); - debug.view_finished(); - - debug.layout_started(); - let user_interface = UserInterface::build( - view, - iced_native::Size::new( - size.width.round() as f32, - size.height.round() as f32, - ), - cache, - renderer, - ); - debug.layout_finished(); - - user_interface +/// Handles a `WindowEvent` and mutates the provided control flow, keyboard +/// modifiers, viewport, and resized flag accordingly. +pub fn handle_window_event( + event: &winit::event::WindowEvent<'_>, + window: &winit::window::Window, + scale_factor: f64, + control_flow: &mut winit::event_loop::ControlFlow, + cursor_position: &mut winit::dpi::PhysicalPosition<f64>, + modifiers: &mut winit::event::ModifiersState, + viewport: &mut Viewport, + resized: &mut bool, + _debug: &mut Debug, +) { + use winit::{event::WindowEvent, event_loop::ControlFlow}; + + match event { + WindowEvent::Resized(new_size) => { + let size = Size::new(new_size.width, new_size.height); + + *viewport = Viewport::with_physical_size( + size, + window.scale_factor() * scale_factor, + ); + *resized = true; + } + WindowEvent::CloseRequested => { + *control_flow = ControlFlow::Exit; + } + WindowEvent::CursorMoved { position, .. } => { + *cursor_position = *position; + } + WindowEvent::ModifiersChanged(new_modifiers) => { + *modifiers = *new_modifiers; + } + #[cfg(target_os = "macos")] + WindowEvent::KeyboardInput { + input: + winit::event::KeyboardInput { + virtual_keycode: Some(winit::event::VirtualKeyCode::Q), + state: winit::event::ElementState::Pressed, + .. + }, + .. + } if modifiers.logo() => { + *control_flow = ControlFlow::Exit; + } + #[cfg(feature = "debug")] + WindowEvent::KeyboardInput { + input: + winit::event::KeyboardInput { + virtual_keycode: Some(winit::event::VirtualKeyCode::F12), + state: winit::event::ElementState::Pressed, + .. + }, + .. + } => _debug.toggle(), + _ => {} + } } diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index eaa26ace..80727bd8 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -3,11 +3,8 @@ //! [`winit`]: https://github.com/rust-windowing/winit //! [`iced_native`]: https://github.com/hecrj/iced/tree/master/native use crate::{ - input::{ - keyboard::{self, KeyCode, ModifiersState}, - mouse, ButtonState, - }, - window, Event, Mode, MouseCursor, + keyboard::{self, KeyCode, ModifiersState}, + mouse, window, Event, Mode, Point, }; /// Converts a winit window event into an iced event. @@ -36,9 +33,15 @@ pub fn window_event( })) } WindowEvent::MouseInput { button, state, .. } => { - Some(Event::Mouse(mouse::Event::Input { - button: mouse_button(*button), - state: button_state(*state), + let button = mouse_button(*button); + + Some(Event::Mouse(match state { + winit::event::ElementState::Pressed => { + mouse::Event::ButtonPressed(button) + } + winit::event::ElementState::Released => { + mouse::Event::ButtonReleased(button) + } })) } WindowEvent::MouseWheel { delta, .. } => match delta { @@ -70,10 +73,24 @@ pub fn window_event( .. }, .. - } => Some(Event::Keyboard(keyboard::Event::Input { - key_code: key_code(*virtual_keycode), - state: button_state(*state), - modifiers: modifiers_state(modifiers), + } => Some(Event::Keyboard({ + let key_code = key_code(*virtual_keycode); + let modifiers = modifiers_state(modifiers); + + match state { + winit::event::ElementState::Pressed => { + keyboard::Event::KeyPressed { + key_code, + modifiers, + } + } + winit::event::ElementState::Released => { + keyboard::Event::KeyReleased { + key_code, + modifiers, + } + } + } })), WindowEvent::HoveredFile(path) => { Some(Event::Window(window::Event::FileHovered(path.clone()))) @@ -108,19 +125,23 @@ pub fn fullscreen( /// /// [`winit`]: https://github.com/rust-windowing/winit /// [`iced_native`]: https://github.com/hecrj/iced/tree/master/native -pub fn mouse_cursor(mouse_cursor: MouseCursor) -> winit::window::CursorIcon { - match mouse_cursor { - MouseCursor::OutOfBounds => winit::window::CursorIcon::Default, - MouseCursor::Idle => winit::window::CursorIcon::Default, - MouseCursor::Pointer => winit::window::CursorIcon::Hand, - MouseCursor::Working => winit::window::CursorIcon::Progress, - MouseCursor::Grab => winit::window::CursorIcon::Grab, - MouseCursor::Grabbing => winit::window::CursorIcon::Grabbing, - MouseCursor::Text => winit::window::CursorIcon::Text, - MouseCursor::ResizingHorizontally => { +pub fn mouse_interaction( + interaction: mouse::Interaction, +) -> winit::window::CursorIcon { + use mouse::Interaction; + + match interaction { + Interaction::Idle => winit::window::CursorIcon::Default, + Interaction::Pointer => winit::window::CursorIcon::Hand, + Interaction::Working => winit::window::CursorIcon::Progress, + Interaction::Grab => winit::window::CursorIcon::Grab, + Interaction::Grabbing => winit::window::CursorIcon::Grabbing, + Interaction::Crosshair => winit::window::CursorIcon::Crosshair, + Interaction::Text => winit::window::CursorIcon::Text, + Interaction::ResizingHorizontally => { winit::window::CursorIcon::EwResize } - MouseCursor::ResizingVertically => winit::window::CursorIcon::NsResize, + Interaction::ResizingVertically => winit::window::CursorIcon::NsResize, } } @@ -137,17 +158,6 @@ pub fn mouse_button(mouse_button: winit::event::MouseButton) -> mouse::Button { } } -/// Converts an `ElementState` from [`winit`] to an [`iced_native`] button state. -/// -/// [`winit`]: https://github.com/rust-windowing/winit -/// [`iced_native`]: https://github.com/hecrj/iced/tree/master/native -pub fn button_state(element_state: winit::event::ElementState) -> ButtonState { - match element_state { - winit::event::ElementState::Pressed => ButtonState::Pressed, - winit::event::ElementState::Released => ButtonState::Released, - } -} - /// Converts some `ModifiersState` from [`winit`] to an [`iced_native`] /// modifiers state. /// @@ -164,6 +174,16 @@ pub fn modifiers_state( } } +/// Converts a physical cursor position to a logical `Point`. +pub fn cursor_position( + position: winit::dpi::PhysicalPosition<f64>, + scale_factor: f64, +) -> Point { + let logical_position = position.to_logical(scale_factor); + + Point::new(logical_position.x, logical_position.y) +} + /// Converts a `VirtualKeyCode` from [`winit`] to an [`iced_native`] key code. /// /// [`winit`]: https://github.com/rust-windowing/winit diff --git a/winit/src/lib.rs b/winit/src/lib.rs index f99e1290..bdab3ed7 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -25,28 +25,18 @@ pub use iced_native::*; pub use winit; +pub mod application; pub mod conversion; pub mod settings; -mod application; mod clipboard; mod mode; mod proxy; -mod size; - -// We disable debug capabilities on release builds unless the `debug` feature -// is explicitly enabled. -#[cfg(feature = "debug")] -#[path = "debug/basic.rs"] -mod debug; -#[cfg(not(feature = "debug"))] -#[path = "debug/null.rs"] -mod debug; pub use application::Application; pub use clipboard::Clipboard; pub use mode::Mode; +pub use proxy::Proxy; pub use settings::Settings; -use debug::Debug; -use proxy::Proxy; +pub use iced_graphics::Viewport; diff --git a/winit/src/proxy.rs b/winit/src/proxy.rs index cff6ca72..532f8c56 100644 --- a/winit/src/proxy.rs +++ b/winit/src/proxy.rs @@ -5,6 +5,8 @@ use iced_native::futures::{ }; use std::pin::Pin; +/// An event loop proxy that implements `Sink`. +#[derive(Debug)] pub struct Proxy<Message: 'static> { raw: winit::event_loop::EventLoopProxy<Message>, } @@ -18,6 +20,9 @@ impl<Message: 'static> Clone for Proxy<Message> { } impl<Message: 'static> Proxy<Message> { + /// Creates a new [`Proxy`] from an `EventLoopProxy`. + /// + /// [`Proxy`]: struct.Proxy.html pub fn new(raw: winit::event_loop::EventLoopProxy<Message>) -> Self { Self { raw } } diff --git a/winit/src/settings.rs b/winit/src/settings.rs index d58c51f0..6799f23f 100644 --- a/winit/src/settings.rs +++ b/winit/src/settings.rs @@ -8,6 +8,11 @@ mod platform; pub use platform::PlatformSpecific; +use crate::conversion; +use crate::Mode; +use winit::monitor::MonitorHandle; +use winit::window::WindowBuilder; + /// The settings of an application. #[derive(Debug, Clone, Copy, PartialEq, Default)] pub struct Settings<Flags> { @@ -28,6 +33,12 @@ pub struct Window { /// The size of the window. pub size: (u32, u32), + /// The minimum size of the window. + pub min_size: Option<(u32, u32)>, + + /// The maximum size of the window. + pub max_size: Option<(u32, u32)>, + /// Whether the window should be resizable or not. pub resizable: bool, @@ -38,10 +49,54 @@ pub struct Window { pub platform_specific: platform::PlatformSpecific, } +impl Window { + /// Converts the window settings into a `WindowBuilder` from `winit`. + pub fn into_builder( + self, + title: &str, + mode: Mode, + primary_monitor: MonitorHandle, + ) -> WindowBuilder { + let mut window_builder = WindowBuilder::new(); + + let (width, height) = self.size; + + window_builder = window_builder + .with_title(title) + .with_inner_size(winit::dpi::LogicalSize { width, height }) + .with_resizable(self.resizable) + .with_decorations(self.decorations) + .with_fullscreen(conversion::fullscreen(primary_monitor, mode)); + + if let Some((width, height)) = self.min_size { + window_builder = window_builder + .with_min_inner_size(winit::dpi::LogicalSize { width, height }); + } + + if let Some((width, height)) = self.max_size { + window_builder = window_builder + .with_max_inner_size(winit::dpi::LogicalSize { width, height }); + } + + #[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); + } + } + + window_builder + } +} + impl Default for Window { fn default() -> Window { Window { size: (1024, 768), + min_size: None, + max_size: None, resizable: true, decorations: true, platform_specific: Default::default(), diff --git a/winit/src/size.rs b/winit/src/size.rs deleted file mode 100644 index 7e3056d4..00000000 --- a/winit/src/size.rs +++ /dev/null @@ -1,30 +0,0 @@ -pub struct Size { - physical: winit::dpi::PhysicalSize<u32>, - logical: winit::dpi::LogicalSize<f64>, - scale_factor: f64, -} - -impl Size { - pub fn new( - physical: winit::dpi::PhysicalSize<u32>, - scale_factor: f64, - ) -> Size { - Size { - logical: physical.to_logical(scale_factor), - physical, - scale_factor, - } - } - - pub fn physical(&self) -> winit::dpi::PhysicalSize<u32> { - self.physical - } - - pub fn logical(&self) -> winit::dpi::LogicalSize<f64> { - self.logical - } - - pub fn scale_factor(&self) -> f64 { - self.scale_factor - } -} |