diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f0fa47c..29cdef58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,70 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.6.0] - 2022-12-07 +### Added +- Support for non-uniform border radius for `Primitive::Quad`. [#1506](https://github.com/iced-rs/iced/pull/1506) +- Operation to query the current focused widget. [#1526](https://github.com/iced-rs/iced/pull/1526) +- Additional operations for `TextInput`. [#1529](https://github.com/iced-rs/iced/pull/1529) +- Styling support for `Svg`. [#1578](https://github.com/iced-rs/iced/pull/1578) + +### Changed +- Triangle geometry using a solid color is now drawn in a single draw call. [#1538](https://github.com/iced-rs/iced/pull/1538) + +### Fixed +- Gradients for WebAssembly target. [#1524](https://github.com/iced-rs/iced/pull/1524) +- `Overlay` layout cache not being invalidated. [#1528](https://github.com/iced-rs/iced/pull/1528) +- Operations not working for `PaneGrid`. [#1533](https://github.com/iced-rs/iced/pull/1533) +- Mapped `widget::Operation` always returning `Outcome::None`. [#1536](https://github.com/iced-rs/iced/pull/1536) +- Padding of `TextInput` with `Length::Units` width. [#1539](https://github.com/iced-rs/iced/pull/1539) +- Clipping of `Image` and `Svg` widgets in `iced_glow`. [#1557](https://github.com/iced-rs/iced/pull/1557) +- Invalid links in documentation. [#1560](https://github.com/iced-rs/iced/pull/1560) +- `Custom` style of `PickList` widget. [#1570](https://github.com/iced-rs/iced/pull/1570) +- Scroller in `Scrollable` always being drawn. [#1574](https://github.com/iced-rs/iced/pull/1574) + +Many thanks to... + +- @bungoboingo +- @l1Dan +- @mmstick +- @mtkennerly +- @PolyMeilex +- @rksm +- @rs017991 +- @tarkah +- @wash2 + +## [0.5.0] - 2022-11-10 +### Added +- __[Stabilization of stateless widgets][stateless]__ (#1393) + The old widget API has been completely replaced by stateless widgets (introduced in #1284). Alongside the new API, there are a bunch of new helper functions and macros for easily describing view logic (like `row!` and `column!`). + +- __[First-class theming][theming]__ (#1362) + A complete overhaul of our styling primitives, introducing a `Theme` as a first-class concept of the library. + +- __[Widget operations][operations]__ (#1399) + An abstraction that can be used to traverse (and operate on) the widget tree of an application in order to query or update some widget state. + +- __[`Lazy` widget][lazy]__ (#1400) + A widget that can call some view logic lazily only when some data has changed. Thanks to @nicksenger! + +- __[Linear gradient support for `Canvas`][gradient]__ (#1448) + The `Canvas` widget can draw linear gradients now. Thanks to @bungoboingo! + +- __[Touch support for `Canvas`][touch]__ (#1305) + The `Canvas` widget now supports touch events. Thanks to @artursapek! + +- __[`Image` and `Svg` support for `iced_glow`][image]__ (#1485) + Our OpenGL renderer now is capable of rendering both the `Image` and `Svg` widgets. Thanks to @ids1024! + +[stateless]: https://github.com/iced-rs/iced/pull/1393 +[theming]: https://github.com/iced-rs/iced/pull/1362 +[operations]: https://github.com/iced-rs/iced/pull/1399 +[lazy]: https://github.com/iced-rs/iced/pull/1400 +[gradient]: https://github.com/iced-rs/iced/pull/1448 +[touch]: https://github.com/iced-rs/iced/pull/1305 +[image]: https://github.com/iced-rs/iced/pull/1485 + ## [0.4.2] - 2022-05-03 ### Fixed - `Padding` type not exposed in `iced`. @@ -257,7 +321,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - First release! :tada: -[Unreleased]: https://github.com/iced-rs/iced/compare/0.4.2...HEAD +[Unreleased]: https://github.com/iced-rs/iced/compare/0.6.0...HEAD +[0.6.0]: https://github.com/iced-rs/iced/compare/0.5.0...0.6.0 +[0.5.0]: https://github.com/iced-rs/iced/compare/0.4.2...0.5.0 [0.4.2]: https://github.com/iced-rs/iced/compare/0.4.1...0.4.2 [0.4.1]: https://github.com/iced-rs/iced/compare/0.4.0...0.4.1 [0.4.0]: https://github.com/iced-rs/iced/compare/0.3.0...0.4.0 diff --git a/Cargo.toml b/Cargo.toml index e0eec17f..9c45b2f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced" -version = "0.4.2" +version = "0.6.0" authors = ["Héctor Ramón Jiménez "] edition = "2021" description = "A cross-platform GUI library inspired by Elm" @@ -10,14 +10,13 @@ documentation = "https://docs.rs/iced" readme = "README.md" keywords = ["gui", "ui", "graphics", "interface", "widgets"] categories = ["gui"] -resolver = "2" [features] default = ["wgpu"] # Enables the `Image` widget -image = ["iced_wgpu/image", "image_rs"] +image = ["iced_wgpu?/image", "iced_glow?/image", "image_rs"] # Enables the `Svg` widget -svg = ["iced_wgpu/svg"] +svg = ["iced_wgpu?/svg", "iced_glow?/svg"] # Enables the `Canvas` widget canvas = ["iced_graphics/canvas"] # Enables the `QRCode` widget @@ -25,11 +24,9 @@ qr_code = ["iced_graphics/qr_code"] # Enables the `iced_wgpu` renderer wgpu = ["iced_wgpu"] # Enables using system fonts -default_system_font = ["iced_wgpu/default_system_font"] +default_system_font = ["iced_wgpu?/default_system_font", "iced_glow?/default_system_font"] # Enables the `iced_glow` renderer. Overrides `iced_wgpu` glow = ["iced_glow", "iced_glutin"] -# Enables 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 @@ -58,61 +55,29 @@ members = [ "style", "wgpu", "winit", - "examples/arc", - "examples/bezier_tool", - "examples/clock", - "examples/color_palette", - "examples/component", - "examples/counter", - "examples/custom_widget", - "examples/download_progress", - "examples/events", - "examples/exit", - "examples/game_of_life", - "examples/geometry", - "examples/integration_opengl", - "examples/integration_wgpu", - "examples/multitouch", - "examples/pane_grid", - "examples/pick_list", - "examples/pokedex", - "examples/progress_bar", - "examples/qr_code", - "examples/scrollable", - "examples/sierpinski_triangle", - "examples/solar_system", - "examples/stopwatch", - "examples/styling", - "examples/svg", - "examples/system_information", - "examples/todos", - "examples/tooltip", - "examples/tour", - "examples/url_handler", - "examples/websocket", - "examples/slider", + "examples/*", ] [dependencies] -iced_core = { version = "0.5", path = "core" } -iced_futures = { version = "0.4", path = "futures" } -iced_native = { version = "0.5", path = "native" } -iced_graphics = { version = "0.3", path = "graphics" } -iced_winit = { version = "0.4", path = "winit", features = ["application"] } -iced_glutin = { version = "0.3", path = "glutin", optional = true } -iced_glow = { version = "0.3", path = "glow", optional = true } +iced_core = { version = "0.6", path = "core" } +iced_futures = { version = "0.5", path = "futures" } +iced_native = { version = "0.7", path = "native" } +iced_graphics = { version = "0.5", path = "graphics" } +iced_winit = { version = "0.6", path = "winit", features = ["application"] } +iced_glutin = { version = "0.5", path = "glutin", optional = true } +iced_glow = { version = "0.5", path = "glow", optional = true } thiserror = "1.0" [dependencies.image_rs] -version = "0.23" +version = "0.24" package = "image" optional = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -iced_wgpu = { version = "0.5", path = "wgpu", optional = true } +iced_wgpu = { version = "0.7", path = "wgpu", optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] -iced_wgpu = { version = "0.5", path = "wgpu", features = ["webgl"], optional = true } +iced_wgpu = { version = "0.7", path = "wgpu", features = ["webgl"], optional = true } [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] diff --git a/README.md b/README.md index dc065ada..bc7f5440 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ __Iced is currently experimental software.__ [Take a look at the roadmap], Add `iced` as a dependency in your `Cargo.toml`: ```toml -iced = "0.4" +iced = "0.6" ``` If your project is using a Rust edition older than 2021, then you will need to @@ -215,7 +215,7 @@ cargo run --features iced/glow --package game_of_life and then use it in your project with ```toml -iced = { version = "0.4", default-features = false, features = ["glow"] } +iced = { version = "0.6", default-features = false, features = ["glow"] } ``` __NOTE:__ Chances are you have hardware that supports at least OpenGL 2.1 or OpenGL ES 2.0, diff --git a/core/Cargo.toml b/core/Cargo.toml index c9c7686e..c401f30a 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_core" -version = "0.5.0" +version = "0.6.2" authors = ["Héctor Ramón Jiménez "] edition = "2021" description = "The essential concepts of Iced" diff --git a/core/src/color.rs b/core/src/color.rs index 212c1214..fe0a1856 100644 --- a/core/src/color.rs +++ b/core/src/color.rs @@ -89,6 +89,17 @@ impl Color { } } + /// Converts the [`Color`] into its RGBA8 equivalent. + #[must_use] + pub fn into_rgba8(self) -> [u8; 4] { + [ + (self.r * 255.0).round() as u8, + (self.g * 255.0).round() as u8, + (self.b * 255.0).round() as u8, + (self.a * 255.0).round() as u8, + ] + } + /// Converts the [`Color`] into its linear values. pub fn into_linear(self) -> [f32; 4] { // As described in: @@ -148,24 +159,26 @@ impl From<[f32; 4]> for Color { #[macro_export] macro_rules! color { ($r:expr, $g:expr, $b:expr) => { - Color::from_rgb8($r, $g, $b) + $crate::Color::from_rgb8($r, $g, $b) }; ($r:expr, $g:expr, $b:expr, $a:expr) => { - Color::from_rgba8($r, $g, $b, $a) + $crate::Color::from_rgba8($r, $g, $b, $a) }; ($hex:expr) => {{ let hex = $hex as u32; let r = (hex & 0xff0000) >> 16; let g = (hex & 0xff00) >> 8; let b = (hex & 0xff); - Color::from_rgb8(r as u8, g as u8, b as u8) + + $crate::Color::from_rgb8(r as u8, g as u8, b as u8) }}; ($hex:expr, $a:expr) => {{ let hex = $hex as u32; let r = (hex & 0xff0000) >> 16; let g = (hex & 0xff00) >> 8; let b = (hex & 0xff); - Color::from_rgba8(r as u8, g as u8, b as u8, $a) + + $crate::Color::from_rgba8(r as u8, g as u8, b as u8, $a) }}; } diff --git a/core/src/lib.rs b/core/src/lib.rs index 03ba8cca..f95d61f6 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -7,7 +7,7 @@ //! ![The foundations of the Iced ecosystem](https://github.com/iced-rs/iced/blob/0525d76ff94e828b7b21634fa94a747022001c83/docs/graphs/foundations.png?raw=true) //! //! [Iced]: https://github.com/iced-rs/iced -//! [`iced_native`]: https://github.com/iced-rs/iced/tree/0.4/native +//! [`iced_native`]: https://github.com/iced-rs/iced/tree/0.6/native //! [`iced_web`]: https://github.com/iced-rs/iced_web #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" diff --git a/core/src/padding.rs b/core/src/padding.rs index 22467d6b..140ad8ee 100644 --- a/core/src/padding.rs +++ b/core/src/padding.rs @@ -1,3 +1,5 @@ +use crate::Size; + /// An amount of space to pad for each side of a box /// /// You can leverage the `From` trait to build [`Padding`] conveniently: @@ -71,9 +73,21 @@ impl Padding { pub fn horizontal(self) -> u16 { self.left + self.right } + + /// Fits the [`Padding`] between the provided `inner` and `outer` [`Size`]. + pub fn fit(self, inner: Size, outer: Size) -> Self { + let available = (outer - inner).max(Size::ZERO); + + Padding { + top: self.top.min((available.height as u16) / 2), + right: self.right.min((available.width as u16) / 2), + bottom: self.bottom.min((available.height as u16) / 2), + left: self.left.min((available.width as u16) / 2), + } + } } -impl std::convert::From for Padding { +impl From for Padding { fn from(p: u16) -> Self { Padding { top: p, @@ -84,7 +98,7 @@ impl std::convert::From for Padding { } } -impl std::convert::From<[u16; 2]> for Padding { +impl From<[u16; 2]> for Padding { fn from(p: [u16; 2]) -> Self { Padding { top: p[0], @@ -95,7 +109,7 @@ impl std::convert::From<[u16; 2]> for Padding { } } -impl std::convert::From<[u16; 4]> for Padding { +impl From<[u16; 4]> for Padding { fn from(p: [u16; 4]) -> Self { Padding { top: p[0], diff --git a/core/src/size.rs b/core/src/size.rs index 2db33a88..31f3171b 100644 --- a/core/src/size.rs +++ b/core/src/size.rs @@ -34,6 +34,22 @@ impl Size { height: self.height + padding.vertical() as f32, } } + + /// Returns the minimum of each component of this size and another + pub fn min(self, other: Self) -> Self { + Size { + width: self.width.min(other.width), + height: self.height.min(other.height), + } + } + + /// Returns the maximum of each component of this size and another + pub fn max(self, other: Self) -> Self { + Size { + width: self.width.max(other.width), + height: self.height.max(other.height), + } + } } impl From<[f32; 2]> for Size { @@ -68,3 +84,14 @@ impl From for Vector { Vector::new(size.width, size.height) } } + +impl std::ops::Sub for Size { + type Output = Size; + + fn sub(self, rhs: Self) -> Self::Output { + Size { + width: self.width - rhs.width, + height: self.height - rhs.height, + } + } +} diff --git a/examples/arc/src/main.rs b/examples/arc/src/main.rs index 0c619dc9..7b6ea0e1 100644 --- a/examples/arc/src/main.rs +++ b/examples/arc/src/main.rs @@ -2,7 +2,7 @@ use std::{f32::consts::PI, time::Instant}; use iced::executor; use iced::widget::canvas::{ - self, Cache, Canvas, Cursor, Geometry, Path, Stroke, + self, stroke, Cache, Canvas, Cursor, Geometry, Path, Stroke, }; use iced::{ Application, Command, Element, Length, Point, Rectangle, Settings, @@ -52,11 +52,6 @@ impl Application for Arc { Command::none() } - fn subscription(&self) -> Subscription { - iced::time::every(std::time::Duration::from_millis(10)) - .map(|_| Message::Tick) - } - fn view(&self) -> Element { Canvas::new(self) .width(Length::Fill) @@ -67,6 +62,11 @@ impl Application for Arc { fn theme(&self) -> Theme { Theme::Dark } + + fn subscription(&self) -> Subscription { + iced::time::every(std::time::Duration::from_millis(10)) + .map(|_| Message::Tick) + } } impl canvas::Program for Arc { @@ -114,7 +114,7 @@ impl canvas::Program for Arc { frame.stroke( &path, Stroke { - color: palette.text, + style: stroke::Style::Solid(palette.text), width: 10.0, ..Stroke::default() }, diff --git a/examples/cached/Cargo.toml b/examples/cached/Cargo.toml new file mode 100644 index 00000000..2c7edde2 --- /dev/null +++ b/examples/cached/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "cached" +version = "0.1.0" +authors = ["Nick Senger "] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../..", features = ["debug"] } +iced_lazy = { path = "../../lazy" } diff --git a/examples/cached/src/main.rs b/examples/cached/src/main.rs new file mode 100644 index 00000000..8845b874 --- /dev/null +++ b/examples/cached/src/main.rs @@ -0,0 +1,139 @@ +use iced::theme; +use iced::widget::{ + button, column, horizontal_space, row, scrollable, text, text_input, +}; +use iced::{Element, Length, Sandbox, Settings}; +use iced_lazy::lazy; + +use std::collections::HashSet; + +pub fn main() -> iced::Result { + App::run(Settings::default()) +} + +struct App { + options: HashSet, + input: String, + order: Order, +} + +impl Default for App { + fn default() -> Self { + Self { + options: ["Foo", "Bar", "Baz", "Qux", "Corge", "Waldo", "Fred"] + .into_iter() + .map(ToString::to_string) + .collect(), + input: Default::default(), + order: Order::Ascending, + } + } +} + +#[derive(Debug, Clone)] +enum Message { + InputChanged(String), + ToggleOrder, + DeleteOption(String), + AddOption(String), +} + +impl Sandbox for App { + type Message = Message; + + fn new() -> Self { + Self::default() + } + + fn title(&self) -> String { + String::from("Cached - Iced") + } + + fn update(&mut self, message: Message) { + match message { + Message::InputChanged(input) => { + self.input = input; + } + Message::ToggleOrder => { + self.order = match self.order { + Order::Ascending => Order::Descending, + Order::Descending => Order::Ascending, + } + } + Message::AddOption(option) => { + self.options.insert(option); + self.input.clear(); + } + Message::DeleteOption(option) => { + self.options.remove(&option); + } + } + } + + fn view(&self) -> Element { + let options = lazy((&self.order, self.options.len()), || { + let mut options: Vec<_> = self.options.iter().collect(); + + options.sort_by(|a, b| match self.order { + Order::Ascending => a.to_lowercase().cmp(&b.to_lowercase()), + Order::Descending => b.to_lowercase().cmp(&a.to_lowercase()), + }); + + column( + options + .into_iter() + .map(|option| { + row![ + text(option), + horizontal_space(Length::Fill), + button("Delete") + .on_press(Message::DeleteOption( + option.to_string(), + ),) + .style(theme::Button::Destructive) + ] + .into() + }) + .collect(), + ) + .spacing(10) + }); + + column![ + scrollable(options).height(Length::Fill), + row![ + text_input( + "Add a new option", + &self.input, + Message::InputChanged, + ) + .on_submit(Message::AddOption(self.input.clone())), + button(text(format!("Toggle Order ({})", self.order))) + .on_press(Message::ToggleOrder) + ] + .spacing(10) + ] + .spacing(20) + .padding(20) + .into() + } +} + +#[derive(Debug, Hash)] +enum Order { + Ascending, + Descending, +} + +impl std::fmt::Display for Order { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Ascending => "Ascending", + Self::Descending => "Descending", + } + ) + } +} diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index 8818fb54..a389c54f 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -1,5 +1,7 @@ use iced::executor; -use iced::widget::canvas::{Cache, Cursor, Geometry, LineCap, Path, Stroke}; +use iced::widget::canvas::{ + stroke, Cache, Cursor, Geometry, LineCap, Path, Stroke, +}; use iced::widget::{canvas, container}; use iced::{ Application, Color, Command, Element, Length, Point, Rectangle, Settings, @@ -24,9 +26,9 @@ enum Message { } impl Application for Clock { + type Executor = executor::Default; type Message = Message; type Theme = Theme; - type Executor = executor::Default; type Flags = (); fn new(_flags: ()) -> (Self, Command) { @@ -59,15 +61,6 @@ impl Application for Clock { Command::none() } - fn subscription(&self) -> Subscription { - iced::time::every(std::time::Duration::from_millis(500)).map(|_| { - Message::Tick( - time::OffsetDateTime::now_local() - .unwrap_or_else(|_| time::OffsetDateTime::now_utc()), - ) - }) - } - fn view(&self) -> Element { let canvas = canvas(self as &Self) .width(Length::Fill) @@ -79,6 +72,15 @@ impl Application for Clock { .padding(20) .into() } + + fn subscription(&self) -> Subscription { + iced::time::every(std::time::Duration::from_millis(500)).map(|_| { + Message::Tick( + time::OffsetDateTime::now_local() + .unwrap_or_else(|_| time::OffsetDateTime::now_utc()), + ) + }) + } } impl canvas::Program for Clock { @@ -104,33 +106,41 @@ impl canvas::Program for Clock { let long_hand = Path::line(Point::ORIGIN, Point::new(0.0, -0.8 * radius)); - let thin_stroke = Stroke { - width: radius / 100.0, - color: Color::WHITE, - line_cap: LineCap::Round, - ..Stroke::default() + let width = radius / 100.0; + + let thin_stroke = || -> Stroke { + Stroke { + width, + style: stroke::Style::Solid(Color::WHITE), + line_cap: LineCap::Round, + ..Stroke::default() + } }; - let wide_stroke = Stroke { - width: thin_stroke.width * 3.0, - ..thin_stroke + let wide_stroke = || -> Stroke { + Stroke { + width: width * 3.0, + style: stroke::Style::Solid(Color::WHITE), + line_cap: LineCap::Round, + ..Stroke::default() + } }; frame.translate(Vector::new(center.x, center.y)); frame.with_save(|frame| { frame.rotate(hand_rotation(self.now.hour(), 12)); - frame.stroke(&short_hand, wide_stroke); + frame.stroke(&short_hand, wide_stroke()); }); frame.with_save(|frame| { frame.rotate(hand_rotation(self.now.minute(), 60)); - frame.stroke(&long_hand, wide_stroke); + frame.stroke(&long_hand, wide_stroke()); }); frame.with_save(|frame| { frame.rotate(hand_rotation(self.now.second(), 60)); - frame.stroke(&long_hand, thin_stroke); + frame.stroke(&long_hand, thin_stroke()); }) }); diff --git a/examples/custom_quad/Cargo.toml b/examples/custom_quad/Cargo.toml new file mode 100644 index 00000000..39154786 --- /dev/null +++ b/examples/custom_quad/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "custom_quad" +version = "0.1.0" +authors = ["Robert Krahn"] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../.." } +iced_native = { path = "../../native" } diff --git a/examples/custom_quad/src/main.rs b/examples/custom_quad/src/main.rs new file mode 100644 index 00000000..6509887c --- /dev/null +++ b/examples/custom_quad/src/main.rs @@ -0,0 +1,160 @@ +//! This example showcases a drawing a quad. +mod quad { + use iced_native::layout::{self, Layout}; + use iced_native::renderer; + use iced_native::widget::{self, Widget}; + use iced_native::{Color, Element, Length, Point, Rectangle, Size}; + + pub struct CustomQuad { + size: f32, + radius: [f32; 4], + border_width: f32, + } + + impl CustomQuad { + pub fn new(size: f32, radius: [f32; 4], border_width: f32) -> Self { + Self { + size, + radius, + border_width, + } + } + } + + impl Widget for CustomQuad + where + Renderer: renderer::Renderer, + { + fn width(&self) -> Length { + Length::Shrink + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + _renderer: &Renderer, + _limits: &layout::Limits, + ) -> layout::Node { + layout::Node::new(Size::new(self.size, self.size)) + } + + fn draw( + &self, + _state: &widget::Tree, + renderer: &mut Renderer, + _theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + _cursor_position: Point, + _viewport: &Rectangle, + ) { + renderer.fill_quad( + renderer::Quad { + bounds: layout.bounds(), + border_radius: self.radius.into(), + border_width: self.border_width, + border_color: Color::from_rgb(1.0, 0.0, 0.0), + }, + Color::BLACK, + ); + } + } + + impl<'a, Message, Renderer> From for Element<'a, Message, Renderer> + where + Renderer: renderer::Renderer, + { + fn from(circle: CustomQuad) -> Self { + Self::new(circle) + } + } +} + +use iced::widget::{column, container, slider, text}; +use iced::{Alignment, Element, Length, Sandbox, Settings}; + +pub fn main() -> iced::Result { + Example::run(Settings::default()) +} + +struct Example { + radius: [f32; 4], + border_width: f32, +} + +#[derive(Debug, Clone, Copy)] +#[allow(clippy::enum_variant_names)] +enum Message { + RadiusTopLeftChanged(f32), + RadiusTopRightChanged(f32), + RadiusBottomRightChanged(f32), + RadiusBottomLeftChanged(f32), + BorderWidthChanged(f32), +} + +impl Sandbox for Example { + type Message = Message; + + fn new() -> Self { + Self { + radius: [50.0; 4], + border_width: 0.0, + } + } + + fn title(&self) -> String { + String::from("Custom widget - Iced") + } + + fn update(&mut self, message: Message) { + let [tl, tr, br, bl] = self.radius; + match message { + Message::RadiusTopLeftChanged(radius) => { + self.radius = [radius, tr, br, bl]; + } + Message::RadiusTopRightChanged(radius) => { + self.radius = [tl, radius, br, bl]; + } + Message::RadiusBottomRightChanged(radius) => { + self.radius = [tl, tr, radius, bl]; + } + Message::RadiusBottomLeftChanged(radius) => { + self.radius = [tl, tr, br, radius]; + } + Message::BorderWidthChanged(width) => { + self.border_width = width; + } + } + } + + fn view(&self) -> Element { + let [tl, tr, br, bl] = self.radius; + + let content = column![ + quad::CustomQuad::new(200.0, self.radius, self.border_width), + text(format!("Radius: {tl:.2}/{tr:.2}/{br:.2}/{bl:.2}")), + slider(1.0..=100.0, tl, Message::RadiusTopLeftChanged).step(0.01), + slider(1.0..=100.0, tr, Message::RadiusTopRightChanged).step(0.01), + slider(1.0..=100.0, br, Message::RadiusBottomRightChanged) + .step(0.01), + slider(1.0..=100.0, bl, Message::RadiusBottomLeftChanged) + .step(0.01), + slider(1.0..=10.0, self.border_width, Message::BorderWidthChanged) + .step(0.01), + ] + .padding(20) + .spacing(20) + .max_width(500) + .align_items(Alignment::Center); + + container(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } +} diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs index c37a1a12..f6bb3b1e 100644 --- a/examples/custom_widget/src/main.rs +++ b/examples/custom_widget/src/main.rs @@ -61,7 +61,7 @@ mod circle { renderer.fill_quad( renderer::Quad { bounds: layout.bounds(), - border_radius: self.radius, + border_radius: self.radius.into(), border_width: 0.0, border_color: Color::TRANSPARENT, }, diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index d8b99ab3..9bacce7f 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -11,22 +11,18 @@ mod rainbow { // if you wish to, by creating your own `Renderer` trait, which could be // implemented by `iced_wgpu` and other renderers. use iced_graphics::renderer::{self, Renderer}; + use iced_graphics::triangle::ColoredVertex2D; use iced_graphics::{Backend, Primitive}; + use iced_native::layout; use iced_native::widget::{self, Widget}; use iced_native::{ - layout, Element, Layout, Length, Point, Rectangle, Size, Vector, + Element, Layout, Length, Point, Rectangle, Size, Vector, }; - #[derive(Default)] + #[derive(Debug, Clone, Copy, Default)] pub struct Rainbow; - impl Rainbow { - pub fn new() -> Self { - Self - } - } - pub fn rainbow() -> Rainbow { Rainbow } @@ -63,7 +59,7 @@ mod rainbow { cursor_position: Point, _viewport: &Rectangle, ) { - use iced_graphics::triangle::{Mesh2D, Vertex2D}; + use iced_graphics::triangle::Mesh2D; use iced_native::Renderer as _; let b = layout.bounds(); @@ -95,43 +91,43 @@ mod rainbow { let posn_bl = [0.0, b.height]; let posn_l = [0.0, b.height / 2.0]; - let mesh = Primitive::Mesh2D { + let mesh = Primitive::SolidMesh { size: b.size(), buffers: Mesh2D { vertices: vec![ - Vertex2D { + ColoredVertex2D { position: posn_center, color: [1.0, 1.0, 1.0, 1.0], }, - Vertex2D { + ColoredVertex2D { position: posn_tl, color: color_r, }, - Vertex2D { + ColoredVertex2D { position: posn_t, color: color_o, }, - Vertex2D { + ColoredVertex2D { position: posn_tr, color: color_y, }, - Vertex2D { + ColoredVertex2D { position: posn_r, color: color_g, }, - Vertex2D { + ColoredVertex2D { position: posn_br, color: color_gb, }, - Vertex2D { + ColoredVertex2D { position: posn_b, color: color_b, }, - Vertex2D { + ColoredVertex2D { position: posn_bl, color: color_i, }, - Vertex2D { + ColoredVertex2D { position: posn_l, color: color_v, }, @@ -166,7 +162,7 @@ mod rainbow { } use iced::widget::{column, container, scrollable}; -use iced::{Alignment, Element, Length, Sandbox, Settings}; +use iced::{Element, Length, Sandbox, Settings}; use rainbow::rainbow; pub fn main() -> iced::Result { @@ -179,7 +175,7 @@ impl Sandbox for Example { type Message = (); fn new() -> Self { - Example + Self } fn title(&self) -> String { @@ -202,8 +198,7 @@ impl Sandbox for Example { ] .padding(20) .spacing(20) - .max_width(500) - .align_items(Alignment::Start); + .max_width(500); let scrollable = scrollable(container(content).width(Length::Fill).center_x()); diff --git a/examples/lazy/Cargo.toml b/examples/lazy/Cargo.toml new file mode 100644 index 00000000..79255c25 --- /dev/null +++ b/examples/lazy/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "lazy" +version = "0.1.0" +authors = ["Nick Senger "] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../..", features = ["debug"] } +iced_lazy = { path = "../../lazy" } diff --git a/examples/lazy/src/main.rs b/examples/lazy/src/main.rs new file mode 100644 index 00000000..8845b874 --- /dev/null +++ b/examples/lazy/src/main.rs @@ -0,0 +1,139 @@ +use iced::theme; +use iced::widget::{ + button, column, horizontal_space, row, scrollable, text, text_input, +}; +use iced::{Element, Length, Sandbox, Settings}; +use iced_lazy::lazy; + +use std::collections::HashSet; + +pub fn main() -> iced::Result { + App::run(Settings::default()) +} + +struct App { + options: HashSet, + input: String, + order: Order, +} + +impl Default for App { + fn default() -> Self { + Self { + options: ["Foo", "Bar", "Baz", "Qux", "Corge", "Waldo", "Fred"] + .into_iter() + .map(ToString::to_string) + .collect(), + input: Default::default(), + order: Order::Ascending, + } + } +} + +#[derive(Debug, Clone)] +enum Message { + InputChanged(String), + ToggleOrder, + DeleteOption(String), + AddOption(String), +} + +impl Sandbox for App { + type Message = Message; + + fn new() -> Self { + Self::default() + } + + fn title(&self) -> String { + String::from("Cached - Iced") + } + + fn update(&mut self, message: Message) { + match message { + Message::InputChanged(input) => { + self.input = input; + } + Message::ToggleOrder => { + self.order = match self.order { + Order::Ascending => Order::Descending, + Order::Descending => Order::Ascending, + } + } + Message::AddOption(option) => { + self.options.insert(option); + self.input.clear(); + } + Message::DeleteOption(option) => { + self.options.remove(&option); + } + } + } + + fn view(&self) -> Element { + let options = lazy((&self.order, self.options.len()), || { + let mut options: Vec<_> = self.options.iter().collect(); + + options.sort_by(|a, b| match self.order { + Order::Ascending => a.to_lowercase().cmp(&b.to_lowercase()), + Order::Descending => b.to_lowercase().cmp(&a.to_lowercase()), + }); + + column( + options + .into_iter() + .map(|option| { + row![ + text(option), + horizontal_space(Length::Fill), + button("Delete") + .on_press(Message::DeleteOption( + option.to_string(), + ),) + .style(theme::Button::Destructive) + ] + .into() + }) + .collect(), + ) + .spacing(10) + }); + + column![ + scrollable(options).height(Length::Fill), + row![ + text_input( + "Add a new option", + &self.input, + Message::InputChanged, + ) + .on_submit(Message::AddOption(self.input.clone())), + button(text(format!("Toggle Order ({})", self.order))) + .on_press(Message::ToggleOrder) + ] + .spacing(10) + ] + .spacing(20) + .padding(20) + .into() + } +} + +#[derive(Debug, Hash)] +enum Order { + Ascending, + Descending, +} + +impl std::fmt::Display for Order { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Ascending => "Ascending", + Self::Descending => "Descending", + } + ) + } +} diff --git a/examples/modal/Cargo.toml b/examples/modal/Cargo.toml new file mode 100644 index 00000000..8770acac --- /dev/null +++ b/examples/modal/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "modal" +version = "0.1.0" +authors = ["tarkah "] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../..", features = [] } +iced_native = { path = "../../native" } diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs new file mode 100644 index 00000000..2f20795c --- /dev/null +++ b/examples/modal/src/main.rs @@ -0,0 +1,475 @@ +use iced::widget::{ + self, button, column, container, horizontal_space, row, text, text_input, +}; +use iced::{ + executor, keyboard, subscription, theme, Alignment, Application, Command, + Element, Event, Length, Settings, Subscription, +}; + +use self::modal::Modal; + +pub fn main() -> iced::Result { + App::run(Settings::default()) +} + +#[derive(Default)] +struct App { + show_modal: bool, + email: String, + password: String, +} + +#[derive(Debug, Clone)] +enum Message { + ShowModal, + HideModal, + Email(String), + Password(String), + Submit, + Event(Event), +} + +impl Application for App { + type Executor = executor::Default; + type Message = Message; + type Theme = iced::Theme; + type Flags = (); + + fn new(_flags: ()) -> (Self, Command) { + (App::default(), Command::none()) + } + + fn title(&self) -> String { + String::from("Modal - Iced") + } + + fn subscription(&self) -> Subscription { + subscription::events().map(Message::Event) + } + + fn update(&mut self, message: Message) -> Command { + match message { + Message::ShowModal => { + self.show_modal = true; + widget::focus_next() + } + Message::HideModal => { + self.hide_modal(); + Command::none() + } + Message::Email(email) => { + self.email = email; + Command::none() + } + Message::Password(password) => { + self.password = password; + Command::none() + } + Message::Submit => { + if !self.email.is_empty() && !self.password.is_empty() { + self.hide_modal(); + } + + Command::none() + } + Message::Event(event) => match event { + Event::Keyboard(keyboard::Event::KeyPressed { + key_code: keyboard::KeyCode::Tab, + modifiers, + }) => { + if modifiers.shift() { + widget::focus_previous() + } else { + widget::focus_next() + } + } + Event::Keyboard(keyboard::Event::KeyPressed { + key_code: keyboard::KeyCode::Escape, + .. + }) => { + self.hide_modal(); + Command::none() + } + _ => Command::none(), + }, + } + } + + fn view(&self) -> Element { + let content = container( + column![ + row![ + text("Top Left"), + horizontal_space(Length::Fill), + text("Top Right") + ] + .align_items(Alignment::Start) + .height(Length::Fill), + container( + button(text("Show Modal")).on_press(Message::ShowModal) + ) + .center_x() + .center_y() + .width(Length::Fill) + .height(Length::Fill), + row![ + text("Bottom Left"), + horizontal_space(Length::Fill), + text("Bottom Right") + ] + .align_items(Alignment::End) + .height(Length::Fill), + ] + .height(Length::Fill), + ) + .padding(10) + .width(Length::Fill) + .height(Length::Fill); + + if self.show_modal { + let modal = container( + column![ + text("Sign Up").size(24), + column![ + column![ + text("Email").size(12), + text_input( + "abc@123.com", + &self.email, + Message::Email + ) + .on_submit(Message::Submit) + .padding(5), + ] + .spacing(5), + column![ + text("Password").size(12), + text_input("", &self.password, Message::Password) + .on_submit(Message::Submit) + .password() + .padding(5), + ] + .spacing(5), + button(text("Submit")).on_press(Message::HideModal), + ] + .spacing(10) + ] + .spacing(20), + ) + .width(Length::Units(300)) + .padding(10) + .style(theme::Container::Box); + + Modal::new(content, modal) + .on_blur(Message::HideModal) + .into() + } else { + content.into() + } + } +} + +impl App { + fn hide_modal(&mut self) { + self.show_modal = false; + self.email.clear(); + self.password.clear(); + } +} + +mod modal { + use iced_native::alignment::Alignment; + use iced_native::widget::{self, Tree}; + use iced_native::{ + event, layout, mouse, overlay, renderer, Clipboard, Color, Element, + Event, Layout, Length, Point, Rectangle, Shell, Size, Widget, + }; + + /// A widget that centers a modal element over some base element + pub struct Modal<'a, Message, Renderer> { + base: Element<'a, Message, Renderer>, + modal: Element<'a, Message, Renderer>, + on_blur: Option, + } + + impl<'a, Message, Renderer> Modal<'a, Message, Renderer> { + /// Returns a new [`Modal`] + pub fn new( + base: impl Into>, + modal: impl Into>, + ) -> Self { + Self { + base: base.into(), + modal: modal.into(), + on_blur: None, + } + } + + /// Sets the message that will be produces when the background + /// of the [`Modal`] is pressed + pub fn on_blur(self, on_blur: Message) -> Self { + Self { + on_blur: Some(on_blur), + ..self + } + } + } + + impl<'a, Message, Renderer> Widget + for Modal<'a, Message, Renderer> + where + Renderer: iced_native::Renderer, + Message: Clone, + { + fn children(&self) -> Vec { + vec![Tree::new(&self.base), Tree::new(&self.modal)] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(&[&self.base, &self.modal]); + } + + fn width(&self) -> Length { + self.base.as_widget().width() + } + + fn height(&self) -> Length { + self.base.as_widget().height() + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.base.as_widget().layout(renderer, limits) + } + + fn on_event( + &mut self, + state: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + self.base.as_widget_mut().on_event( + &mut state.children[0], + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + } + + fn draw( + &self, + state: &Tree, + renderer: &mut Renderer, + theme: &::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + self.base.as_widget().draw( + &state.children[0], + renderer, + theme, + style, + layout, + cursor_position, + viewport, + ); + } + + fn overlay<'b>( + &'b mut self, + state: &'b mut Tree, + layout: Layout<'_>, + _renderer: &Renderer, + ) -> Option> { + Some(overlay::Element::new( + layout.position(), + Box::new(Overlay { + content: &mut self.modal, + tree: &mut state.children[1], + size: layout.bounds().size(), + on_blur: self.on_blur.clone(), + }), + )) + } + + fn mouse_interaction( + &self, + state: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.base.as_widget().mouse_interaction( + &state.children[0], + layout, + cursor_position, + viewport, + renderer, + ) + } + + fn operate( + &self, + state: &mut Tree, + layout: Layout<'_>, + operation: &mut dyn widget::Operation, + ) { + self.base.as_widget().operate( + &mut state.children[0], + layout, + operation, + ); + } + } + + struct Overlay<'a, 'b, Message, Renderer> { + content: &'b mut Element<'a, Message, Renderer>, + tree: &'b mut Tree, + size: Size, + on_blur: Option, + } + + impl<'a, 'b, Message, Renderer> overlay::Overlay + for Overlay<'a, 'b, Message, Renderer> + where + Renderer: iced_native::Renderer, + Message: Clone, + { + fn layout( + &self, + renderer: &Renderer, + _bounds: Size, + position: Point, + ) -> layout::Node { + let limits = layout::Limits::new(Size::ZERO, self.size) + .width(Length::Fill) + .height(Length::Fill); + + let mut child = self.content.as_widget().layout(renderer, &limits); + child.align(Alignment::Center, Alignment::Center, limits.max()); + + let mut node = layout::Node::with_children(self.size, vec![child]); + node.move_to(position); + + node + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + let content_bounds = layout.children().next().unwrap().bounds(); + + if let Some(message) = self.on_blur.as_ref() { + if let Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + )) = &event + { + if !content_bounds.contains(cursor_position) { + shell.publish(message.clone()); + return event::Status::Captured; + } + } + } + + self.content.as_widget_mut().on_event( + self.tree, + event, + layout.children().next().unwrap(), + cursor_position, + renderer, + clipboard, + shell, + ) + } + + fn draw( + &self, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + ) { + renderer.fill_quad( + renderer::Quad { + bounds: layout.bounds(), + border_radius: renderer::BorderRadius::from(0.0), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + Color { + a: 0.80, + ..Color::BLACK + }, + ); + + self.content.as_widget().draw( + self.tree, + renderer, + theme, + style, + layout.children().next().unwrap(), + cursor_position, + &layout.bounds(), + ); + } + + fn operate( + &mut self, + layout: Layout<'_>, + operation: &mut dyn widget::Operation, + ) { + self.content.as_widget().operate( + self.tree, + layout.children().next().unwrap(), + operation, + ); + } + + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.content.as_widget().mouse_interaction( + self.tree, + layout.children().next().unwrap(), + cursor_position, + viewport, + renderer, + ) + } + } + + impl<'a, Message, Renderer> From> + for Element<'a, Message, Renderer> + where + Renderer: 'a + iced_native::Renderer, + Message: 'a + Clone, + { + fn from(modal: Modal<'a, Message, Renderer>) -> Self { + Element::new(modal) + } + } +} diff --git a/examples/modern_art/Cargo.toml b/examples/modern_art/Cargo.toml new file mode 100644 index 00000000..4242d209 --- /dev/null +++ b/examples/modern_art/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "modern_art" +version = "0.1.0" +authors = ["Bingus "] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../..", features = ["canvas", "tokio", "debug"] } +rand = "0.8.5" +env_logger = "0.9" diff --git a/examples/modern_art/src/main.rs b/examples/modern_art/src/main.rs new file mode 100644 index 00000000..28ed3e21 --- /dev/null +++ b/examples/modern_art/src/main.rs @@ -0,0 +1,142 @@ +use iced::widget::canvas::{ + self, gradient::Location, gradient::Position, Cache, Canvas, Cursor, Frame, + Geometry, Gradient, +}; +use iced::{ + executor, Application, Color, Command, Element, Length, Point, Rectangle, + Renderer, Settings, Size, Theme, +}; +use rand::{thread_rng, Rng}; + +fn main() -> iced::Result { + env_logger::builder().format_timestamp(None).init(); + + ModernArt::run(Settings { + antialiasing: true, + ..Settings::default() + }) +} + +#[derive(Debug, Clone, Copy)] +enum Message {} + +struct ModernArt { + cache: Cache, +} + +impl Application for ModernArt { + type Executor = executor::Default; + type Message = Message; + type Theme = Theme; + type Flags = (); + + fn new(_flags: Self::Flags) -> (Self, Command) { + ( + ModernArt { + cache: Default::default(), + }, + Command::none(), + ) + } + + fn title(&self) -> String { + String::from("Modern Art") + } + + fn update(&mut self, _message: Message) -> Command { + Command::none() + } + + fn view(&self) -> Element<'_, Self::Message, Renderer> { + Canvas::new(self) + .width(Length::Fill) + .height(Length::Fill) + .into() + } +} + +impl canvas::Program for ModernArt { + type State = (); + + fn draw( + &self, + _state: &Self::State, + _theme: &Theme, + bounds: Rectangle, + _cursor: Cursor, + ) -> Vec { + let geometry = self.cache.draw(bounds.size(), |frame| { + let num_squares = thread_rng().gen_range(0..1200); + + let mut i = 0; + while i <= num_squares { + generate_box(frame, bounds.size()); + i += 1; + } + }); + + vec![geometry] + } +} + +fn random_direction() -> Location { + match thread_rng().gen_range(0..8) { + 0 => Location::TopLeft, + 1 => Location::Top, + 2 => Location::TopRight, + 3 => Location::Right, + 4 => Location::BottomRight, + 5 => Location::Bottom, + 6 => Location::BottomLeft, + 7 => Location::Left, + _ => Location::TopLeft, + } +} + +fn generate_box(frame: &mut Frame, bounds: Size) -> bool { + let solid = rand::random::(); + + let random_color = || -> Color { + Color::from_rgb( + thread_rng().gen_range(0.0..1.0), + thread_rng().gen_range(0.0..1.0), + thread_rng().gen_range(0.0..1.0), + ) + }; + + let gradient = |top_left: Point, size: Size| -> Gradient { + let mut builder = Gradient::linear(Position::Relative { + top_left, + size, + start: random_direction(), + end: random_direction(), + }); + let stops = thread_rng().gen_range(1..15u32); + + let mut i = 0; + while i <= stops { + builder = builder.add_stop(i as f32 / stops as f32, random_color()); + i += 1; + } + + builder.build().unwrap() + }; + + let top_left = Point::new( + thread_rng().gen_range(0.0..bounds.width), + thread_rng().gen_range(0.0..bounds.height), + ); + + let size = Size::new( + thread_rng().gen_range(50.0..200.0), + thread_rng().gen_range(50.0..200.0), + ); + + if solid { + frame.fill_rectangle(top_left, size, random_color()); + } else { + frame.fill_rectangle(top_left, size, gradient(top_left, size)); + }; + + solid +} diff --git a/examples/multitouch/src/main.rs b/examples/multitouch/src/main.rs index 0345ceb7..f5faae0f 100644 --- a/examples/multitouch/src/main.rs +++ b/examples/multitouch/src/main.rs @@ -2,7 +2,8 @@ //! a circle around each fingertip. This only works on touch-enabled //! computers like Microsoft Surface. use iced::widget::canvas::event; -use iced::widget::canvas::{self, Canvas, Cursor, Geometry, Stroke}; +use iced::widget::canvas::stroke::{self, Stroke}; +use iced::widget::canvas::{self, Canvas, Cursor, Geometry}; use iced::{ executor, touch, window, Application, Color, Command, Element, Length, Point, Rectangle, Settings, Subscription, Theme, @@ -186,7 +187,7 @@ impl canvas::Program for State { frame.stroke( &path, Stroke { - color: Color::BLACK, + style: stroke::Style::Solid(Color::BLACK), width: 3.0, ..Stroke::default() }, diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs index ae8fa22b..c9f1376c 100644 --- a/examples/pane_grid/src/main.rs +++ b/examples/pane_grid/src/main.rs @@ -29,6 +29,8 @@ enum Message { Dragged(pane_grid::DragEvent), Resized(pane_grid::ResizeEvent), TogglePin(pane_grid::Pane), + Maximize(pane_grid::Pane), + Restore, Close(pane_grid::Pane), CloseFocused, } @@ -114,6 +116,10 @@ impl Application for Example { *is_pinned = !*is_pinned; } } + Message::Maximize(pane) => self.panes.maximize(&pane), + Message::Restore => { + self.panes.restore(); + } Message::Close(pane) => { if let Some((_, sibling)) = self.panes.close(&pane) { self.focus = Some(sibling); @@ -157,7 +163,7 @@ impl Application for Example { let focus = self.focus; let total_panes = self.panes.len(); - let pane_grid = PaneGrid::new(&self.panes, |id, pane| { + let pane_grid = PaneGrid::new(&self.panes, |id, pane, is_maximized| { let is_focused = focus == Some(id); let pin_button = button( @@ -178,7 +184,12 @@ impl Application for Example { .spacing(5); let title_bar = pane_grid::TitleBar::new(title) - .controls(view_controls(id, total_panes, pane.is_pinned)) + .controls(view_controls( + id, + total_panes, + pane.is_pinned, + is_maximized, + )) .padding(10) .style(if is_focused { style::title_bar_focused @@ -314,16 +325,35 @@ fn view_controls<'a>( pane: pane_grid::Pane, total_panes: usize, is_pinned: bool, + is_maximized: bool, ) -> Element<'a, Message> { - let mut button = button(text("Close").size(14)) + let mut row = row![].spacing(5); + + if total_panes > 1 { + let toggle = { + let (content, message) = if is_maximized { + ("Restore", Message::Restore) + } else { + ("Maximize", Message::Maximize(pane)) + }; + button(text(content).size(14)) + .style(theme::Button::Secondary) + .padding(3) + .on_press(message) + }; + + row = row.push(toggle); + } + + let mut close = button(text("Close").size(14)) .style(theme::Button::Destructive) .padding(3); if total_panes > 1 && !is_pinned { - button = button.on_press(Message::Close(pane)); + close = close.on_press(Message::Close(pane)); } - button.into() + row.push(close).into() } mod style { diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index b7b3dedc..6eba34e2 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -14,9 +14,15 @@ struct ScrollableDemo { variants: Vec, } +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +enum ThemeType { + Light, + Dark, +} + #[derive(Debug, Clone)] enum Message { - ThemeChanged(Theme), + ThemeChanged(ThemeType), ScrollToTop(usize), ScrollToBottom(usize), Scrolled(usize, f32), @@ -45,7 +51,10 @@ impl Application for ScrollableDemo { fn update(&mut self, message: Message) -> Command { match message { Message::ThemeChanged(theme) => { - self.theme = theme; + self.theme = match theme { + ThemeType::Light => Theme::Light, + ThemeType::Dark => Theme::Dark, + }; Command::none() } @@ -78,17 +87,15 @@ impl Application for ScrollableDemo { } fn view(&self) -> Element { - let ScrollableDemo { - theme, variants, .. - } = self; + let ScrollableDemo { variants, .. } = self; - let choose_theme = [Theme::Light, Theme::Dark].iter().fold( + let choose_theme = [ThemeType::Light, ThemeType::Dark].iter().fold( column!["Choose a theme:"].spacing(10), |column, option| { column.push(radio( format!("{:?}", option), *option, - Some(*theme), + Some(*option), Message::ThemeChanged, )) }, @@ -198,7 +205,7 @@ impl Application for ScrollableDemo { } fn theme(&self) -> Theme { - self.theme + self.theme.clone() } } diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index c59d73a8..9e303576 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -11,7 +11,9 @@ use iced::executor; use iced::theme::{self, Theme}; use iced::time; use iced::widget::canvas; -use iced::widget::canvas::{Cursor, Path, Stroke}; +use iced::widget::canvas::gradient::{self, Gradient}; +use iced::widget::canvas::stroke::{self, Stroke}; +use iced::widget::canvas::{Cursor, Path}; use iced::window; use iced::{ Application, Color, Command, Element, Length, Point, Rectangle, Settings, @@ -37,9 +39,9 @@ enum Message { } impl Application for SolarSystem { + type Executor = executor::Default; type Message = Message; type Theme = Theme; - type Executor = executor::Default; type Flags = (); fn new(_flags: ()) -> (Self, Command) { @@ -65,10 +67,6 @@ impl Application for SolarSystem { Command::none() } - fn subscription(&self) -> Subscription { - time::every(std::time::Duration::from_millis(10)).map(Message::Tick) - } - fn view(&self) -> Element { canvas(&self.state) .width(Length::Fill) @@ -81,10 +79,18 @@ impl Application for SolarSystem { } fn style(&self) -> theme::Application { - theme::Application::Custom(|_theme| application::Appearance { - background_color: Color::BLACK, - text_color: Color::WHITE, - }) + fn dark_background(_theme: &Theme) -> application::Appearance { + application::Appearance { + background_color: Color::BLACK, + text_color: Color::WHITE, + } + } + + theme::Application::from(dark_background as fn(&Theme) -> _) + } + + fn subscription(&self) -> Subscription { + time::every(time::Duration::from_millis(10)).map(Message::Tick) } } @@ -178,8 +184,10 @@ impl canvas::Program for State { frame.stroke( &orbit, Stroke { + style: stroke::Style::Solid(Color::from_rgba8( + 0, 153, 255, 0.1, + )), width: 1.0, - color: Color::from_rgba8(0, 153, 255, 0.1), line_dash: canvas::LineDash { offset: 0, segments: &[3.0, 6.0], @@ -198,15 +206,18 @@ impl canvas::Program for State { 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.fill(&earth, Color::from_rgb8(0x6B, 0x93, 0xD6)); + let earth_fill = + Gradient::linear(gradient::Position::Absolute { + start: Point::new(-Self::EARTH_RADIUS, 0.0), + end: Point::new(Self::EARTH_RADIUS, 0.0), + }) + .add_stop(0.2, Color::from_rgb(0.15, 0.50, 1.0)) + .add_stop(0.8, Color::from_rgb(0.0, 0.20, 0.47)) + .build() + .expect("Build Earth fill gradient"); + + frame.fill(&earth, earth_fill); frame.with_save(|frame| { frame.rotate(rotation * 10.0); @@ -215,14 +226,6 @@ impl canvas::Program for State { let moon = Path::circle(Point::ORIGIN, Self::MOON_RADIUS); frame.fill(&moon, Color::WHITE); }); - - frame.fill( - &shadow, - Color { - a: 0.7, - ..Color::BLACK - }, - ); }); }); diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index cda53e87..e16860ad 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -1,9 +1,10 @@ +use iced::theme::{self, Theme}; use iced::widget::{ button, checkbox, column, container, horizontal_rule, progress_bar, radio, row, scrollable, slider, text, text_input, toggler, vertical_rule, vertical_space, }; -use iced::{Alignment, Element, Length, Sandbox, Settings, Theme}; +use iced::{Alignment, Color, Element, Length, Sandbox, Settings}; pub fn main() -> iced::Result { Styling::run(Settings::default()) @@ -18,9 +19,16 @@ struct Styling { toggler_value: bool, } +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +enum ThemeType { + Light, + Dark, + Custom, +} + #[derive(Debug, Clone)] enum Message { - ThemeChanged(Theme), + ThemeChanged(ThemeType), InputChanged(String), ButtonPressed, SliderChanged(f32), @@ -41,7 +49,19 @@ impl Sandbox for Styling { fn update(&mut self, message: Message) { match message { - Message::ThemeChanged(theme) => self.theme = theme, + Message::ThemeChanged(theme) => { + self.theme = match theme { + ThemeType::Light => Theme::Light, + ThemeType::Dark => Theme::Dark, + ThemeType::Custom => Theme::custom(theme::Palette { + background: Color::from_rgb(1.0, 0.9, 1.0), + text: Color::BLACK, + primary: Color::from_rgb(0.5, 0.5, 0.0), + success: Color::from_rgb(0.0, 1.0, 0.0), + danger: Color::from_rgb(1.0, 0.0, 0.0), + }), + } + } Message::InputChanged(value) => self.input_value = value, Message::ButtonPressed => {} Message::SliderChanged(value) => self.slider_value = value, @@ -51,17 +71,24 @@ impl Sandbox for Styling { } fn view(&self) -> Element { - let choose_theme = [Theme::Light, Theme::Dark].iter().fold( - column![text("Choose a theme:")].spacing(10), - |column, theme| { - column.push(radio( - format!("{:?}", theme), - *theme, - Some(self.theme), - Message::ThemeChanged, - )) - }, - ); + let choose_theme = + [ThemeType::Light, ThemeType::Dark, ThemeType::Custom] + .iter() + .fold( + column![text("Choose a theme:")].spacing(10), + |column, theme| { + column.push(radio( + format!("{:?}", theme), + *theme, + Some(match self.theme { + Theme::Light => ThemeType::Light, + Theme::Dark => ThemeType::Dark, + Theme::Custom { .. } => ThemeType::Custom, + }), + Message::ThemeChanged, + )) + }, + ); let text_input = text_input( "Type something...", @@ -132,6 +159,6 @@ impl Sandbox for Styling { } fn theme(&self) -> Theme { - self.theme + self.theme.clone() } } diff --git a/examples/svg/src/main.rs b/examples/svg/src/main.rs index 27d175da..4dc92416 100644 --- a/examples/svg/src/main.rs +++ b/examples/svg/src/main.rs @@ -1,39 +1,76 @@ -use iced::widget::{container, svg}; -use iced::{Element, Length, Sandbox, Settings}; +use iced::theme; +use iced::widget::{checkbox, column, container, svg}; +use iced::{color, Element, Length, Sandbox, Settings}; pub fn main() -> iced::Result { Tiger::run(Settings::default()) } -struct Tiger; +#[derive(Debug, Default)] +struct Tiger { + apply_color_filter: bool, +} + +#[derive(Debug, Clone, Copy)] +pub enum Message { + ToggleColorFilter(bool), +} impl Sandbox for Tiger { - type Message = (); + type Message = Message; fn new() -> Self { - Tiger + Tiger::default() } fn title(&self) -> String { String::from("SVG - Iced") } - fn update(&mut self, _message: ()) {} + fn update(&mut self, message: Self::Message) { + match message { + Message::ToggleColorFilter(apply_color_filter) => { + self.apply_color_filter = apply_color_filter; + } + } + } - fn view(&self) -> Element<()> { - let svg = svg(svg::Handle::from_path(format!( + fn view(&self) -> Element { + let handle = svg::Handle::from_path(format!( "{}/resources/tiger.svg", env!("CARGO_MANIFEST_DIR") - ))) - .width(Length::Fill) - .height(Length::Fill); + )); - container(svg) + let svg = svg(handle).width(Length::Fill).height(Length::Fill).style( + if self.apply_color_filter { + theme::Svg::custom_fn(|_theme| svg::Appearance { + color: Some(color!(0x0000ff)), + }) + } else { + theme::Svg::Default + }, + ); + + let apply_color_filter = checkbox( + "Apply a color filter", + self.apply_color_filter, + Message::ToggleColorFilter, + ); + + container( + column![ + svg, + container(apply_color_filter).width(Length::Fill).center_x() + ] + .spacing(20) .width(Length::Fill) - .height(Length::Fill) - .padding(20) - .center_x() - .center_y() - .into() + .height(Length::Fill), + ) + .width(Length::Fill) + .height(Length::Fill) + .padding(20) + .center_x() + .center_y() + .into() } } diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index be48ae8c..690d9c09 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -131,7 +131,11 @@ impl Application for Todos { task.update(task_message); if should_focus { - text_input::focus(Task::text_input_id(i)) + let id = Task::text_input_id(i); + Command::batch(vec![ + text_input::focus(id.clone()), + text_input::select_all(id), + ]) } else { Command::none() } diff --git a/futures/Cargo.toml b/futures/Cargo.toml index 61ee00a5..b69cb59b 100644 --- a/futures/Cargo.toml +++ b/futures/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_futures" -version = "0.4.1" +version = "0.5.1" authors = ["Héctor Ramón Jiménez "] edition = "2021" description = "Commands, subscriptions, and runtimes for Iced" diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs index 0479c63c..1d4b68a6 100644 --- a/futures/src/subscription.rs +++ b/futures/src/subscription.rs @@ -125,9 +125,9 @@ impl std::fmt::Debug for Subscription { /// - [`stopwatch`], a watch with start/stop and reset buttons showcasing how /// to listen to time. /// -/// [examples]: https://github.com/iced-rs/iced/tree/0.4/examples -/// [`download_progress`]: https://github.com/iced-rs/iced/tree/0.4/examples/download_progress -/// [`stopwatch`]: https://github.com/iced-rs/iced/tree/0.4/examples/stopwatch +/// [examples]: https://github.com/iced-rs/iced/tree/0.6/examples +/// [`download_progress`]: https://github.com/iced-rs/iced/tree/0.6/examples/download_progress +/// [`stopwatch`]: https://github.com/iced-rs/iced/tree/0.6/examples/stopwatch pub trait Recipe { /// The events that will be produced by a [`Subscription`] with this /// [`Recipe`]. diff --git a/glow/Cargo.toml b/glow/Cargo.toml index 18215e9b..f69ce48a 100644 --- a/glow/Cargo.toml +++ b/glow/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_glow" -version = "0.3.0" +version = "0.5.0" authors = ["Héctor Ramón Jiménez "] edition = "2021" description = "A glow renderer for iced" @@ -8,12 +8,22 @@ license = "MIT AND OFL-1.1" repository = "https://github.com/iced-rs/iced" [features] +svg = ["iced_graphics/svg"] +image = ["iced_graphics/image"] +png = ["iced_graphics/png"] +jpeg = ["iced_graphics/jpeg"] +jpeg_rayon = ["iced_graphics/jpeg_rayon"] +gif = ["iced_graphics/gif"] +webp = ["iced_graphics/webp"] +pnm = ["iced_graphics/pnm"] +ico = ["iced_graphics/ico"] +bmp = ["iced_graphics/bmp"] +hdr = ["iced_graphics/hdr"] +dds = ["iced_graphics/dds"] +farbfeld = ["iced_graphics/farbfeld"] canvas = ["iced_graphics/canvas"] qr_code = ["iced_graphics/qr_code"] default_system_font = ["iced_graphics/font-source"] -# Not supported yet! -image = [] -svg = [] [dependencies] glow = "0.11.1" @@ -24,11 +34,11 @@ bytemuck = "1.4" log = "0.4" [dependencies.iced_native] -version = "0.5" +version = "0.7" path = "../native" [dependencies.iced_graphics] -version = "0.3" +version = "0.5" path = "../graphics" features = ["font-fallback", "font-icons", "opengl"] diff --git a/glow/src/backend.rs b/glow/src/backend.rs index 78d4229e..416c3b94 100644 --- a/glow/src/backend.rs +++ b/glow/src/backend.rs @@ -1,7 +1,8 @@ -use crate::program; +#[cfg(any(feature = "image", feature = "svg"))] +use crate::image; use crate::quad; use crate::text; -use crate::triangle; +use crate::{program, triangle}; use crate::{Settings, Transformation, Viewport}; use iced_graphics::backend; @@ -16,6 +17,8 @@ use iced_native::{Font, Size}; /// [`iced`]: https://github.com/iced-rs/iced #[derive(Debug)] pub struct Backend { + #[cfg(any(feature = "image", feature = "svg"))] + image_pipeline: image::Pipeline, quad_pipeline: quad::Pipeline, text_pipeline: text::Pipeline, triangle_pipeline: triangle::Pipeline, @@ -33,10 +36,14 @@ impl Backend { let shader_version = program::Version::new(gl); + #[cfg(any(feature = "image", feature = "svg"))] + let image_pipeline = image::Pipeline::new(gl, &shader_version); let quad_pipeline = quad::Pipeline::new(gl, &shader_version); let triangle_pipeline = triangle::Pipeline::new(gl, &shader_version); Self { + #[cfg(any(feature = "image", feature = "svg"))] + image_pipeline, quad_pipeline, text_pipeline, triangle_pipeline, @@ -71,6 +78,9 @@ impl Backend { viewport_size.height, ); } + + #[cfg(any(feature = "image", feature = "svg"))] + self.image_pipeline.trim_cache(gl); } fn flush( @@ -105,11 +115,26 @@ impl Backend { * Transformation::scale(scale_factor, scale_factor); self.triangle_pipeline.draw( + &layer.meshes, gl, 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( + gl, + target_height, + scaled, + scale_factor, + &layer.images, + bounds, ); } @@ -239,8 +264,8 @@ impl backend::Text for Backend { #[cfg(feature = "image")] impl backend::Image for Backend { - fn dimensions(&self, _handle: &iced_native::image::Handle) -> (u32, u32) { - (50, 50) + fn dimensions(&self, handle: &iced_native::image::Handle) -> Size { + self.image_pipeline.dimensions(handle) } } @@ -248,8 +273,8 @@ impl backend::Image for Backend { impl backend::Svg for Backend { fn viewport_dimensions( &self, - _handle: &iced_native::svg::Handle, - ) -> (u32, u32) { - (50, 50) + handle: &iced_native::svg::Handle, + ) -> Size { + self.image_pipeline.viewport_dimensions(handle) } } diff --git a/glow/src/image.rs b/glow/src/image.rs new file mode 100644 index 00000000..521a01e7 --- /dev/null +++ b/glow/src/image.rs @@ -0,0 +1,248 @@ +mod storage; + +use storage::Storage; + +pub use iced_graphics::triangle::{Mesh2D, Vertex2D}; + +use crate::program::{self, Shader}; +use crate::Transformation; + +#[cfg(feature = "image")] +use iced_graphics::image::raster; + +#[cfg(feature = "svg")] +use iced_graphics::image::vector; + +use iced_graphics::layer; +use iced_graphics::Rectangle; +use iced_graphics::Size; + +use glow::HasContext; + +use std::cell::RefCell; + +#[derive(Debug)] +pub(crate) struct Pipeline { + program: ::Program, + vertex_array: ::VertexArray, + vertex_buffer: ::Buffer, + transform_location: ::UniformLocation, + storage: Storage, + #[cfg(feature = "image")] + raster_cache: RefCell>, + #[cfg(feature = "svg")] + vector_cache: RefCell>, +} + +impl Pipeline { + pub fn new( + gl: &glow::Context, + shader_version: &program::Version, + ) -> Pipeline { + let program = unsafe { + let vertex_shader = Shader::vertex( + gl, + shader_version, + include_str!("shader/common/image.vert"), + ); + let fragment_shader = Shader::fragment( + gl, + shader_version, + include_str!("shader/common/image.frag"), + ); + + program::create( + gl, + &[vertex_shader, fragment_shader], + &[(0, "i_Position")], + ) + }; + + 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_buffer = + unsafe { gl.create_buffer().expect("Create vertex buffer") }; + let vertex_array = + unsafe { gl.create_vertex_array().expect("Create vertex array") }; + + unsafe { + gl.bind_vertex_array(Some(vertex_array)); + gl.bind_buffer(glow::ARRAY_BUFFER, Some(vertex_buffer)); + + let vertices = &[0u8, 0, 1, 0, 0, 1, 1, 1]; + gl.buffer_data_size( + glow::ARRAY_BUFFER, + vertices.len() as i32, + glow::STATIC_DRAW, + ); + gl.buffer_sub_data_u8_slice( + glow::ARRAY_BUFFER, + 0, + bytemuck::cast_slice(vertices), + ); + + gl.enable_vertex_attrib_array(0); + gl.vertex_attrib_pointer_f32( + 0, + 2, + glow::UNSIGNED_BYTE, + false, + 0, + 0, + ); + + gl.bind_buffer(glow::ARRAY_BUFFER, None); + gl.bind_vertex_array(None); + } + + Pipeline { + program, + vertex_array, + vertex_buffer, + transform_location, + storage: Storage::default(), + #[cfg(feature = "image")] + raster_cache: RefCell::new(raster::Cache::default()), + #[cfg(feature = "svg")] + vector_cache: RefCell::new(vector::Cache::default()), + } + } + + #[cfg(feature = "image")] + pub fn dimensions(&self, handle: &iced_native::image::Handle) -> Size { + self.raster_cache.borrow_mut().load(handle).dimensions() + } + + #[cfg(feature = "svg")] + pub fn viewport_dimensions( + &self, + handle: &iced_native::svg::Handle, + ) -> Size { + let mut cache = self.vector_cache.borrow_mut(); + let svg = cache.load(handle); + + svg.viewport_dimensions() + } + + pub fn draw( + &mut self, + mut gl: &glow::Context, + target_height: u32, + transformation: Transformation, + _scale_factor: f32, + images: &[layer::Image], + layer_bounds: Rectangle, + ) { + unsafe { + gl.use_program(Some(self.program)); + gl.bind_vertex_array(Some(self.vertex_array)); + gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.vertex_buffer)); + gl.enable(glow::SCISSOR_TEST); + } + + #[cfg(feature = "image")] + let mut raster_cache = self.raster_cache.borrow_mut(); + + #[cfg(feature = "svg")] + let mut vector_cache = self.vector_cache.borrow_mut(); + + for image in images { + let (entry, bounds) = match &image { + #[cfg(feature = "image")] + layer::Image::Raster { handle, bounds } => ( + raster_cache.upload(handle, &mut gl, &mut self.storage), + bounds, + ), + #[cfg(not(feature = "image"))] + layer::Image::Raster { handle: _, bounds } => (None, bounds), + + #[cfg(feature = "svg")] + layer::Image::Vector { + handle, + color, + bounds, + } => { + let size = [bounds.width, bounds.height]; + ( + vector_cache.upload( + handle, + *color, + size, + _scale_factor, + &mut gl, + &mut self.storage, + ), + bounds, + ) + } + + #[cfg(not(feature = "svg"))] + layer::Image::Vector { bounds, .. } => (None, bounds), + }; + + unsafe { + gl.scissor( + layer_bounds.x as i32, + (target_height - (layer_bounds.y + layer_bounds.height)) + as i32, + layer_bounds.width as i32, + layer_bounds.height as i32, + ); + + if let Some(storage::Entry { texture, .. }) = entry { + gl.bind_texture(glow::TEXTURE_2D, Some(*texture)) + } else { + continue; + } + + let translate = Transformation::translate(bounds.x, bounds.y); + let scale = Transformation::scale(bounds.width, bounds.height); + let transformation = transformation * translate * scale; + let matrix: [f32; 16] = transformation.into(); + gl.uniform_matrix_4_f32_slice( + Some(&self.transform_location), + false, + &matrix, + ); + + gl.draw_arrays(glow::TRIANGLE_STRIP, 0, 4); + + gl.bind_texture(glow::TEXTURE_2D, None); + } + } + + unsafe { + gl.bind_buffer(glow::ARRAY_BUFFER, None); + gl.bind_vertex_array(None); + gl.use_program(None); + gl.disable(glow::SCISSOR_TEST); + } + } + + pub fn trim_cache(&mut self, mut gl: &glow::Context) { + #[cfg(feature = "image")] + self.raster_cache + .borrow_mut() + .trim(&mut self.storage, &mut gl); + + #[cfg(feature = "svg")] + self.vector_cache + .borrow_mut() + .trim(&mut self.storage, &mut gl); + } +} diff --git a/glow/src/image/storage.rs b/glow/src/image/storage.rs new file mode 100644 index 00000000..9bc20641 --- /dev/null +++ b/glow/src/image/storage.rs @@ -0,0 +1,78 @@ +use iced_graphics::image; +use iced_graphics::Size; + +use glow::HasContext; + +#[derive(Debug, Default)] +pub struct Storage; + +impl image::Storage for Storage { + type Entry = Entry; + type State<'a> = &'a glow::Context; + + fn upload( + &mut self, + width: u32, + height: u32, + data: &[u8], + gl: &mut &glow::Context, + ) -> Option { + unsafe { + let texture = gl.create_texture().expect("create texture"); + gl.bind_texture(glow::TEXTURE_2D, Some(texture)); + gl.tex_image_2d( + glow::TEXTURE_2D, + 0, + glow::SRGB8_ALPHA8 as i32, + width as i32, + height as i32, + 0, + glow::RGBA, + glow::UNSIGNED_BYTE, + Some(data), + ); + gl.tex_parameter_i32( + glow::TEXTURE_2D, + glow::TEXTURE_WRAP_S, + glow::CLAMP_TO_EDGE as _, + ); + gl.tex_parameter_i32( + glow::TEXTURE_2D, + glow::TEXTURE_WRAP_T, + glow::CLAMP_TO_EDGE as _, + ); + gl.tex_parameter_i32( + glow::TEXTURE_2D, + glow::TEXTURE_MIN_FILTER, + glow::LINEAR as _, + ); + gl.tex_parameter_i32( + glow::TEXTURE_2D, + glow::TEXTURE_MAG_FILTER, + glow::LINEAR as _, + ); + gl.bind_texture(glow::TEXTURE_2D, None); + + Some(Entry { + size: Size::new(width, height), + texture, + }) + } + } + + fn remove(&mut self, entry: &Entry, gl: &mut &glow::Context) { + unsafe { gl.delete_texture(entry.texture) } + } +} + +#[derive(Debug)] +pub struct Entry { + size: Size, + pub(super) texture: glow::NativeTexture, +} + +impl image::storage::Entry for Entry { + fn size(&self) -> Size { + self.size + } +} diff --git a/glow/src/lib.rs b/glow/src/lib.rs index de9c0002..710ac36d 100644 --- a/glow/src/lib.rs +++ b/glow/src/lib.rs @@ -3,7 +3,7 @@ //! ![The native path of the Iced ecosystem](https://github.com/iced-rs/iced/blob/0525d76ff94e828b7b21634fa94a747022001c83/docs/graphs/native.png?raw=true) //! //! [`glow`]: https://github.com/grovesNL/glow -//! [`iced_native`]: https://github.com/iced-rs/iced/tree/0.4/native +//! [`iced_native`]: https://github.com/iced-rs/iced/tree/0.6/native #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] @@ -24,6 +24,8 @@ pub use glow; mod backend; +#[cfg(any(feature = "image", feature = "svg"))] +mod image; mod program; mod quad; mod text; diff --git a/glow/src/quad/compatibility.rs b/glow/src/quad/compatibility.rs index eb3fb7e0..e909162c 100644 --- a/glow/src/quad/compatibility.rs +++ b/glow/src/quad/compatibility.rs @@ -70,11 +70,10 @@ impl Pipeline { unsafe { gl.use_program(Some(program)); - let matrix: [f32; 16] = Transformation::identity().into(); gl.uniform_matrix_4_f32_slice( Some(&transform_location), false, - &matrix, + Transformation::identity().as_ref(), ); gl.uniform_1_f32(Some(&scale_location), 1.0); @@ -139,11 +138,10 @@ impl Pipeline { if transformation != self.current_transform { unsafe { - let matrix: [f32; 16] = transformation.into(); gl.uniform_matrix_4_f32_slice( Some(&self.transform_location), false, - &matrix, + transformation.as_ref(), ); self.current_transform = transformation; @@ -256,7 +254,7 @@ unsafe fn create_buffers( gl.enable_vertex_attrib_array(4); gl.vertex_attrib_pointer_f32( 4, - 1, + 4, glow::FLOAT, false, stride, @@ -270,7 +268,7 @@ unsafe fn create_buffers( glow::FLOAT, false, stride, - 4 * (2 + 2 + 4 + 4 + 1), + 4 * (2 + 2 + 4 + 4 + 4), ); gl.enable_vertex_attrib_array(6); @@ -280,7 +278,7 @@ unsafe fn create_buffers( glow::FLOAT, false, stride, - 4 * (2 + 2 + 4 + 4 + 1 + 1), + 4 * (2 + 2 + 4 + 4 + 4 + 1), ); gl.bind_vertex_array(None); @@ -309,7 +307,7 @@ pub struct Vertex { pub border_color: [f32; 4], /// The border radius of the [`Vertex`]. - pub border_radius: f32, + pub border_radius: [f32; 4], /// The border width of the [`Vertex`]. pub border_width: f32, diff --git a/glow/src/quad/core.rs b/glow/src/quad/core.rs index 3e51b1ca..89036530 100644 --- a/glow/src/quad/core.rs +++ b/glow/src/quad/core.rs @@ -65,11 +65,10 @@ impl Pipeline { unsafe { gl.use_program(Some(program)); - let matrix: [f32; 16] = Transformation::identity().into(); gl.uniform_matrix_4_f32_slice( Some(&transform_location), false, - &matrix, + Transformation::identity().as_ref(), ); gl.uniform_1_f32(Some(&scale_location), 1.0); @@ -119,11 +118,10 @@ impl Pipeline { if transformation != self.current_transform { unsafe { - let matrix: [f32; 16] = transformation.into(); gl.uniform_matrix_4_f32_slice( Some(&self.transform_location), false, - &matrix, + transformation.as_ref(), ); self.current_transform = transformation; @@ -220,7 +218,7 @@ unsafe fn create_instance_buffer( gl.enable_vertex_attrib_array(4); gl.vertex_attrib_pointer_f32( 4, - 1, + 4, glow::FLOAT, false, stride, @@ -235,7 +233,7 @@ unsafe fn create_instance_buffer( glow::FLOAT, false, stride, - 4 * (2 + 2 + 4 + 4 + 1), + 4 * (2 + 2 + 4 + 4 + 4), ); gl.vertex_attrib_divisor(5, 1); diff --git a/glow/src/shader/common/gradient.frag b/glow/src/shader/common/gradient.frag new file mode 100644 index 00000000..9af0cb6e --- /dev/null +++ b/glow/src/shader/common/gradient.frag @@ -0,0 +1,59 @@ +#ifdef GL_ES +#ifdef GL_FRAGMENT_PRECISION_HIGH +precision highp float; +#else +precision mediump float; +#endif +#endif + +#ifdef HIGHER_THAN_300 +layout (location = 0) out vec4 fragColor; +#define gl_FragColor fragColor +#endif + +in vec2 raw_position; + +uniform vec4 gradient_direction; +uniform int color_stops_size; +// GLSL does not support dynamically sized arrays without SSBOs so this is capped to 16 stops +//stored as color(vec4) -> offset(vec4) sequentially; +uniform vec4 color_stops[32]; + +//TODO: rewrite without branching to make ALUs happy +void main() { + vec2 start = gradient_direction.xy; + vec2 end = gradient_direction.zw; + vec2 gradient_vec = vec2(end - start); + vec2 current_vec = vec2(raw_position.xy - start); + vec2 unit = normalize(gradient_vec); + float coord_offset = dot(unit, current_vec) / length(gradient_vec); + //if a gradient has a start/end stop that is identical, the mesh will have a transparent fill + gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0); + + float min_offset = color_stops[1].x; + float max_offset = color_stops[color_stops_size - 1].x; + + for (int i = 0; i < color_stops_size - 2; i += 2) { + float curr_offset = color_stops[i+1].x; + float next_offset = color_stops[i+3].x; + + if (coord_offset <= min_offset) { + //current coordinate is before the first defined offset, set it to the start color + gl_FragColor = color_stops[0]; + } + + if (curr_offset <= coord_offset && coord_offset <= next_offset) { + //current fragment is between the current offset processing & the next one, interpolate colors + gl_FragColor = mix(color_stops[i], color_stops[i+2], smoothstep( + curr_offset, + next_offset, + coord_offset + )); + } + + if (coord_offset >= max_offset) { + //current coordinate is before the last defined offset, set it to the last color + gl_FragColor = color_stops[color_stops_size - 2]; + } + } +} diff --git a/glow/src/shader/common/gradient.vert b/glow/src/shader/common/gradient.vert new file mode 100644 index 00000000..fe505997 --- /dev/null +++ b/glow/src/shader/common/gradient.vert @@ -0,0 +1,9 @@ +uniform mat4 u_Transform; + +in vec2 i_Position; +out vec2 raw_position; + +void main() { + gl_Position = u_Transform * vec4(i_Position, 0.0, 1.0); + raw_position = i_Position; +} diff --git a/glow/src/shader/common/image.frag b/glow/src/shader/common/image.frag new file mode 100644 index 00000000..5e05abdf --- /dev/null +++ b/glow/src/shader/common/image.frag @@ -0,0 +1,22 @@ +#ifdef GL_ES +#ifdef GL_FRAGMENT_PRECISION_HIGH +precision highp float; +#else +precision mediump float; +#endif +#endif + +uniform sampler2D tex; +in vec2 tex_pos; + +#ifdef HIGHER_THAN_300 +out vec4 fragColor; +#define gl_FragColor fragColor +#endif +#ifdef GL_ES +#define texture texture2D +#endif + +void main() { + gl_FragColor = texture(tex, tex_pos); +} diff --git a/glow/src/shader/common/image.vert b/glow/src/shader/common/image.vert new file mode 100644 index 00000000..93e541f2 --- /dev/null +++ b/glow/src/shader/common/image.vert @@ -0,0 +1,9 @@ +uniform mat4 u_Transform; + +in vec2 i_Position; +out vec2 tex_pos; + +void main() { + gl_Position = u_Transform * vec4(i_Position, 0.0, 1.0); + tex_pos = i_Position; +} diff --git a/glow/src/shader/common/triangle.frag b/glow/src/shader/common/solid.frag similarity index 99% rename from glow/src/shader/common/triangle.frag rename to glow/src/shader/common/solid.frag index e8689f2e..174ffdd3 100644 --- a/glow/src/shader/common/triangle.frag +++ b/glow/src/shader/common/solid.frag @@ -15,4 +15,4 @@ in vec4 v_Color; void main() { gl_FragColor = v_Color; -} \ No newline at end of file +} diff --git a/glow/src/shader/common/triangle.vert b/glow/src/shader/common/solid.vert similarity index 98% rename from glow/src/shader/common/triangle.vert rename to glow/src/shader/common/solid.vert index d0494a5f..59ed88e5 100644 --- a/glow/src/shader/common/triangle.vert +++ b/glow/src/shader/common/solid.vert @@ -8,4 +8,4 @@ out vec4 v_Color; void main() { gl_Position = u_Transform * vec4(i_Position, 0.0, 1.0); v_Color = i_Color; -} \ No newline at end of file +} diff --git a/glow/src/shader/compatibility/quad.frag b/glow/src/shader/compatibility/quad.frag index 8ea5693d..bb9d8122 100644 --- a/glow/src/shader/compatibility/quad.frag +++ b/glow/src/shader/compatibility/quad.frag @@ -12,7 +12,7 @@ varying vec4 v_Color; varying vec4 v_BorderColor; varying vec2 v_Pos; varying vec2 v_Scale; -varying float v_BorderRadius; +varying vec4 v_BorderRadius; varying float v_BorderWidth; float _distance(vec2 frag_coord, vec2 position, vec2 size, float radius) @@ -33,10 +33,26 @@ float _distance(vec2 frag_coord, vec2 position, vec2 size, float radius) return sqrt(distance.x * distance.x + distance.y * distance.y); } +float selectBorderRadius(vec4 radi, vec2 position, vec2 center) +{ + float rx = radi.x; + float ry = radi.y; + rx = position.x > center.x ? radi.y : radi.x; + ry = position.x > center.x ? radi.z : radi.w; + rx = position.y > center.y ? ry : rx; + return rx; +} + void main() { vec2 fragCoord = vec2(gl_FragCoord.x, u_ScreenHeight - gl_FragCoord.y); - float internal_border = max(v_BorderRadius - v_BorderWidth, 0.0); + float border_radius = selectBorderRadius( + v_BorderRadius, + fragCoord, + (v_Pos + v_Scale * 0.5).xy + ); + + float internal_border = max(border_radius - v_BorderWidth, 0.0); float internal_distance = _distance( fragCoord, @@ -57,11 +73,11 @@ void main() { fragCoord, v_Pos, v_Scale, - v_BorderRadius + border_radius ); float radius_alpha = - 1.0 - smoothstep(max(v_BorderRadius - 0.5, 0.0), v_BorderRadius + 0.5, d); + 1.0 - smoothstep(max(border_radius - 0.5, 0.0), border_radius + 0.5, d); gl_FragColor = vec4(mixed_color.xyz, mixed_color.w * radius_alpha); } diff --git a/glow/src/shader/compatibility/quad.vert b/glow/src/shader/compatibility/quad.vert index abe70c0e..89931f06 100644 --- a/glow/src/shader/compatibility/quad.vert +++ b/glow/src/shader/compatibility/quad.vert @@ -5,7 +5,7 @@ attribute vec2 i_Pos; attribute vec2 i_Scale; attribute vec4 i_Color; attribute vec4 i_BorderColor; -attribute float i_BorderRadius; +attribute vec4 i_BorderRadius; attribute float i_BorderWidth; attribute vec2 q_Pos; @@ -13,7 +13,7 @@ varying vec4 v_Color; varying vec4 v_BorderColor; varying vec2 v_Pos; varying vec2 v_Scale; -varying float v_BorderRadius; +varying vec4 v_BorderRadius; varying float v_BorderWidth; @@ -21,9 +21,11 @@ void main() { vec2 p_Pos = i_Pos * u_Scale; vec2 p_Scale = i_Scale * u_Scale; - float i_BorderRadius = min( - i_BorderRadius, - min(i_Scale.x, i_Scale.y) / 2.0 + vec4 i_BorderRadius = vec4( + min(i_BorderRadius.x, min(i_Scale.x, i_Scale.y) / 2.0), + min(i_BorderRadius.y, min(i_Scale.x, i_Scale.y) / 2.0), + min(i_BorderRadius.z, min(i_Scale.x, i_Scale.y) / 2.0), + min(i_BorderRadius.w, min(i_Scale.x, i_Scale.y) / 2.0) ); mat4 i_Transform = mat4( diff --git a/glow/src/shader/core/quad.frag b/glow/src/shader/core/quad.frag index 57e2e8e7..71147aa5 100644 --- a/glow/src/shader/core/quad.frag +++ b/glow/src/shader/core/quad.frag @@ -17,7 +17,7 @@ in vec4 v_Color; in vec4 v_BorderColor; in vec2 v_Pos; in vec2 v_Scale; -in float v_BorderRadius; +in vec4 v_BorderRadius; in float v_BorderWidth; float fDistance(vec2 frag_coord, vec2 position, vec2 size, float radius) @@ -38,14 +38,30 @@ float fDistance(vec2 frag_coord, vec2 position, vec2 size, float radius) return sqrt(distance.x * distance.x + distance.y * distance.y); } +float selectBorderRadius(vec4 radi, vec2 position, vec2 center) +{ + float rx = radi.x; + float ry = radi.y; + rx = position.x > center.x ? radi.y : radi.x; + ry = position.x > center.x ? radi.z : radi.w; + rx = position.y > center.y ? ry : rx; + return rx; +} + void main() { vec4 mixed_color; vec2 fragCoord = vec2(gl_FragCoord.x, u_ScreenHeight - gl_FragCoord.y); + float border_radius = selectBorderRadius( + v_BorderRadius, + fragCoord, + (v_Pos + v_Scale * 0.5).xy + ); + // TODO: Remove branching (?) if(v_BorderWidth > 0.0) { - float internal_border = max(v_BorderRadius - v_BorderWidth, 0.0); + float internal_border = max(border_radius - v_BorderWidth, 0.0); float internal_distance = fDistance( fragCoord, @@ -69,11 +85,11 @@ void main() { fragCoord, v_Pos, v_Scale, - v_BorderRadius + border_radius ); float radius_alpha = - 1.0 - smoothstep(max(v_BorderRadius - 0.5, 0.0), v_BorderRadius + 0.5, d); + 1.0 - smoothstep(max(border_radius - 0.5, 0.0), border_radius + 0.5, d); gl_FragColor = vec4(mixed_color.xyz, mixed_color.w * radius_alpha); } diff --git a/glow/src/shader/core/quad.vert b/glow/src/shader/core/quad.vert index b1fb2365..17c3e641 100644 --- a/glow/src/shader/core/quad.vert +++ b/glow/src/shader/core/quad.vert @@ -5,14 +5,14 @@ in vec2 i_Pos; in vec2 i_Scale; in vec4 i_Color; in vec4 i_BorderColor; -in float i_BorderRadius; +in vec4 i_BorderRadius; 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 vec4 v_BorderRadius; out float v_BorderWidth; vec2 positions[4] = vec2[]( @@ -27,9 +27,11 @@ void main() { vec2 p_Pos = i_Pos * u_Scale; vec2 p_Scale = i_Scale * u_Scale; - float i_BorderRadius = min( - i_BorderRadius, - min(i_Scale.x, i_Scale.y) / 2.0 + vec4 i_BorderRadius = vec4( + min(i_BorderRadius.x, min(i_Scale.x, i_Scale.y) / 2.0), + min(i_BorderRadius.y, min(i_Scale.x, i_Scale.y) / 2.0), + min(i_BorderRadius.z, min(i_Scale.x, i_Scale.y) / 2.0), + min(i_BorderRadius.w, min(i_Scale.x, i_Scale.y) / 2.0) ); mat4 i_Transform = mat4( diff --git a/glow/src/triangle.rs b/glow/src/triangle.rs index ae4f83ef..d0205e08 100644 --- a/glow/src/triangle.rs +++ b/glow/src/triangle.rs @@ -1,204 +1,145 @@ //! Draw meshes of triangles. -use crate::program::{self, Shader}; +use crate::program; use crate::Transformation; + +use iced_graphics::gradient::Gradient; +use iced_graphics::layer::mesh::{self, Mesh}; +use iced_graphics::triangle::{ColoredVertex2D, Vertex2D}; + 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; +const DEFAULT_VERTICES: usize = 1_000; +const DEFAULT_INDICES: usize = 1_000; #[derive(Debug)] pub(crate) struct Pipeline { - program: ::Program, - vertex_array: ::VertexArray, - vertices: Buffer, indices: Buffer, - transform_location: ::UniformLocation, - current_transform: Transformation, + solid: solid::Program, + gradient: gradient::Program, } impl Pipeline { - pub fn new( - gl: &glow::Context, - shader_version: &program::Version, - ) -> Pipeline { - let program = unsafe { - let vertex_shader = Shader::vertex( - gl, - shader_version, - include_str!("shader/common/triangle.vert"), - ); - let fragment_shader = Shader::fragment( - gl, - shader_version, - include_str!("shader/common/triangle.frag"), - ); - - program::create( - gl, - &[vertex_shader, fragment_shader], - &[(0, "i_Position"), (1, "i_Color")], - ) - }; - - 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 { + pub fn new(gl: &glow::Context, shader_version: &program::Version) -> Self { + let mut indices = unsafe { Buffer::new( gl, glow::ELEMENT_ARRAY_BUFFER, glow::DYNAMIC_DRAW, - INDEX_BUFFER_SIZE, + DEFAULT_INDICES, ) }; + let solid = solid::Program::new(gl, shader_version); + let gradient = gradient::Program::new(gl, shader_version); + unsafe { - let stride = std::mem::size_of::() as i32; + gl.bind_vertex_array(Some(solid.vertex_array)); + indices.bind(gl, 0); - 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(Some(gradient.vertex_array)); + indices.bind(gl, 0); gl.bind_vertex_array(None); } - Pipeline { - program, - vertex_array, - vertices, + Self { indices, - transform_location, - current_transform: Transformation::identity(), + solid, + gradient, } } pub fn draw( &mut self, + meshes: &[Mesh<'_>], 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) - }); + // Count the total amount of vertices & indices we need to handle + let count = mesh::attribute_count_of(meshes); - // Then we ensure the current buffers are big enough, resizing if - // necessary + // Then we ensure the current attribute buffers are big enough, resizing if necessary unsafe { - self.vertices.bind(gl, total_vertices); - self.indices.bind(gl, total_indices); + self.indices.bind(gl, count.indices); } // We upload all the vertices and indices upfront - let mut last_vertex = 0; - let mut last_index = 0; + let mut solid_vertex_offset = 0; + let mut gradient_vertex_offset = 0; + let mut index_offset = 0; + + for mesh in meshes { + let indices = mesh.indices(); - for layer::Mesh { buffers, .. } in meshes { unsafe { - gl.buffer_sub_data_u8_slice( - glow::ARRAY_BUFFER, - (last_vertex * std::mem::size_of::()) as i32, - bytemuck::cast_slice(&buffers.vertices), - ); - gl.buffer_sub_data_u8_slice( glow::ELEMENT_ARRAY_BUFFER, - (last_index * std::mem::size_of::()) as i32, - bytemuck::cast_slice(&buffers.indices), + (index_offset * std::mem::size_of::()) as i32, + bytemuck::cast_slice(indices), ); - last_vertex += buffers.vertices.len(); - last_index += buffers.indices.len(); + index_offset += indices.len(); + } + + match mesh { + Mesh::Solid { buffers, .. } => { + unsafe { + self.solid.vertices.bind(gl, count.solid_vertices); + + gl.buffer_sub_data_u8_slice( + glow::ARRAY_BUFFER, + (solid_vertex_offset + * std::mem::size_of::()) + as i32, + bytemuck::cast_slice(&buffers.vertices), + ); + } + + solid_vertex_offset += buffers.vertices.len(); + } + Mesh::Gradient { buffers, .. } => { + unsafe { + self.gradient + .vertices + .bind(gl, count.gradient_vertices); + + gl.buffer_sub_data_u8_slice( + glow::ARRAY_BUFFER, + (gradient_vertex_offset + * std::mem::size_of::()) + as i32, + bytemuck::cast_slice(&buffers.vertices), + ); + } + + gradient_vertex_offset += buffers.vertices.len(); + } } } // Then we draw each mesh using offsets - let mut last_vertex = 0; + let mut last_solid_vertex = 0; + let mut last_gradient_vertex = 0; let mut last_index = 0; - for layer::Mesh { - buffers, - origin, - clip_bounds, - } in meshes - { + for mesh in meshes { + let indices = mesh.indices(); + let origin = mesh.origin(); + let transform = transformation * Transformation::translate(origin.x, origin.y); - let clip_bounds = (*clip_bounds * scale_factor).snap(); + let clip_bounds = (mesh.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)) @@ -206,56 +147,138 @@ impl Pipeline { 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::()) as i32, - last_vertex as i32, - ); - - last_vertex += buffers.vertices.len(); - last_index += buffers.indices.len(); } + + match mesh { + Mesh::Solid { buffers, .. } => unsafe { + gl.use_program(Some(self.solid.program)); + gl.bind_vertex_array(Some(self.solid.vertex_array)); + + if transform != self.solid.uniforms.transform { + gl.uniform_matrix_4_f32_slice( + Some(&self.solid.uniforms.transform_location), + false, + transform.as_ref(), + ); + + self.solid.uniforms.transform = transform; + } + + gl.draw_elements_base_vertex( + glow::TRIANGLES, + indices.len() as i32, + glow::UNSIGNED_INT, + (last_index * std::mem::size_of::()) as i32, + last_solid_vertex as i32, + ); + + last_solid_vertex += buffers.vertices.len(); + }, + Mesh::Gradient { + buffers, gradient, .. + } => unsafe { + gl.use_program(Some(self.gradient.program)); + gl.bind_vertex_array(Some(self.gradient.vertex_array)); + + if transform != self.gradient.uniforms.transform { + gl.uniform_matrix_4_f32_slice( + Some(&self.gradient.uniforms.locations.transform), + false, + transform.as_ref(), + ); + + self.gradient.uniforms.transform = transform; + } + + if &self.gradient.uniforms.gradient != *gradient { + match gradient { + Gradient::Linear(linear) => { + gl.uniform_4_f32( + Some( + &self + .gradient + .uniforms + .locations + .gradient_direction, + ), + linear.start.x, + linear.start.y, + linear.end.x, + linear.end.y, + ); + + gl.uniform_1_i32( + Some( + &self + .gradient + .uniforms + .locations + .color_stops_size, + ), + (linear.color_stops.len() * 2) as i32, + ); + + let mut stops = [0.0; 128]; + + for (index, stop) in linear + .color_stops + .iter() + .enumerate() + .take(16) + { + let [r, g, b, a] = stop.color.into_linear(); + + stops[index * 8] = r; + stops[(index * 8) + 1] = g; + stops[(index * 8) + 2] = b; + stops[(index * 8) + 3] = a; + stops[(index * 8) + 4] = stop.offset; + stops[(index * 8) + 5] = 0.; + stops[(index * 8) + 6] = 0.; + stops[(index * 8) + 7] = 0.; + } + + gl.uniform_4_f32_slice( + Some( + &self + .gradient + .uniforms + .locations + .color_stops, + ), + &stops, + ); + } + } + + self.gradient.uniforms.gradient = (*gradient).clone(); + } + + gl.draw_elements_base_vertex( + glow::TRIANGLES, + indices.len() as i32, + glow::UNSIGNED_INT, + (last_index * std::mem::size_of::()) as i32, + last_gradient_vertex as i32, + ); + + last_gradient_vertex += buffers.vertices.len(); + }, + } + + last_index += 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 for Uniforms { - fn from(transformation: Transformation) -> Uniforms { - Self { - transform: transformation.into(), - } - } -} - #[derive(Debug)] -struct Buffer { +pub struct Buffer { raw: ::Buffer, target: u32, usage: u32, @@ -299,3 +322,268 @@ impl Buffer { } } } + +mod solid { + use crate::program; + use crate::triangle; + use glow::{Context, HasContext, NativeProgram}; + use iced_graphics::triangle::ColoredVertex2D; + use iced_graphics::Transformation; + + #[derive(Debug)] + pub struct Program { + pub program: ::Program, + pub vertex_array: ::VertexArray, + pub vertices: triangle::Buffer, + pub uniforms: Uniforms, + } + + impl Program { + pub fn new(gl: &Context, shader_version: &program::Version) -> Self { + let program = unsafe { + let vertex_shader = program::Shader::vertex( + gl, + shader_version, + include_str!("shader/common/solid.vert"), + ); + + let fragment_shader = program::Shader::fragment( + gl, + shader_version, + include_str!("shader/common/solid.frag"), + ); + + program::create( + gl, + &[vertex_shader, fragment_shader], + &[(0, "i_Position"), (1, "i_Color")], + ) + }; + + let vertex_array = unsafe { + gl.create_vertex_array().expect("Create vertex array") + }; + + let vertices = unsafe { + triangle::Buffer::new( + gl, + glow::ARRAY_BUFFER, + glow::DYNAMIC_DRAW, + super::DEFAULT_VERTICES, + ) + }; + + unsafe { + gl.bind_vertex_array(Some(vertex_array)); + + let stride = std::mem::size_of::() 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); + }; + + Self { + program, + vertex_array, + vertices, + uniforms: Uniforms::new(gl, program), + } + } + } + + #[derive(Debug)] + pub struct Uniforms { + pub transform: Transformation, + pub transform_location: ::UniformLocation, + } + + impl Uniforms { + fn new(gl: &Context, program: NativeProgram) -> Self { + let transform = Transformation::identity(); + let transform_location = + unsafe { gl.get_uniform_location(program, "u_Transform") } + .expect("Solid - Get u_Transform."); + + unsafe { + gl.use_program(Some(program)); + + gl.uniform_matrix_4_f32_slice( + Some(&transform_location), + false, + transform.as_ref(), + ); + + gl.use_program(None); + } + + Self { + transform, + transform_location, + } + } + } +} + +mod gradient { + use crate::program; + use crate::triangle; + use glow::{Context, HasContext, NativeProgram}; + use iced_graphics::gradient::{self, Gradient}; + use iced_graphics::triangle::Vertex2D; + use iced_graphics::Transformation; + + #[derive(Debug)] + pub struct Program { + pub program: ::Program, + pub vertex_array: ::VertexArray, + pub vertices: triangle::Buffer, + pub uniforms: Uniforms, + } + + impl Program { + pub fn new(gl: &Context, shader_version: &program::Version) -> Self { + let program = unsafe { + let vertex_shader = program::Shader::vertex( + gl, + shader_version, + include_str!("shader/common/gradient.vert"), + ); + + let fragment_shader = program::Shader::fragment( + gl, + shader_version, + include_str!("shader/common/gradient.frag"), + ); + + program::create( + gl, + &[vertex_shader, fragment_shader], + &[(0, "i_Position")], + ) + }; + + let vertex_array = unsafe { + gl.create_vertex_array().expect("Create vertex array") + }; + + let vertices = unsafe { + triangle::Buffer::new( + gl, + glow::ARRAY_BUFFER, + glow::DYNAMIC_DRAW, + super::DEFAULT_VERTICES, + ) + }; + + unsafe { + gl.bind_vertex_array(Some(vertex_array)); + + let stride = std::mem::size_of::() as i32; + + gl.enable_vertex_attrib_array(0); + gl.vertex_attrib_pointer_f32( + 0, + 2, + glow::FLOAT, + false, + stride, + 0, + ); + + gl.bind_vertex_array(None); + }; + + Self { + program, + vertex_array, + vertices, + uniforms: Uniforms::new(gl, program), + } + } + } + + #[derive(Debug)] + pub struct Uniforms { + pub gradient: Gradient, + pub transform: Transformation, + pub locations: Locations, + } + + #[derive(Debug)] + pub struct Locations { + pub gradient_direction: ::UniformLocation, + pub color_stops_size: ::UniformLocation, + //currently the maximum number of stops is 16 due to lack of SSBO in GL2.1 + pub color_stops: ::UniformLocation, + pub transform: ::UniformLocation, + } + + impl Uniforms { + fn new(gl: &Context, program: NativeProgram) -> Self { + let gradient_direction = unsafe { + gl.get_uniform_location(program, "gradient_direction") + } + .expect("Gradient - Get gradient_direction."); + + let color_stops_size = + unsafe { gl.get_uniform_location(program, "color_stops_size") } + .expect("Gradient - Get color_stops_size."); + + let color_stops = unsafe { + gl.get_uniform_location(program, "color_stops") + .expect("Gradient - Get color_stops.") + }; + + let transform = Transformation::identity(); + let transform_location = + unsafe { gl.get_uniform_location(program, "u_Transform") } + .expect("Solid - Get u_Transform."); + + unsafe { + gl.use_program(Some(program)); + + gl.uniform_matrix_4_f32_slice( + Some(&transform_location), + false, + transform.as_ref(), + ); + + gl.use_program(None); + } + + Self { + gradient: Gradient::Linear(gradient::Linear { + start: Default::default(), + end: Default::default(), + color_stops: vec![], + }), + transform: Transformation::identity(), + locations: Locations { + gradient_direction, + color_stops_size, + color_stops, + transform: transform_location, + }, + } + } + } +} diff --git a/glow/src/window/compositor.rs b/glow/src/window/compositor.rs index f6afaa68..20756032 100644 --- a/glow/src/window/compositor.rs +++ b/glow/src/window/compositor.rs @@ -26,8 +26,11 @@ impl iced_graphics::window::GLCompositor for Compositor { log::info!("{:#?}", settings); let version = gl.version(); - log::info!("Version: {:?}", version); - log::info!("Embedded: {}", version.is_embedded); + log::info!( + "OpenGL version: {:?} (Embedded: {})", + version, + version.is_embedded + ); let renderer = gl.get_parameter_string(glow::RENDERER); log::info!("Renderer: {}", renderer); diff --git a/glutin/Cargo.toml b/glutin/Cargo.toml index d84f9d70..022457b1 100644 --- a/glutin/Cargo.toml +++ b/glutin/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_glutin" -version = "0.3.0" +version = "0.5.0" authors = ["Héctor Ramón Jiménez "] edition = "2021" description = "A glutin runtime for Iced" @@ -23,14 +23,15 @@ git = "https://github.com/iced-rs/glutin" rev = "da8d291486b4c9bec12487a46c119c4b1d386abf" [dependencies.iced_native] -version = "0.5" +version = "0.7" path = "../native" [dependencies.iced_winit] -version = "0.4" +version = "0.6" path = "../winit" +features = ["application"] [dependencies.iced_graphics] -version = "0.3" +version = "0.5" path = "../graphics" features = ["opengl"] diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index 18fef54f..823a05f4 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_graphics" -version = "0.3.1" +version = "0.5.0" authors = ["Héctor Ramón Jiménez "] edition = "2021" description = "A bunch of backend-agnostic types that can be leveraged to build a renderer for Iced" @@ -11,28 +11,44 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"] categories = ["gui"] [features] +svg = ["resvg", "usvg", "tiny-skia"] +image = ["png", "jpeg", "jpeg_rayon", "gif", "webp", "bmp"] +png = ["image_rs/png"] +jpeg = ["image_rs/jpeg"] +jpeg_rayon = ["image_rs/jpeg_rayon"] +gif = ["image_rs/gif"] +webp = ["image_rs/webp"] +pnm = ["image_rs/pnm"] +ico = ["image_rs/ico"] +bmp = ["image_rs/bmp"] +hdr = ["image_rs/hdr"] +dds = ["image_rs/dds"] +farbfeld = ["image_rs/farbfeld"] canvas = ["lyon"] qr_code = ["qrcode", "canvas"] font-source = ["font-kit"] font-fallback = [] font-icons = [] opengl = [] +image_rs = ["kamadak-exif"] [dependencies] -glam = "0.10" +glam = "0.21.3" +log = "0.4" raw-window-handle = "0.5" thiserror = "1.0" +bitflags = "1.2" [dependencies.bytemuck] version = "1.4" features = ["derive"] [dependencies.iced_native] -version = "0.5" +version = "0.7" path = "../native" [dependencies.iced_style] -version = "0.4" +version = "0.5" path = "../style" [dependencies.lyon] @@ -48,6 +64,28 @@ default-features = false version = "0.10" optional = true +[dependencies.image_rs] +version = "0.24" +package = "image" +default-features = false +optional = true + +[dependencies.resvg] +version = "0.18" +optional = true + +[dependencies.usvg] +version = "0.18" +optional = true + +[dependencies.tiny-skia] +version = "0.6" +optional = true + +[dependencies.kamadak-exif] +version = "0.5" +optional = true + [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] all-features = true diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs index 7e0af2cc..2f8e9fc3 100644 --- a/graphics/src/backend.rs +++ b/graphics/src/backend.rs @@ -66,11 +66,11 @@ pub trait Text { /// 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); + fn dimensions(&self, handle: &image::Handle) -> Size; } /// 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); + fn viewport_dimensions(&self, handle: &svg::Handle) -> Size; } diff --git a/graphics/src/gradient.rs b/graphics/src/gradient.rs new file mode 100644 index 00000000..83f25238 --- /dev/null +++ b/graphics/src/gradient.rs @@ -0,0 +1,117 @@ +//! For creating a Gradient. +pub mod linear; + +pub use linear::Linear; + +use crate::{Color, Point, Size}; + +#[derive(Debug, Clone, PartialEq)] +/// A fill which transitions colors progressively along a direction, either linearly, radially (TBD), +/// or conically (TBD). +pub enum Gradient { + /// A linear gradient interpolates colors along a direction from its `start` to its `end` + /// point. + Linear(Linear), +} + +impl Gradient { + /// Creates a new linear [`linear::Builder`]. + pub fn linear(position: impl Into) -> linear::Builder { + linear::Builder::new(position.into()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +/// A point along the gradient vector where the specified [`color`] is unmixed. +/// +/// [`color`]: Self::color +pub struct ColorStop { + /// Offset along the gradient vector. + pub offset: f32, + + /// The color of the gradient at the specified [`offset`]. + /// + /// [`offset`]: Self::offset + pub color: Color, +} + +#[derive(Debug)] +/// The position of the gradient within its bounds. +pub enum Position { + /// The gradient will be positioned with respect to two points. + Absolute { + /// The starting point of the gradient. + start: Point, + /// The ending point of the gradient. + end: Point, + }, + /// The gradient will be positioned relative to the provided bounds. + Relative { + /// The top left position of the bounds. + top_left: Point, + /// The width & height of the bounds. + size: Size, + /// The start [Location] of the gradient. + start: Location, + /// The end [Location] of the gradient. + end: Location, + }, +} + +impl From<(Point, Point)> for Position { + fn from((start, end): (Point, Point)) -> Self { + Self::Absolute { start, end } + } +} + +#[derive(Debug)] +/// The location of a relatively-positioned gradient. +pub enum Location { + /// Top left. + TopLeft, + /// Top. + Top, + /// Top right. + TopRight, + /// Right. + Right, + /// Bottom right. + BottomRight, + /// Bottom. + Bottom, + /// Bottom left. + BottomLeft, + /// Left. + Left, +} + +impl Location { + fn to_absolute(&self, top_left: Point, size: Size) -> Point { + match self { + Location::TopLeft => top_left, + Location::Top => { + Point::new(top_left.x + size.width / 2.0, top_left.y) + } + Location::TopRight => { + Point::new(top_left.x + size.width, top_left.y) + } + Location::Right => Point::new( + top_left.x + size.width, + top_left.y + size.height / 2.0, + ), + Location::BottomRight => { + Point::new(top_left.x + size.width, top_left.y + size.height) + } + Location::Bottom => Point::new( + top_left.x + size.width / 2.0, + top_left.y + size.height, + ), + Location::BottomLeft => { + Point::new(top_left.x, top_left.y + size.height) + } + Location::Left => { + Point::new(top_left.x, top_left.y + size.height / 2.0) + } + } + } +} diff --git a/graphics/src/gradient/linear.rs b/graphics/src/gradient/linear.rs new file mode 100644 index 00000000..c886db47 --- /dev/null +++ b/graphics/src/gradient/linear.rs @@ -0,0 +1,112 @@ +//! Linear gradient builder & definition. +use crate::gradient::{ColorStop, Gradient, Position}; +use crate::{Color, Point}; + +/// A linear gradient that can be used in the style of [`Fill`] or [`Stroke`]. +/// +/// [`Fill`]: crate::widget::canvas::Fill +/// [`Stroke`]: crate::widget::canvas::Stroke +#[derive(Debug, Clone, PartialEq)] +pub struct Linear { + /// The point where the linear gradient begins. + pub start: Point, + /// The point where the linear gradient ends. + pub end: Point, + /// [`ColorStop`]s along the linear gradient path. + pub color_stops: Vec, +} + +/// A [`Linear`] builder. +#[derive(Debug)] +pub struct Builder { + start: Point, + end: Point, + stops: Vec, + error: Option, +} + +impl Builder { + /// Creates a new [`Builder`]. + pub fn new(position: Position) -> Self { + let (start, end) = match position { + Position::Absolute { start, end } => (start, end), + Position::Relative { + top_left, + size, + start, + end, + } => ( + start.to_absolute(top_left, size), + end.to_absolute(top_left, size), + ), + }; + + Self { + start, + end, + stops: vec![], + error: None, + } + } + + /// Adds a new stop, defined by an offset and a color, to the gradient. + /// + /// `offset` must be between `0.0` and `1.0` or the gradient cannot be built. + /// + /// Note: when using the [`glow`] backend, any color stop added after the 16th + /// will not be displayed. + /// + /// On the [`wgpu`] backend this limitation does not exist (technical limit is 524,288 stops). + /// + /// [`glow`]: https://docs.rs/iced_glow + /// [`wgpu`]: https://docs.rs/iced_wgpu + pub fn add_stop(mut self, offset: f32, color: Color) -> Self { + if offset.is_finite() && (0.0..=1.0).contains(&offset) { + match self.stops.binary_search_by(|stop| { + stop.offset.partial_cmp(&offset).unwrap() + }) { + Ok(_) => { + self.error = Some(BuilderError::DuplicateOffset(offset)) + } + Err(index) => { + self.stops.insert(index, ColorStop { offset, color }); + } + } + } else { + self.error = Some(BuilderError::InvalidOffset(offset)) + }; + + self + } + + /// Builds the linear [`Gradient`] of this [`Builder`]. + /// + /// Returns `BuilderError` if gradient in invalid. + pub fn build(self) -> Result { + if self.stops.is_empty() { + Err(BuilderError::MissingColorStop) + } else if let Some(error) = self.error { + Err(error) + } else { + Ok(Gradient::Linear(Linear { + start: self.start, + end: self.end, + color_stops: self.stops, + })) + } + } +} + +/// An error that happened when building a [`Linear`] gradient. +#[derive(Debug, thiserror::Error)] +pub enum BuilderError { + #[error("Gradients must contain at least one color stop.")] + /// Gradients must contain at least one color stop. + MissingColorStop, + #[error("Offset {0} must be a unique, finite number.")] + /// Offsets in a gradient must all be unique & finite. + DuplicateOffset(f32), + #[error("Offset {0} must be between 0.0..=1.0.")] + /// Offsets in a gradient must be between 0.0..=1.0. + InvalidOffset(f32), +} diff --git a/graphics/src/image.rs b/graphics/src/image.rs new file mode 100644 index 00000000..04f4ff9d --- /dev/null +++ b/graphics/src/image.rs @@ -0,0 +1,10 @@ +//! Render images. +#[cfg(feature = "image_rs")] +pub mod raster; + +#[cfg(feature = "svg")] +pub mod vector; + +pub mod storage; + +pub use storage::Storage; diff --git a/wgpu/src/image/raster.rs b/graphics/src/image/raster.rs similarity index 76% rename from wgpu/src/image/raster.rs rename to graphics/src/image/raster.rs index 2b4d4af3..da46c30f 100644 --- a/wgpu/src/image/raster.rs +++ b/graphics/src/image/raster.rs @@ -1,43 +1,53 @@ -use crate::image::atlas::{self, Atlas}; +//! Raster image loading and caching. +use crate::image::Storage; +use crate::Size; + use iced_native::image; -use std::collections::{HashMap, HashSet}; use bitflags::bitflags; +use std::collections::{HashMap, HashSet}; +/// Entry in cache corresponding to an image handle #[derive(Debug)] -pub enum Memory { - Host(::image_rs::ImageBuffer<::image_rs::Bgra, Vec>), - Device(atlas::Entry), +pub enum Memory { + /// Image data on host + Host(::image_rs::ImageBuffer<::image_rs::Rgba, Vec>), + /// Storage entry + Device(T::Entry), + /// Image not found NotFound, + /// Invalid image data Invalid, } -impl Memory { - pub fn dimensions(&self) -> (u32, u32) { +impl Memory { + /// Width and height of image + pub fn dimensions(&self) -> Size { + use crate::image::storage::Entry; + match self { - Memory::Host(image) => image.dimensions(), + Memory::Host(image) => { + let (width, height) = image.dimensions(); + + Size::new(width, height) + } Memory::Device(entry) => entry.size(), - Memory::NotFound => (1, 1), - Memory::Invalid => (1, 1), + Memory::NotFound => Size::new(1, 1), + Memory::Invalid => Size::new(1, 1), } } } +/// Caches image raster data #[derive(Debug)] -pub struct Cache { - map: HashMap, +pub struct Cache { + map: HashMap>, hits: HashSet, } -impl Cache { - pub fn new() -> Self { - Self { - map: HashMap::new(), - hits: HashSet::new(), - } - } - - pub fn load(&mut self, handle: &image::Handle) -> &mut Memory { +impl Cache { + /// Load image + pub fn load(&mut self, handle: &image::Handle) -> &mut Memory { if self.contains(handle) { return self.get(handle).unwrap(); } @@ -53,7 +63,7 @@ impl Cache { }) .unwrap_or_else(Operation::empty); - Memory::Host(operation.perform(image.to_bgra8())) + Memory::Host(operation.perform(image.to_rgba8())) } else { Memory::NotFound } @@ -65,12 +75,12 @@ impl Cache { .ok() .unwrap_or_else(Operation::empty); - Memory::Host(operation.perform(image.to_bgra8())) + Memory::Host(operation.perform(image.to_rgba8())) } else { Memory::Invalid } } - image::Data::Pixels { + image::Data::Rgba { width, height, pixels, @@ -91,19 +101,19 @@ impl Cache { self.get(handle).unwrap() } + /// Load image and upload raster data pub fn upload( &mut self, handle: &image::Handle, - device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - atlas: &mut Atlas, - ) -> Option<&atlas::Entry> { + state: &mut T::State<'_>, + storage: &mut T, + ) -> Option<&T::Entry> { let memory = self.load(handle); if let Memory::Host(image) = memory { let (width, height) = image.dimensions(); - let entry = atlas.upload(width, height, image, device, encoder)?; + let entry = storage.upload(width, height, image, state)?; *memory = Memory::Device(entry); } @@ -115,7 +125,8 @@ impl Cache { } } - pub fn trim(&mut self, atlas: &mut Atlas) { + /// Trim cache misses from cache + pub fn trim(&mut self, storage: &mut T, state: &mut T::State<'_>) { let hits = &self.hits; self.map.retain(|k, memory| { @@ -123,7 +134,7 @@ impl Cache { if !retain { if let Memory::Device(entry) = memory { - atlas.remove(entry); + storage.remove(entry, state); } } @@ -133,13 +144,13 @@ impl Cache { self.hits.clear(); } - fn get(&mut self, handle: &image::Handle) -> Option<&mut Memory> { + fn get(&mut self, handle: &image::Handle) -> Option<&mut Memory> { let _ = self.hits.insert(handle.id()); self.map.get_mut(&handle.id()) } - fn insert(&mut self, handle: &image::Handle, memory: Memory) { + fn insert(&mut self, handle: &image::Handle, memory: Memory) { let _ = self.map.insert(handle.id(), memory); } @@ -148,6 +159,15 @@ impl Cache { } } +impl Default for Cache { + fn default() -> Self { + Self { + map: HashMap::new(), + hits: HashSet::new(), + } + } +} + bitflags! { struct Operation: u8 { const FLIP_HORIZONTALLY = 0b001; diff --git a/graphics/src/image/storage.rs b/graphics/src/image/storage.rs new file mode 100644 index 00000000..2098c7b2 --- /dev/null +++ b/graphics/src/image/storage.rs @@ -0,0 +1,31 @@ +//! Store images. +use crate::Size; + +use std::fmt::Debug; + +/// Stores cached image data for use in rendering +pub trait Storage { + /// The type of an [`Entry`] in the [`Storage`]. + type Entry: Entry; + + /// State provided to upload or remove a [`Self::Entry`]. + type State<'a>; + + /// Upload the image data of a [`Self::Entry`]. + fn upload( + &mut self, + width: u32, + height: u32, + data: &[u8], + state: &mut Self::State<'_>, + ) -> Option; + + /// Romve a [`Self::Entry`] from the [`Storage`]. + fn remove(&mut self, entry: &Self::Entry, state: &mut Self::State<'_>); +} + +/// An entry in some [`Storage`], +pub trait Entry: Debug { + /// The [`Size`] of the [`Entry`]. + fn size(&self) -> Size; +} diff --git a/wgpu/src/image/vector.rs b/graphics/src/image/vector.rs similarity index 67% rename from wgpu/src/image/vector.rs rename to graphics/src/image/vector.rs index b08a0aa2..82d77aff 100644 --- a/wgpu/src/image/vector.rs +++ b/graphics/src/image/vector.rs @@ -1,46 +1,48 @@ -use crate::image::atlas::{self, Atlas}; +//! Vector image loading and caching +use crate::image::Storage; +use crate::Color; use iced_native::svg; +use iced_native::Size; use std::collections::{HashMap, HashSet}; use std::fs; +/// Entry in cache corresponding to an svg handle pub enum Svg { + /// Parsed svg Loaded(usvg::Tree), + /// Svg not found or failed to parse NotFound, } impl Svg { - pub fn viewport_dimensions(&self) -> (u32, u32) { + /// Viewport width and height + pub fn viewport_dimensions(&self) -> Size { match self { Svg::Loaded(tree) => { let size = tree.svg_node().size; - (size.width() as u32, size.height() as u32) + Size::new(size.width() as u32, size.height() as u32) } - Svg::NotFound => (1, 1), + Svg::NotFound => Size::new(1, 1), } } } +/// Caches svg vector and raster data #[derive(Debug)] -pub struct Cache { +pub struct Cache { svgs: HashMap, - rasterized: HashMap<(u64, u32, u32), atlas::Entry>, + rasterized: HashMap<(u64, u32, u32, ColorFilter), T::Entry>, svg_hits: HashSet, - rasterized_hits: HashSet<(u64, u32, u32)>, + rasterized_hits: HashSet<(u64, u32, u32, ColorFilter)>, } -impl Cache { - pub fn new() -> Self { - Self { - svgs: HashMap::new(), - rasterized: HashMap::new(), - svg_hits: HashSet::new(), - rasterized_hits: HashSet::new(), - } - } +type ColorFilter = Option<[u8; 4]>; +impl Cache { + /// Load svg pub fn load(&mut self, handle: &svg::Handle) -> &Svg { if self.svgs.contains_key(&handle.id()) { return self.svgs.get(&handle.id()).unwrap(); @@ -73,15 +75,16 @@ impl Cache { self.svgs.get(&handle.id()).unwrap() } + /// Load svg and upload raster data pub fn upload( &mut self, handle: &svg::Handle, + color: Option, [width, height]: [f32; 2], scale: f32, - device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - texture_atlas: &mut Atlas, - ) -> Option<&atlas::Entry> { + state: &mut T::State<'_>, + storage: &mut T, + ) -> Option<&T::Entry> { let id = handle.id(); let (width, height) = ( @@ -89,15 +92,18 @@ impl Cache { (scale * height).ceil() as u32, ); + let color = color.map(Color::into_rgba8); + let key = (id, width, height, color); + // TODO: Optimize! // We currently rerasterize the SVG when its size changes. This is slow // as heck. A GPU rasterizer like `pathfinder` may perform better. // It would be cool to be able to smooth resize the `svg` example. - if self.rasterized.contains_key(&(id, width, height)) { + if self.rasterized.contains_key(&key) { let _ = self.svg_hits.insert(id); - let _ = self.rasterized_hits.insert((id, width, height)); + let _ = self.rasterized_hits.insert(key); - return self.rasterized.get(&(id, width, height)); + return self.rasterized.get(&key); } match self.load(handle) { @@ -123,28 +129,32 @@ impl Cache { )?; let mut rgba = img.take(); - rgba.chunks_exact_mut(4).for_each(|rgba| rgba.swap(0, 2)); - let allocation = texture_atlas.upload( - width, - height, - bytemuck::cast_slice(rgba.as_slice()), - device, - encoder, - )?; + if let Some(color) = color { + rgba.chunks_exact_mut(4).for_each(|rgba| { + if rgba[3] > 0 { + rgba[0] = color[0]; + rgba[1] = color[1]; + rgba[2] = color[2]; + } + }); + } + + let allocation = storage.upload(width, height, &rgba, state)?; log::debug!("allocating {} {}x{}", id, width, height); let _ = self.svg_hits.insert(id); - let _ = self.rasterized_hits.insert((id, width, height)); - let _ = self.rasterized.insert((id, width, height), allocation); + let _ = self.rasterized_hits.insert(key); + let _ = self.rasterized.insert(key, allocation); - self.rasterized.get(&(id, width, height)) + self.rasterized.get(&key) } Svg::NotFound => None, } } - pub fn trim(&mut self, atlas: &mut Atlas) { + /// Load svg and upload raster data + pub fn trim(&mut self, storage: &mut T, state: &mut T::State<'_>) { let svg_hits = &self.svg_hits; let rasterized_hits = &self.rasterized_hits; @@ -153,7 +163,7 @@ impl Cache { let retain = rasterized_hits.contains(k); if !retain { - atlas.remove(entry); + storage.remove(entry, state); } retain @@ -163,6 +173,17 @@ impl Cache { } } +impl Default for Cache { + fn default() -> Self { + Self { + svgs: HashMap::new(), + rasterized: HashMap::new(), + svg_hits: HashSet::new(), + rasterized_hits: HashSet::new(), + } + } +} + impl std::fmt::Debug for Svg { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/graphics/src/layer.rs b/graphics/src/layer.rs index af545713..1d453caa 100644 --- a/graphics/src/layer.rs +++ b/graphics/src/layer.rs @@ -1,15 +1,22 @@ //! Organize rendering primitives into a flattened list of layers. +mod image; +mod quad; +mod text; + +pub mod mesh; + +pub use image::Image; +pub use mesh::Mesh; +pub use quad::Quad; +pub use text::Text; + use crate::alignment; -use crate::triangle; use crate::{ Background, Font, Point, Primitive, Rectangle, Size, Vector, Viewport, }; -use iced_native::image; -use iced_native::svg; - /// A group of primitives that should be clipped together. -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct Layer<'a> { /// The clipping bounds of the [`Layer`]. pub bounds: Rectangle, @@ -159,7 +166,7 @@ impl<'a> Layer<'a> { border_color: border_color.into_linear(), }); } - Primitive::Mesh2D { buffers, size } => { + Primitive::SolidMesh { buffers, size } => { let layer = &mut layers[current_layer]; let bounds = Rectangle::new( @@ -169,13 +176,35 @@ impl<'a> Layer<'a> { // Only draw visible content if let Some(clip_bounds) = layer.bounds.intersection(&bounds) { - layer.meshes.push(Mesh { + layer.meshes.push(Mesh::Solid { origin: Point::new(translation.x, translation.y), buffers, clip_bounds, }); } } + Primitive::GradientMesh { + buffers, + size, + gradient, + } => { + let layer = &mut layers[current_layer]; + + let bounds = Rectangle::new( + Point::new(translation.x, translation.y), + *size, + ); + + // Only draw visible content + if let Some(clip_bounds) = layer.bounds.intersection(&bounds) { + layer.meshes.push(Mesh::Gradient { + origin: Point::new(translation.x, translation.y), + buffers, + clip_bounds, + gradient, + }); + } + } Primitive::Clip { bounds, content } => { let layer = &mut layers[current_layer]; let translated_bounds = *bounds + translation; @@ -222,104 +251,19 @@ impl<'a> Layer<'a> { bounds: *bounds + translation, }); } - Primitive::Svg { handle, bounds } => { + Primitive::Svg { + handle, + color, + bounds, + } => { let layer = &mut layers[current_layer]; layer.images.push(Image::Vector { handle: handle.clone(), + color: *color, 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`]. - pub position: [f32; 2], - - /// The size of the [`Quad`]. - pub size: [f32; 2], - - /// The color of the [`Quad`], in __linear RGB__. - pub color: [f32; 4], - - /// The border color of the [`Quad`], in __linear RGB__. - pub border_color: [f32; 4], - - /// The border radius of the [`Quad`]. - pub border_radius: f32, - - /// The border width of the [`Quad`]. - pub border_width: f32, -} - -/// A mesh of triangles. -#[derive(Debug, Clone, Copy)] -pub struct Mesh<'a> { - /// The origin of the vertices of the [`Mesh`]. - pub origin: Point, - - /// The vertex and index buffers of the [`Mesh`]. - pub buffers: &'a triangle::Mesh2D, - - /// The clipping bounds of the [`Mesh`]. - pub clip_bounds: Rectangle, -} - -/// A paragraph of text. -#[derive(Debug, Clone, Copy)] -pub struct Text<'a> { - /// The content of the [`Text`]. - pub content: &'a str, - - /// The layout bounds of the [`Text`]. - pub bounds: Rectangle, - - /// The color of the [`Text`], in __linear RGB_. - pub color: [f32; 4], - - /// The size of the [`Text`]. - pub size: f32, - - /// The font of the [`Text`]. - pub font: Font, - - /// The horizontal alignment of the [`Text`]. - pub horizontal_alignment: alignment::Horizontal, - - /// The vertical alignment of the [`Text`]. - pub vertical_alignment: alignment::Vertical, -} - -/// 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/layer/image.rs b/graphics/src/layer/image.rs new file mode 100644 index 00000000..3eff2397 --- /dev/null +++ b/graphics/src/layer/image.rs @@ -0,0 +1,27 @@ +use crate::{Color, Rectangle}; + +use iced_native::{image, svg}; + +/// A raster or vector image. +#[derive(Debug, Clone)] +pub enum Image { + /// A raster image. + Raster { + /// The handle of a raster image. + handle: image::Handle, + + /// The bounds of the image. + bounds: Rectangle, + }, + /// A vector image. + Vector { + /// The handle of a vector image. + handle: svg::Handle, + + /// The [`Color`] filter + color: Option, + + /// The bounds of the image. + bounds: Rectangle, + }, +} diff --git a/graphics/src/layer/mesh.rs b/graphics/src/layer/mesh.rs new file mode 100644 index 00000000..7661c5c9 --- /dev/null +++ b/graphics/src/layer/mesh.rs @@ -0,0 +1,93 @@ +//! A collection of triangle primitives. +use crate::triangle; +use crate::{Gradient, Point, Rectangle}; + +/// A mesh of triangles. +#[derive(Debug, Clone, Copy)] +pub enum Mesh<'a> { + /// A mesh of triangles with a solid color. + Solid { + /// The origin of the vertices of the [`Mesh`]. + origin: Point, + + /// The vertex and index buffers of the [`Mesh`]. + buffers: &'a triangle::Mesh2D, + + /// The clipping bounds of the [`Mesh`]. + clip_bounds: Rectangle, + }, + /// A mesh of triangles with a gradient color. + Gradient { + /// The origin of the vertices of the [`Mesh`]. + origin: Point, + + /// The vertex and index buffers of the [`Mesh`]. + buffers: &'a triangle::Mesh2D, + + /// The clipping bounds of the [`Mesh`]. + clip_bounds: Rectangle, + + /// The gradient to apply to the [`Mesh`]. + gradient: &'a Gradient, + }, +} + +impl Mesh<'_> { + /// Returns the origin of the [`Mesh`]. + pub fn origin(&self) -> Point { + match self { + Self::Solid { origin, .. } | Self::Gradient { origin, .. } => { + *origin + } + } + } + + /// Returns the indices of the [`Mesh`]. + pub fn indices(&self) -> &[u32] { + match self { + Self::Solid { buffers, .. } => &buffers.indices, + Self::Gradient { buffers, .. } => &buffers.indices, + } + } + + /// Returns the clip bounds of the [`Mesh`]. + pub fn clip_bounds(&self) -> Rectangle { + match self { + Self::Solid { clip_bounds, .. } + | Self::Gradient { clip_bounds, .. } => *clip_bounds, + } + } +} + +/// The result of counting the attributes of a set of meshes. +#[derive(Debug, Clone, Copy, Default)] +pub struct AttributeCount { + /// The total amount of solid vertices. + pub solid_vertices: usize, + + /// The total amount of gradient vertices. + pub gradient_vertices: usize, + + /// The total amount of indices. + pub indices: usize, +} + +/// Returns the number of total vertices & total indices of all [`Mesh`]es. +pub fn attribute_count_of<'a>(meshes: &'a [Mesh<'a>]) -> AttributeCount { + meshes + .iter() + .fold(AttributeCount::default(), |mut count, mesh| { + match mesh { + Mesh::Solid { buffers, .. } => { + count.solid_vertices += buffers.vertices.len(); + count.indices += buffers.indices.len(); + } + Mesh::Gradient { buffers, .. } => { + count.gradient_vertices += buffers.vertices.len(); + count.indices += buffers.indices.len(); + } + } + + count + }) +} diff --git a/graphics/src/layer/quad.rs b/graphics/src/layer/quad.rs new file mode 100644 index 00000000..0d8bde9d --- /dev/null +++ b/graphics/src/layer/quad.rs @@ -0,0 +1,30 @@ +/// A colored rectangle with a border. +/// +/// This type can be directly uploaded to GPU memory. +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct Quad { + /// The position of the [`Quad`]. + pub position: [f32; 2], + + /// The size of the [`Quad`]. + pub size: [f32; 2], + + /// The color of the [`Quad`], in __linear RGB__. + pub color: [f32; 4], + + /// The border color of the [`Quad`], in __linear RGB__. + pub border_color: [f32; 4], + + /// The border radius of the [`Quad`]. + pub border_radius: [f32; 4], + + /// The border width of the [`Quad`]. + pub border_width: f32, +} + +#[allow(unsafe_code)] +unsafe impl bytemuck::Zeroable for Quad {} + +#[allow(unsafe_code)] +unsafe impl bytemuck::Pod for Quad {} diff --git a/graphics/src/layer/text.rs b/graphics/src/layer/text.rs new file mode 100644 index 00000000..74f7a676 --- /dev/null +++ b/graphics/src/layer/text.rs @@ -0,0 +1,26 @@ +use crate::{alignment, Font, Rectangle}; + +/// A paragraph of text. +#[derive(Debug, Clone, Copy)] +pub struct Text<'a> { + /// The content of the [`Text`]. + pub content: &'a str, + + /// The layout bounds of the [`Text`]. + pub bounds: Rectangle, + + /// The color of the [`Text`], in __linear RGB_. + pub color: [f32; 4], + + /// The size of the [`Text`]. + pub size: f32, + + /// The font of the [`Text`]. + pub font: Font, + + /// The horizontal alignment of the [`Text`]. + pub horizontal_alignment: alignment::Horizontal, + + /// The vertical alignment of the [`Text`]. + pub vertical_alignment: alignment::Vertical, +} diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index 11082472..d39dd90c 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -29,6 +29,8 @@ mod viewport; pub mod backend; pub mod font; +pub mod gradient; +pub mod image; pub mod layer; pub mod overlay; pub mod renderer; @@ -39,6 +41,7 @@ pub mod window; pub use antialiasing::Antialiasing; pub use backend::Backend; pub use error::Error; +pub use gradient::Gradient; pub use layer::Layer; pub use primitive::Primitive; pub use renderer::Renderer; diff --git a/graphics/src/primitive.rs b/graphics/src/primitive.rs index 5f7a344d..5a163a2f 100644 --- a/graphics/src/primitive.rs +++ b/graphics/src/primitive.rs @@ -3,6 +3,7 @@ use iced_native::svg; use iced_native::{Background, Color, Font, Rectangle, Size, Vector}; use crate::alignment; +use crate::gradient::Gradient; use crate::triangle; use std::sync::Arc; @@ -41,7 +42,7 @@ pub enum Primitive { /// The background of the quad background: Background, /// The border radius of the quad - border_radius: f32, + border_radius: [f32; 4], /// The border width of the quad border_width: f32, /// The border color of the quad @@ -59,6 +60,9 @@ pub enum Primitive { /// The path of the SVG file handle: svg::Handle, + /// The [`Color`] filter + color: Option, + /// The bounds of the viewport bounds: Rectangle, }, @@ -77,18 +81,33 @@ pub enum Primitive { /// The primitive to translate content: Box, }, - /// A low-level primitive to render a mesh of triangles. + /// A low-level primitive to render a mesh of triangles with a solid color. /// /// It can be used to render many kinds of geometry freely. - Mesh2D { - /// The vertex and index buffers of the mesh - buffers: triangle::Mesh2D, + SolidMesh { + /// The vertices and indices 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 low-level primitive to render a mesh of triangles with a gradient. + /// + /// It can be used to render many kinds of geometry freely. + GradientMesh { + /// The vertices and indices 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, + + /// The [`Gradient`] to apply to the mesh. + gradient: Gradient, + }, /// A cached primitive. /// /// This can be useful if you are implementing a widget where primitive diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index cdbc4f40..aabdf7fc 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -6,7 +6,7 @@ use iced_native::layout; use iced_native::renderer; use iced_native::svg; use iced_native::text::{self, Text}; -use iced_native::{Background, Element, Font, Point, Rectangle, Size}; +use iced_native::{Background, Color, Element, Font, Point, Rectangle, Size}; pub use iced_native::renderer::Style; @@ -109,7 +109,7 @@ where self.primitives.push(Primitive::Quad { bounds: quad.bounds, background: background.into(), - border_radius: quad.border_radius, + border_radius: quad.border_radius.into(), border_width: quad.border_width, border_color: quad.border_color, }); @@ -183,7 +183,7 @@ where { type Handle = image::Handle; - fn dimensions(&self, handle: &image::Handle) -> (u32, u32) { + fn dimensions(&self, handle: &image::Handle) -> Size { self.backend().dimensions(handle) } @@ -196,11 +196,20 @@ impl svg::Renderer for Renderer where B: Backend + backend::Svg, { - fn dimensions(&self, handle: &svg::Handle) -> (u32, u32) { + fn dimensions(&self, handle: &svg::Handle) -> Size { self.backend().viewport_dimensions(handle) } - fn draw(&mut self, handle: svg::Handle, bounds: Rectangle) { - self.draw_primitive(Primitive::Svg { handle, bounds }) + fn draw( + &mut self, + handle: svg::Handle, + color: Option, + bounds: Rectangle, + ) { + self.draw_primitive(Primitive::Svg { + handle, + color, + bounds, + }) } } diff --git a/graphics/src/transformation.rs b/graphics/src/transformation.rs index 2a19caed..cf0457a4 100644 --- a/graphics/src/transformation.rs +++ b/graphics/src/transformation.rs @@ -8,7 +8,7 @@ pub struct Transformation(Mat4); impl Transformation { /// Get the identity transformation. pub fn identity() -> Transformation { - Transformation(Mat4::identity()) + Transformation(Mat4::IDENTITY) } /// Creates an orthographic projection. @@ -51,3 +51,9 @@ impl From for [f32; 16] { *t.as_ref() } } + +impl From for Mat4 { + fn from(transformation: Transformation) -> Self { + transformation.0 + } +} diff --git a/graphics/src/triangle.rs b/graphics/src/triangle.rs index 05028f51..f52b2339 100644 --- a/graphics/src/triangle.rs +++ b/graphics/src/triangle.rs @@ -3,23 +3,31 @@ use bytemuck::{Pod, Zeroable}; /// A set of [`Vertex2D`] and indices representing a list of triangles. #[derive(Clone, Debug)] -pub struct Mesh2D { +pub struct Mesh2D { /// The vertices of the mesh - pub vertices: Vec, + pub vertices: Vec, /// The list of vertex indices that defines the triangles of the mesh. /// - /// Therefore, this list should always have a length that is a multiple of - /// 3. + /// Therefore, this list should always have a length that is a multiple of 3. pub indices: Vec, } -/// A two-dimensional vertex with some color in __linear__ RGBA. +/// A two-dimensional vertex. #[derive(Copy, Clone, Debug, Zeroable, Pod)] #[repr(C)] pub struct Vertex2D { - /// The vertex position + /// The vertex position in 2D space. pub position: [f32; 2], - /// The vertex color in __linear__ RGBA. +} + +/// A two-dimensional vertex with a color. +#[derive(Copy, Clone, Debug, Zeroable, Pod)] +#[repr(C)] +pub struct ColoredVertex2D { + /// The vertex position in 2D space. + pub position: [f32; 2], + + /// The color of the vertex in __linear__ RGBA. pub color: [f32; 4], } diff --git a/graphics/src/widget/canvas.rs b/graphics/src/widget/canvas.rs index b4afd998..b070d0a6 100644 --- a/graphics/src/widget/canvas.rs +++ b/graphics/src/widget/canvas.rs @@ -3,19 +3,20 @@ //! A [`Canvas`] widget can be used to draw different kinds of 2D shapes in a //! [`Frame`]. It can be used for animation, data visualization, game graphics, //! and more! - pub mod event; +pub mod fill; pub mod path; +pub mod stroke; mod cache; mod cursor; -mod fill; mod frame; mod geometry; mod program; -mod stroke; +mod style; mod text; +pub use crate::gradient::{self, Gradient}; pub use cache::Cache; pub use cursor::Cursor; pub use event::Event; @@ -25,6 +26,7 @@ pub use geometry::Geometry; pub use path::Path; pub use program::Program; pub use stroke::{LineCap, LineDash, LineJoin, Stroke}; +pub use style::Style; pub use text::Text; use crate::{Backend, Primitive, Renderer}; diff --git a/graphics/src/widget/canvas/fill.rs b/graphics/src/widget/canvas/fill.rs index 56495435..e954ebb5 100644 --- a/graphics/src/widget/canvas/fill.rs +++ b/graphics/src/widget/canvas/fill.rs @@ -1,12 +1,15 @@ -use iced_native::Color; +//! Fill [crate::widget::canvas::Geometry] with a certain style. +use crate::{Color, Gradient}; + +pub use crate::widget::canvas::Style; /// The style used to fill geometry. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] pub struct Fill { - /// The color used to fill geometry. + /// The color or gradient of the fill. /// - /// By default, it is set to `BLACK`. - pub color: Color, + /// By default, it is set to [`Style::Solid`] with [`Color::BLACK`]. + pub style: Style, /// The fill rule defines how to determine what is inside and what is /// outside of a shape. @@ -20,9 +23,9 @@ pub struct Fill { } impl Default for Fill { - fn default() -> Fill { - Fill { - color: Color::BLACK, + fn default() -> Self { + Self { + style: Style::Solid(Color::BLACK), rule: FillRule::NonZero, } } @@ -31,12 +34,21 @@ impl Default for Fill { impl From for Fill { fn from(color: Color) -> Fill { Fill { - color, + style: Style::Solid(color), ..Fill::default() } } } +impl From for Fill { + fn from(gradient: Gradient) -> Self { + Fill { + style: Style::Gradient(gradient), + ..Default::default() + } + } +} + /// The fill rule defines how to determine what is inside and what is outside of /// a shape. /// diff --git a/graphics/src/widget/canvas/frame.rs b/graphics/src/widget/canvas/frame.rs index 516539ca..d68548ae 100644 --- a/graphics/src/widget/canvas/frame.rs +++ b/graphics/src/widget/canvas/frame.rs @@ -1,13 +1,13 @@ -use std::borrow::Cow; +use crate::gradient::Gradient; +use crate::triangle; +use crate::widget::canvas::{path, Fill, Geometry, Path, Stroke, Style, Text}; +use crate::Primitive; use iced_native::{Point, Rectangle, Size, Vector}; -use crate::triangle; -use crate::widget::canvas::path; -use crate::widget::canvas::{Fill, Geometry, Path, Stroke, Text}; -use crate::Primitive; - +use lyon::geom::euclid; use lyon::tessellation; +use std::borrow::Cow; /// The frame of a [`Canvas`]. /// @@ -15,13 +15,91 @@ use lyon::tessellation; #[allow(missing_debug_implementations)] pub struct Frame { size: Size, - buffers: lyon::tessellation::VertexBuffers, + buffers: BufferStack, primitives: Vec, transforms: Transforms, fill_tessellator: tessellation::FillTessellator, stroke_tessellator: tessellation::StrokeTessellator, } +enum Buffer { + Solid(tessellation::VertexBuffers), + Gradient( + tessellation::VertexBuffers, + Gradient, + ), +} + +struct BufferStack { + stack: Vec, +} + +impl BufferStack { + fn new() -> Self { + Self { stack: Vec::new() } + } + + fn get_mut(&mut self, style: &Style) -> &mut Buffer { + match style { + Style::Solid(_) => match self.stack.last() { + Some(Buffer::Solid(_)) => {} + _ => { + self.stack.push(Buffer::Solid( + tessellation::VertexBuffers::new(), + )); + } + }, + Style::Gradient(gradient) => match self.stack.last() { + Some(Buffer::Gradient(_, last)) if gradient == last => {} + _ => { + self.stack.push(Buffer::Gradient( + tessellation::VertexBuffers::new(), + gradient.clone(), + )); + } + }, + } + + self.stack.last_mut().unwrap() + } + + fn get_fill<'a>( + &'a mut self, + style: &Style, + ) -> Box { + match (style, self.get_mut(style)) { + (Style::Solid(color), Buffer::Solid(buffer)) => { + Box::new(tessellation::BuffersBuilder::new( + buffer, + TriangleVertex2DBuilder(color.into_linear()), + )) + } + (Style::Gradient(_), Buffer::Gradient(buffer, _)) => Box::new( + tessellation::BuffersBuilder::new(buffer, Vertex2DBuilder), + ), + _ => unreachable!(), + } + } + + fn get_stroke<'a>( + &'a mut self, + style: &Style, + ) -> Box { + match (style, self.get_mut(style)) { + (Style::Solid(color), Buffer::Solid(buffer)) => { + Box::new(tessellation::BuffersBuilder::new( + buffer, + TriangleVertex2DBuilder(color.into_linear()), + )) + } + (Style::Gradient(_), Buffer::Gradient(buffer, _)) => Box::new( + tessellation::BuffersBuilder::new(buffer, Vertex2DBuilder), + ), + _ => unreachable!(), + } + } +} + #[derive(Debug)] struct Transforms { previous: Vec, @@ -34,6 +112,35 @@ struct Transform { is_identity: bool, } +impl Transform { + /// Transforms the given [Point] by the transformation matrix. + fn transform_point(&self, point: &mut Point) { + let transformed = self + .raw + .transform_point(euclid::Point2D::new(point.x, point.y)); + point.x = transformed.x; + point.y = transformed.y; + } + + fn transform_style(&self, style: Style) -> Style { + match style { + Style::Solid(color) => Style::Solid(color), + Style::Gradient(gradient) => { + Style::Gradient(self.transform_gradient(gradient)) + } + } + } + + fn transform_gradient(&self, mut gradient: Gradient) -> Gradient { + let (start, end) = match &mut gradient { + Gradient::Linear(linear) => (&mut linear.start, &mut linear.end), + }; + self.transform_point(start); + self.transform_point(end); + gradient + } +} + impl Frame { /// Creates a new empty [`Frame`] with the given dimensions. /// @@ -42,7 +149,7 @@ impl Frame { pub fn new(size: Size) -> Frame { Frame { size, - buffers: lyon::tessellation::VertexBuffers::new(), + buffers: BufferStack::new(), primitives: Vec::new(), transforms: Transforms { previous: Vec::new(), @@ -83,21 +190,20 @@ impl Frame { /// Draws the given [`Path`] on the [`Frame`] by filling it with the /// provided style. pub fn fill(&mut self, path: &Path, fill: impl Into) { - let Fill { color, rule } = fill.into(); + let Fill { style, rule } = fill.into(); - let mut buffers = tessellation::BuffersBuilder::new( - &mut self.buffers, - FillVertex(color.into_linear()), - ); + let mut buffer = self + .buffers + .get_fill(&self.transforms.current.transform_style(style)); let options = tessellation::FillOptions::default().with_fill_rule(rule.into()); - let result = if self.transforms.current.is_identity { + if self.transforms.current.is_identity { self.fill_tessellator.tessellate_path( path.raw(), &options, - &mut buffers, + buffer.as_mut(), ) } else { let path = path.transformed(&self.transforms.current.raw); @@ -105,11 +211,10 @@ impl Frame { self.fill_tessellator.tessellate_path( path.raw(), &options, - &mut buffers, + buffer.as_mut(), ) - }; - - result.expect("Tessellate path"); + } + .expect("Tessellate path."); } /// Draws an axis-aligned rectangle given its top-left corner coordinate and @@ -120,12 +225,11 @@ impl Frame { size: Size, fill: impl Into, ) { - let Fill { color, rule } = fill.into(); + let Fill { style, rule } = fill.into(); - let mut buffers = tessellation::BuffersBuilder::new( - &mut self.buffers, - FillVertex(color.into_linear()), - ); + let mut buffer = self + .buffers + .get_fill(&self.transforms.current.transform_style(style)); let top_left = self.transforms.current.raw.transform_point( @@ -144,7 +248,7 @@ impl Frame { .tessellate_rectangle( &lyon::math::Box2D::new(top_left, top_left + size), &options, - &mut buffers, + buffer.as_mut(), ) .expect("Fill rectangle"); } @@ -154,10 +258,9 @@ impl Frame { pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { let stroke = stroke.into(); - let mut buffers = tessellation::BuffersBuilder::new( - &mut self.buffers, - StrokeVertex(stroke.color.into_linear()), - ); + let mut buffer = self + .buffers + .get_stroke(&self.transforms.current.transform_style(stroke.style)); let mut options = tessellation::StrokeOptions::default(); options.line_width = stroke.width; @@ -171,11 +274,11 @@ impl Frame { Cow::Owned(path::dashed(path, stroke.line_dash)) }; - let result = if self.transforms.current.is_identity { + if self.transforms.current.is_identity { self.stroke_tessellator.tessellate_path( path.raw(), &options, - &mut buffers, + buffer.as_mut(), ) } else { let path = path.transformed(&self.transforms.current.raw); @@ -183,11 +286,10 @@ impl Frame { self.stroke_tessellator.tessellate_path( path.raw(), &options, - &mut buffers, + buffer.as_mut(), ) - }; - - result.expect("Stroke path"); + } + .expect("Stroke path"); } /// Draws the characters of the given [`Text`] on the [`Frame`], filling @@ -206,8 +308,6 @@ impl Frame { /// /// [`Canvas`]: crate::widget::Canvas pub fn fill_text(&mut self, text: impl Into) { - use std::f32; - let text = text.into(); let position = if self.transforms.current.is_identity { @@ -304,7 +404,7 @@ impl Frame { self.transforms.current.is_identity = false; } - /// Applies a rotation to the current transform of the [`Frame`]. + /// Applies a rotation in radians to the current transform of the [`Frame`]. #[inline] pub fn rotate(&mut self, angle: f32) { self.transforms.current.raw = self @@ -331,50 +431,98 @@ impl Frame { } fn into_primitives(mut self) -> Vec { - if !self.buffers.indices.is_empty() { - self.primitives.push(Primitive::Mesh2D { - buffers: triangle::Mesh2D { - vertices: self.buffers.vertices, - indices: self.buffers.indices, - }, - size: self.size, - }); + for buffer in self.buffers.stack { + match buffer { + Buffer::Solid(buffer) => { + if !buffer.indices.is_empty() { + self.primitives.push(Primitive::SolidMesh { + buffers: triangle::Mesh2D { + vertices: buffer.vertices, + indices: buffer.indices, + }, + size: self.size, + }) + } + } + Buffer::Gradient(buffer, gradient) => { + if !buffer.indices.is_empty() { + self.primitives.push(Primitive::GradientMesh { + buffers: triangle::Mesh2D { + vertices: buffer.vertices, + indices: buffer.indices, + }, + size: self.size, + gradient, + }) + } + } + } } self.primitives } } -struct FillVertex([f32; 4]); +struct Vertex2DBuilder; -impl lyon::tessellation::FillVertexConstructor - for FillVertex +impl tessellation::FillVertexConstructor + for Vertex2DBuilder { fn new_vertex( &mut self, - vertex: lyon::tessellation::FillVertex<'_>, + vertex: tessellation::FillVertex<'_>, ) -> triangle::Vertex2D { let position = vertex.position(); triangle::Vertex2D { position: [position.x, position.y], + } + } +} + +impl tessellation::StrokeVertexConstructor + for Vertex2DBuilder +{ + fn new_vertex( + &mut self, + vertex: tessellation::StrokeVertex<'_, '_>, + ) -> triangle::Vertex2D { + let position = vertex.position(); + + triangle::Vertex2D { + position: [position.x, position.y], + } + } +} + +struct TriangleVertex2DBuilder([f32; 4]); + +impl tessellation::FillVertexConstructor + for TriangleVertex2DBuilder +{ + fn new_vertex( + &mut self, + vertex: tessellation::FillVertex<'_>, + ) -> triangle::ColoredVertex2D { + let position = vertex.position(); + + triangle::ColoredVertex2D { + position: [position.x, position.y], color: self.0, } } } -struct StrokeVertex([f32; 4]); - -impl lyon::tessellation::StrokeVertexConstructor - for StrokeVertex +impl tessellation::StrokeVertexConstructor + for TriangleVertex2DBuilder { fn new_vertex( &mut self, - vertex: lyon::tessellation::StrokeVertex<'_, '_>, - ) -> triangle::Vertex2D { + vertex: tessellation::StrokeVertex<'_, '_>, + ) -> triangle::ColoredVertex2D { let position = vertex.position(); - triangle::Vertex2D { + triangle::ColoredVertex2D { position: [position.x, position.y], color: self.0, } diff --git a/graphics/src/widget/canvas/stroke.rs b/graphics/src/widget/canvas/stroke.rs index 6accc2fb..4c19251d 100644 --- a/graphics/src/widget/canvas/stroke.rs +++ b/graphics/src/widget/canvas/stroke.rs @@ -1,10 +1,15 @@ +//! Create lines from a [crate::widget::canvas::Path] and assigns them various attributes/styles. +pub use crate::widget::canvas::Style; + use iced_native::Color; /// The style of a stroke. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] pub struct Stroke<'a> { - /// The color of the stroke. - pub color: Color, + /// The color or gradient of the stroke. + /// + /// By default, it is set to a [`Style::Solid`] with [`Color::BLACK`]. + pub style: Style, /// The distance between the two edges of the stroke. pub width: f32, /// The shape to be used at the end of open subpaths when they are stroked. @@ -19,7 +24,10 @@ pub struct Stroke<'a> { impl<'a> Stroke<'a> { /// Sets the color of the [`Stroke`]. pub fn with_color(self, color: Color) -> Self { - Stroke { color, ..self } + Stroke { + style: Style::Solid(color), + ..self + } } /// Sets the width of the [`Stroke`]. @@ -41,7 +49,7 @@ impl<'a> Stroke<'a> { impl<'a> Default for Stroke<'a> { fn default() -> Self { Stroke { - color: Color::BLACK, + style: Style::Solid(Color::BLACK), width: 1.0, line_cap: LineCap::default(), line_join: LineJoin::default(), diff --git a/graphics/src/widget/canvas/style.rs b/graphics/src/widget/canvas/style.rs new file mode 100644 index 00000000..6794f2e7 --- /dev/null +++ b/graphics/src/widget/canvas/style.rs @@ -0,0 +1,23 @@ +use crate::{Color, Gradient}; + +/// The coloring style of some drawing. +#[derive(Debug, Clone, PartialEq)] +pub enum Style { + /// A solid [`Color`]. + Solid(Color), + + /// A [`Gradient`] color. + Gradient(Gradient), +} + +impl From for Style { + fn from(color: Color) -> Self { + Self::Solid(color) + } +} + +impl From for Style { + fn from(gradient: Gradient) -> Self { + Self::Gradient(gradient) + } +} diff --git a/graphics/src/window/compositor.rs b/graphics/src/window/compositor.rs index 52255666..db4ba45d 100644 --- a/graphics/src/window/compositor.rs +++ b/graphics/src/window/compositor.rs @@ -40,7 +40,7 @@ pub trait Compositor: Sized { height: u32, ); - /// Returns [`GraphicsInformation`] used by this [`Compositor`]. + /// Returns [`Information`] used by this [`Compositor`]. fn fetch_information(&self) -> Information; /// Presents the [`Renderer`] primitives to the next frame of the given [`Surface`]. diff --git a/graphics/src/window/gl_compositor.rs b/graphics/src/window/gl_compositor.rs index 722e4d9c..a45a7ca1 100644 --- a/graphics/src/window/gl_compositor.rs +++ b/graphics/src/window/gl_compositor.rs @@ -54,7 +54,7 @@ pub trait GLCompositor: Sized { /// Resizes the viewport of the [`GLCompositor`]. fn resize_viewport(&mut self, physical_size: Size); - /// Returns [`GraphicsInformation`] used by this [`Compositor`]. + /// Returns [`Information`] used by this [`GLCompositor`]. fn fetch_information(&self) -> Information; /// Presents the primitives of the [`Renderer`] to the next frame of the diff --git a/lazy/Cargo.toml b/lazy/Cargo.toml index 12e4e313..1b26e5c9 100644 --- a/lazy/Cargo.toml +++ b/lazy/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_lazy" -version = "0.1.1" +version = "0.3.0" authors = ["Héctor Ramón Jiménez "] edition = "2021" description = "Lazy widgets for Iced" @@ -14,5 +14,5 @@ categories = ["gui"] ouroboros = "0.13" [dependencies.iced_native] -version = "0.5" +version = "0.7" path = "../native" diff --git a/lazy/src/component.rs b/lazy/src/component.rs index 8987b993..3d7b8758 100644 --- a/lazy/src/component.rs +++ b/lazy/src/component.rs @@ -11,7 +11,7 @@ use iced_native::{ }; use ouroboros::self_referencing; -use std::cell::{Ref, RefCell}; +use std::cell::RefCell; use std::marker::PhantomData; /// A reusable, custom widget that uses The Elm Architecture. @@ -260,6 +260,14 @@ where ) { self.operation.focusable(state, id); } + + fn text_input( + &mut self, + state: &mut dyn widget::operation::TextInput, + id: Option<&widget::Id>, + ) { + self.operation.text_input(state, id); + } } self.with_element(|element| { @@ -314,25 +322,25 @@ where } fn overlay<'b>( - &'b self, + &'b mut self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option> { let overlay = OverlayBuilder { instance: self, - instance_ref_builder: |instance| instance.state.borrow(), tree, types: PhantomData, overlay_builder: |instance, tree| { - instance - .as_ref() - .unwrap() - .borrow_element() - .as_ref() - .unwrap() - .as_widget() - .overlay(&mut tree.children[0], layout, renderer) + instance.state.get_mut().as_mut().unwrap().with_element_mut( + move |element| { + element.as_mut().unwrap().as_widget_mut().overlay( + &mut tree.children[0], + layout, + renderer, + ) + }, + ) }, } .build(); @@ -354,15 +362,11 @@ where #[self_referencing] struct Overlay<'a, 'b, Message, Renderer, Event, S> { - instance: &'a Instance<'b, Message, Renderer, Event, S>, + instance: &'a mut Instance<'b, Message, Renderer, Event, S>, tree: &'a mut Tree, types: PhantomData<(Message, Event, S)>, - #[borrows(instance)] - #[covariant] - instance_ref: Ref<'this, Option>>, - - #[borrows(instance_ref, mut tree)] + #[borrows(mut instance, mut tree)] #[covariant] overlay: Option>, } @@ -506,7 +510,6 @@ where self.overlay = Some( OverlayBuilder { instance: overlay.instance, - instance_ref_builder: |instance| instance.state.borrow(), tree: overlay.tree, types: PhantomData, overlay_builder: |_, _| None, diff --git a/lazy/src/lazy.rs b/lazy/src/lazy.rs new file mode 100644 index 00000000..2611dd10 --- /dev/null +++ b/lazy/src/lazy.rs @@ -0,0 +1,353 @@ +use iced_native::event; +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::overlay; +use iced_native::renderer; +use iced_native::widget::tree::{self, Tree}; +use iced_native::widget::{self, Widget}; +use iced_native::Element; +use iced_native::{Clipboard, Hasher, Length, Point, Rectangle, Shell, Size}; + +use ouroboros::self_referencing; +use std::cell::{Ref, RefCell, RefMut}; +use std::hash::{Hash, Hasher as H}; +use std::marker::PhantomData; +use std::rc::Rc; + +#[allow(missing_debug_implementations)] +pub struct Lazy<'a, Message, Renderer, Dependency, View> { + dependency: Dependency, + view: Box View + 'a>, + element: RefCell>>>>, +} + +impl<'a, Message, Renderer, Dependency, View> + Lazy<'a, Message, Renderer, Dependency, View> +where + Dependency: Hash + 'a, + View: Into>, +{ + pub fn new(dependency: Dependency, view: impl Fn() -> View + 'a) -> Self { + Self { + dependency, + view: Box::new(view), + element: RefCell::new(None), + } + } + + fn with_element( + &self, + f: impl FnOnce(Ref>) -> T, + ) -> T { + f(self.element.borrow().as_ref().unwrap().borrow()) + } + + fn with_element_mut( + &self, + f: impl FnOnce(RefMut>) -> T, + ) -> T { + f(self.element.borrow().as_ref().unwrap().borrow_mut()) + } +} + +struct Internal { + element: Rc>>, + hash: u64, +} + +impl<'a, Message, Renderer, Dependency, View> Widget + for Lazy<'a, Message, Renderer, Dependency, View> +where + View: Into> + 'static, + Dependency: Hash + 'a, + Message: 'static, + Renderer: iced_native::Renderer + 'static, +{ + fn tag(&self) -> tree::Tag { + struct Tag(T); + tree::Tag::of::>() + } + + fn state(&self) -> tree::State { + let mut hasher = Hasher::default(); + self.dependency.hash(&mut hasher); + let hash = hasher.finish(); + + let element = Rc::new(RefCell::new((self.view)().into())); + + (*self.element.borrow_mut()) = Some(element.clone()); + + tree::State::new(Internal { element, hash }) + } + + fn children(&self) -> Vec { + vec![Tree::new( + self.element.borrow().as_ref().unwrap().borrow().as_widget(), + )] + } + + fn diff(&self, tree: &mut Tree) { + let current = tree.state.downcast_mut::>(); + + let mut hasher = Hasher::default(); + self.dependency.hash(&mut hasher); + let new_hash = hasher.finish(); + + if current.hash != new_hash { + current.hash = new_hash; + + let element = (self.view)().into(); + current.element = Rc::new(RefCell::new(element)); + + (*self.element.borrow_mut()) = Some(current.element.clone()); + tree.diff_children(std::slice::from_ref( + &self.element.borrow().as_ref().unwrap().borrow().as_widget(), + )); + } else { + (*self.element.borrow_mut()) = Some(current.element.clone()); + } + } + + fn width(&self) -> Length { + self.with_element(|element| element.as_widget().width()) + } + + fn height(&self) -> Length { + self.with_element(|element| element.as_widget().height()) + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.with_element(|element| { + element.as_widget().layout(renderer, limits) + }) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + operation: &mut dyn widget::Operation, + ) { + self.with_element(|element| { + element.as_widget().operate( + &mut tree.children[0], + layout, + operation, + ); + }); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: iced_native::Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + self.with_element_mut(|mut element| { + element.as_widget_mut().on_event( + &mut tree.children[0], + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + }) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.with_element(|element| { + element.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor_position, + viewport, + renderer, + ) + }) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + self.with_element(|element| { + element.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + layout, + cursor_position, + viewport, + ) + }) + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option> { + let overlay = OverlayBuilder { + cached: self, + tree: &mut tree.children[0], + types: PhantomData, + overlay_builder: |cached, tree| { + Rc::get_mut(cached.element.get_mut().as_mut().unwrap()) + .unwrap() + .get_mut() + .as_widget_mut() + .overlay(tree, layout, renderer) + }, + } + .build(); + + let has_overlay = overlay.with_overlay(|overlay| { + overlay.as_ref().map(overlay::Element::position) + }); + + has_overlay + .map(|position| overlay::Element::new(position, Box::new(overlay))) + } +} + +#[self_referencing] +struct Overlay<'a, 'b, Message, Renderer, Dependency, View> { + cached: &'a mut Lazy<'b, Message, Renderer, Dependency, View>, + tree: &'a mut Tree, + types: PhantomData<(Message, Dependency, View)>, + + #[borrows(mut cached, mut tree)] + #[covariant] + overlay: Option>, +} + +impl<'a, 'b, Message, Renderer, Dependency, View> + Overlay<'a, 'b, Message, Renderer, Dependency, View> +{ + fn with_overlay_maybe( + &self, + f: impl FnOnce(&overlay::Element<'_, Message, Renderer>) -> T, + ) -> Option { + self.borrow_overlay().as_ref().map(f) + } + + fn with_overlay_mut_maybe( + &mut self, + f: impl FnOnce(&mut overlay::Element<'_, Message, Renderer>) -> T, + ) -> Option { + self.with_overlay_mut(|overlay| overlay.as_mut().map(f)) + } +} + +impl<'a, 'b, Message, Renderer, Dependency, View> + overlay::Overlay + for Overlay<'a, 'b, Message, Renderer, Dependency, View> +where + Renderer: iced_native::Renderer, +{ + fn layout( + &self, + renderer: &Renderer, + bounds: Size, + position: Point, + ) -> layout::Node { + self.with_overlay_maybe(|overlay| { + let vector = position - overlay.position(); + + overlay.layout(renderer, bounds).translate(vector) + }) + .unwrap_or_default() + } + + fn draw( + &self, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + ) { + let _ = self.with_overlay_maybe(|overlay| { + overlay.draw(renderer, theme, style, layout, cursor_position); + }); + } + + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.with_overlay_maybe(|overlay| { + overlay.mouse_interaction( + layout, + cursor_position, + viewport, + renderer, + ) + }) + .unwrap_or_default() + } + + fn on_event( + &mut self, + event: iced_native::Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + self.with_overlay_mut_maybe(|overlay| { + overlay.on_event( + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + }) + .unwrap_or(iced_native::event::Status::Ignored) + } +} + +impl<'a, Message, Renderer, Dependency, View> + From> + for Element<'a, Message, Renderer> +where + View: Into> + 'static, + Renderer: iced_native::Renderer + 'static, + Message: 'static, + Dependency: Hash + 'a, +{ + fn from(lazy: Lazy<'a, Message, Renderer, Dependency, View>) -> Self { + Self::new(lazy) + } +} diff --git a/lazy/src/lib.rs b/lazy/src/lib.rs index 3827746c..f49fe4b6 100644 --- a/lazy/src/lib.rs +++ b/lazy/src/lib.rs @@ -17,15 +17,30 @@ clippy::type_complexity )] #![cfg_attr(docsrs, feature(doc_cfg))] +mod lazy; + pub mod component; pub mod responsive; pub use component::Component; +pub use lazy::Lazy; pub use responsive::Responsive; mod cache; use iced_native::{Element, Size}; +use std::hash::Hash; + +pub fn lazy<'a, Message, Renderer, Dependency, View>( + dependency: Dependency, + view: impl Fn() -> View + 'a, +) -> Lazy<'a, Message, Renderer, Dependency, View> +where + Dependency: Hash + 'a, + View: Into>, +{ + Lazy::new(dependency, view) +} /// Turns an implementor of [`Component`] into an [`Element`] that can be /// embedded in any application. diff --git a/lazy/src/responsive.rs b/lazy/src/responsive.rs index 0b7ae6de..5e1b5dff 100644 --- a/lazy/src/responsive.rs +++ b/lazy/src/responsive.rs @@ -235,18 +235,20 @@ where } fn overlay<'b>( - &'b self, + &'b mut self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option> { + use std::ops::DerefMut; + let state = tree.state.downcast_ref::(); let overlay = OverlayBuilder { content: self.content.borrow_mut(), tree: state.tree.borrow_mut(), types: PhantomData, - overlay_builder: |content, tree| { + overlay_builder: |content: &mut RefMut>, tree| { content.update( tree, renderer, @@ -254,16 +256,18 @@ where &self.view, ); + let Content { + element, layout, .. + } = content.deref_mut(); + let content_layout = Layout::with_offset( - layout.position() - Point::ORIGIN, - &content.layout, + layout.bounds().position() - Point::ORIGIN, + layout, ); - content.element.as_widget().overlay( - tree, - content_layout, - renderer, - ) + element + .as_widget_mut() + .overlay(tree, content_layout, renderer) }, } .build(); diff --git a/native/Cargo.toml b/native/Cargo.toml index b4945c05..bbf92951 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_native" -version = "0.5.1" +version = "0.7.0" authors = ["Héctor Ramón Jiménez "] edition = "2021" description = "A renderer-agnostic library for native GUIs" @@ -16,14 +16,14 @@ unicode-segmentation = "1.6" num-traits = "0.2" [dependencies.iced_core] -version = "0.5" +version = "0.6" path = "../core" [dependencies.iced_futures] -version = "0.4" +version = "0.5" path = "../futures" features = ["thread-pool"] [dependencies.iced_style] -version = "0.4" +version = "0.5.1" path = "../style" diff --git a/native/src/element.rs b/native/src/element.rs index 074e422e..2f1adeff 100644 --- a/native/src/element.rs +++ b/native/src/element.rs @@ -316,6 +316,22 @@ where ) { self.operation.focusable(state, id); } + + fn scrollable( + &mut self, + state: &mut dyn widget::operation::Scrollable, + id: Option<&widget::Id>, + ) { + self.operation.scrollable(state, id); + } + + fn text_input( + &mut self, + state: &mut dyn widget::operation::TextInput, + id: Option<&widget::Id>, + ) { + self.operation.text_input(state, id); + } } self.widget @@ -389,7 +405,7 @@ where } fn overlay<'b>( - &'b self, + &'b mut self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, @@ -503,7 +519,7 @@ where bounds: layout.bounds(), border_color: color, border_width: 1.0, - border_radius: 0.0, + border_radius: 0.0.into(), }, Color::TRANSPARENT, ); @@ -544,7 +560,7 @@ where } fn overlay<'b>( - &'b self, + &'b mut self, state: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, diff --git a/native/src/image.rs b/native/src/image.rs index 516eb2db..06fd7ae6 100644 --- a/native/src/image.rs +++ b/native/src/image.rs @@ -1,6 +1,7 @@ //! Load and draw raster graphics. -use crate::{Hasher, Rectangle}; +use crate::{Hasher, Rectangle, Size}; +use std::borrow::Cow; use std::hash::{Hash, Hasher as _}; use std::path::PathBuf; use std::sync::Arc; @@ -21,15 +22,19 @@ impl Handle { } /// Creates an image [`Handle`] containing the image pixels directly. This - /// function expects the input data to be provided as a `Vec` of BGRA + /// function expects the input data to be provided as a `Vec` of RGBA /// pixels. /// /// This is useful if you have already decoded your image. - pub fn from_pixels(width: u32, height: u32, pixels: Vec) -> Handle { - Self::from_data(Data::Pixels { + pub fn from_pixels( + width: u32, + height: u32, + pixels: impl Into>, + ) -> Handle { + Self::from_data(Data::Rgba { width, height, - pixels, + pixels: pixels.into(), }) } @@ -39,8 +44,8 @@ impl Handle { /// /// This is useful if you already have your image loaded in-memory, maybe /// because you downloaded or generated it procedurally. - pub fn from_memory(bytes: Vec) -> Handle { - Self::from_data(Data::Bytes(bytes)) + pub fn from_memory(bytes: impl Into>) -> Handle { + Self::from_data(Data::Bytes(bytes.into())) } fn from_data(data: Data) -> Handle { @@ -86,16 +91,16 @@ pub enum Data { Path(PathBuf), /// In-memory data - Bytes(Vec), + Bytes(Cow<'static, [u8]>), - /// Decoded image pixels in BGRA format. - Pixels { + /// Decoded image pixels in RGBA format. + Rgba { /// The width of the image. width: u32, /// The height of the image. height: u32, /// The pixels. - pixels: Vec, + pixels: Cow<'static, [u8]>, }, } @@ -104,7 +109,7 @@ impl std::fmt::Debug for Data { match self { Data::Path(path) => write!(f, "Path({:?})", path), Data::Bytes(_) => write!(f, "Bytes(...)"), - Data::Pixels { width, height, .. } => { + Data::Rgba { width, height, .. } => { write!(f, "Pixels({} * {})", width, height) } } @@ -121,7 +126,7 @@ pub trait Renderer: crate::Renderer { type Handle: Clone + Hash; /// Returns the dimensions of an image for the given [`Handle`]. - fn dimensions(&self, handle: &Self::Handle) -> (u32, u32); + fn dimensions(&self, handle: &Self::Handle) -> Size; /// Draws an image with the given [`Handle`] and inside the provided /// `bounds`. diff --git a/native/src/lib.rs b/native/src/lib.rs index 02269265..ce7c010d 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -23,8 +23,8 @@ //! - Build a new renderer, see the [renderer] module. //! - Build a custom widget, start at the [`Widget`] trait. //! -//! [`iced_core`]: https://github.com/iced-rs/iced/tree/0.4/core -//! [`iced_winit`]: https://github.com/iced-rs/iced/tree/0.4/winit +//! [`iced_core`]: https://github.com/iced-rs/iced/tree/0.6/core +//! [`iced_winit`]: https://github.com/iced-rs/iced/tree/0.6/winit //! [`druid`]: https://github.com/xi-editor/druid //! [`raw-window-handle`]: https://github.com/rust-windowing/raw-window-handle //! [renderer]: crate::renderer diff --git a/native/src/overlay.rs b/native/src/overlay.rs index 905d3389..0b05b058 100644 --- a/native/src/overlay.rs +++ b/native/src/overlay.rs @@ -11,7 +11,7 @@ use crate::layout; use crate::mouse; use crate::renderer; use crate::widget; -use crate::widget::tree::{self, Tree}; +use crate::widget::Tree; use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size}; /// An interactive component that can be displayed on top of other widgets. @@ -42,31 +42,9 @@ where cursor_position: Point, ); - /// Returns the [`Tag`] of the [`Widget`]. - /// - /// [`Tag`]: tree::Tag - fn tag(&self) -> tree::Tag { - tree::Tag::stateless() - } - - /// Returns the [`State`] of the [`Widget`]. - /// - /// [`State`]: tree::State - fn state(&self) -> tree::State { - tree::State::None - } - - /// Returns the state [`Tree`] of the children of the [`Widget`]. - fn children(&self) -> Vec { - Vec::new() - } - - /// Reconciliates the [`Widget`] with the provided [`Tree`]. - fn diff(&self, _tree: &mut Tree) {} - - /// Applies an [`Operation`] to the [`Widget`]. + /// Applies a [`widget::Operation`] to the [`Overlay`]. fn operate( - &self, + &mut self, _layout: Layout<'_>, _operation: &mut dyn widget::Operation, ) { @@ -115,7 +93,7 @@ where /// This method will generally only be used by advanced users that are /// implementing the [`Widget`](crate::Widget) trait. pub fn from_children<'a, Message, Renderer>( - children: &'a [crate::Element<'_, Message, Renderer>], + children: &'a mut [crate::Element<'_, Message, Renderer>], tree: &'a mut Tree, layout: Layout<'_>, renderer: &Renderer, @@ -124,11 +102,11 @@ where Renderer: crate::Renderer, { children - .iter() + .iter_mut() .zip(&mut tree.children) .zip(layout.children()) .filter_map(|((child, state), layout)| { - child.as_widget().overlay(state, layout, renderer) + child.as_widget_mut().overlay(state, layout, renderer) }) .next() } diff --git a/native/src/overlay/element.rs b/native/src/overlay/element.rs index b919c221..4f5ef32a 100644 --- a/native/src/overlay/element.rs +++ b/native/src/overlay/element.rs @@ -104,9 +104,9 @@ where .draw(renderer, theme, style, layout, cursor_position) } - /// Applies an [`Operation`] to the [`Element`]. + /// Applies a [`widget::Operation`] to the [`Element`]. pub fn operate( - &self, + &mut self, layout: Layout<'_>, operation: &mut dyn widget::Operation, ) { @@ -141,6 +141,57 @@ where self.content.layout(renderer, bounds, position) } + fn operate( + &mut self, + layout: Layout<'_>, + operation: &mut dyn widget::Operation, + ) { + struct MapOperation<'a, B> { + operation: &'a mut dyn widget::Operation, + } + + impl<'a, T, B> widget::Operation for MapOperation<'a, B> { + fn container( + &mut self, + id: Option<&widget::Id>, + operate_on_children: &mut dyn FnMut( + &mut dyn widget::Operation, + ), + ) { + self.operation.container(id, &mut |operation| { + operate_on_children(&mut MapOperation { operation }); + }); + } + + fn focusable( + &mut self, + state: &mut dyn widget::operation::Focusable, + id: Option<&widget::Id>, + ) { + self.operation.focusable(state, id); + } + + fn scrollable( + &mut self, + state: &mut dyn widget::operation::Scrollable, + id: Option<&widget::Id>, + ) { + self.operation.scrollable(state, id); + } + + fn text_input( + &mut self, + state: &mut dyn widget::operation::TextInput, + id: Option<&widget::Id>, + ) { + self.operation.text_input(state, id) + } + } + + self.content + .operate(layout, &mut MapOperation { operation }); + } + fn on_event( &mut self, event: Event, diff --git a/native/src/overlay/menu.rs b/native/src/overlay/menu.rs index 08135872..099b1a97 100644 --- a/native/src/overlay/menu.rs +++ b/native/src/overlay/menu.rs @@ -9,7 +9,7 @@ use crate::text::{self, Text}; use crate::touch; use crate::widget::container::{self, Container}; use crate::widget::scrollable::{self, Scrollable}; -use crate::widget::tree::{self, Tree}; +use crate::widget::Tree; use crate::{ Clipboard, Color, Element, Layout, Length, Padding, Point, Rectangle, Shell, Size, Vector, Widget, @@ -178,7 +178,7 @@ where font, text_size, padding, - style, + style: style.clone(), })); state.tree.diff(&container as &dyn Widget<_, _>); @@ -199,18 +199,6 @@ where Renderer: text::Renderer, Renderer::Theme: StyleSheet + container::StyleSheet, { - fn tag(&self) -> tree::Tag { - self.container.tag() - } - - fn state(&self) -> tree::State { - self.container.state() - } - - fn children(&self) -> Vec { - self.container.children() - } - fn layout( &self, renderer: &Renderer, @@ -288,7 +276,7 @@ where layout: Layout<'_>, cursor_position: Point, ) { - let appearance = theme.appearance(self.style); + let appearance = theme.appearance(&self.style); let bounds = layout.bounds(); renderer.fill_quad( @@ -299,7 +287,7 @@ where }, border_color: appearance.border_color, border_width: appearance.border_width, - border_radius: appearance.border_radius, + border_radius: appearance.border_radius.into(), }, appearance.background, ); @@ -460,7 +448,7 @@ where _cursor_position: Point, viewport: &Rectangle, ) { - let appearance = theme.appearance(self.style); + let appearance = theme.appearance(&self.style); let bounds = layout.bounds(); let text_size = @@ -491,7 +479,7 @@ where bounds, border_color: Color::TRANSPARENT, border_width: 0.0, - border_radius: appearance.border_radius, + border_radius: appearance.border_radius.into(), }, appearance.selected_background, ); diff --git a/native/src/renderer.rs b/native/src/renderer.rs index ef64ac36..5e776be6 100644 --- a/native/src/renderer.rs +++ b/native/src/renderer.rs @@ -50,7 +50,7 @@ pub struct Quad { pub bounds: Rectangle, /// The border radius of the [`Quad`]. - pub border_radius: f32, + pub border_radius: BorderRadius, /// The border width of the [`Quad`]. pub border_width: f32, @@ -59,6 +59,29 @@ pub struct Quad { pub border_color: Color, } +/// The border radi for the corners of a graphics primitive in the order: +/// top-left, top-right, bottom-right, bottom-left. +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub struct BorderRadius([f32; 4]); + +impl From for BorderRadius { + fn from(w: f32) -> Self { + Self([w; 4]) + } +} + +impl From<[f32; 4]> for BorderRadius { + fn from(radi: [f32; 4]) -> Self { + Self(radi) + } +} + +impl From for [f32; 4] { + fn from(radi: BorderRadius) -> Self { + radi.0 + } +} + /// The styling attributes of a [`Renderer`]. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { diff --git a/native/src/subscription.rs b/native/src/subscription.rs index 9775c84b..c60b1281 100644 --- a/native/src/subscription.rs +++ b/native/src/subscription.rs @@ -155,7 +155,7 @@ where /// Check out the [`websocket`] example, which showcases this pattern to maintain a WebSocket /// connection open. /// -/// [`websocket`]: https://github.com/iced-rs/iced/tree/0.4/examples/websocket +/// [`websocket`]: https://github.com/iced-rs/iced/tree/0.6/examples/websocket pub fn unfold( id: I, initial: T, diff --git a/native/src/svg.rs b/native/src/svg.rs index f86fec5b..2168e409 100644 --- a/native/src/svg.rs +++ b/native/src/svg.rs @@ -1,6 +1,7 @@ //! Load and draw vector graphics. -use crate::{Hasher, Rectangle}; +use crate::{Color, Hasher, Rectangle, Size}; +use std::borrow::Cow; use std::hash::{Hash, Hasher as _}; use std::path::PathBuf; use std::sync::Arc; @@ -24,7 +25,7 @@ impl Handle { /// /// This is useful if you already have your SVG data in-memory, maybe /// because you downloaded or generated it procedurally. - pub fn from_memory(bytes: impl Into>) -> Handle { + pub fn from_memory(bytes: impl Into>) -> Handle { Self::from_data(Data::Bytes(bytes.into())) } @@ -64,7 +65,7 @@ pub enum Data { /// In-memory data /// /// Can contain an SVG string or a gzip compressed data. - Bytes(Vec), + Bytes(Cow<'static, [u8]>), } impl std::fmt::Debug for Data { @@ -81,8 +82,8 @@ impl std::fmt::Debug for Data { /// [renderer]: crate::renderer pub trait Renderer: crate::Renderer { /// Returns the default dimensions of an SVG for the given [`Handle`]. - fn dimensions(&self, handle: &Handle) -> (u32, u32); + fn dimensions(&self, handle: &Handle) -> Size; - /// Draws an SVG with the given [`Handle`] and inside the provided `bounds`. - fn draw(&mut self, handle: Handle, bounds: Rectangle); + /// Draws an SVG with the given [`Handle`], an optional [`Color`] filter, and inside the provided `bounds`. + fn draw(&mut self, handle: Handle, color: Option, bounds: Rectangle); } diff --git a/native/src/user_interface.rs b/native/src/user_interface.rs index 344ba4d6..376ce568 100644 --- a/native/src/user_interface.rs +++ b/native/src/user_interface.rs @@ -18,8 +18,8 @@ use crate::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size}; /// The [`integration_opengl`] & [`integration_wgpu`] examples use a /// [`UserInterface`] to integrate Iced in an existing graphical application. /// -/// [`integration_opengl`]: https://github.com/iced-rs/iced/tree/0.4/examples/integration_opengl -/// [`integration_wgpu`]: https://github.com/iced-rs/iced/tree/0.4/examples/integration_wgpu +/// [`integration_opengl`]: https://github.com/iced-rs/iced/tree/0.6/examples/integration_opengl +/// [`integration_wgpu`]: https://github.com/iced-rs/iced/tree/0.6/examples/integration_wgpu #[allow(missing_debug_implementations)] pub struct UserInterface<'a, Message, Renderer> { root: Element<'a, Message, Renderer>, @@ -190,7 +190,7 @@ where let mut state = State::Updated; let mut manual_overlay = - ManuallyDrop::new(self.root.as_widget().overlay( + ManuallyDrop::new(self.root.as_widget_mut().overlay( &mut self.state, Layout::new(&self.base), renderer, @@ -226,7 +226,7 @@ where ); manual_overlay = - ManuallyDrop::new(self.root.as_widget().overlay( + ManuallyDrop::new(self.root.as_widget_mut().overlay( &mut self.state, Layout::new(&self.base), renderer, @@ -285,6 +285,10 @@ where &mut shell, ); + if matches!(event_status, event::Status::Captured) { + self.overlay = None; + } + shell.revalidate_layout(|| { self.base = renderer.layout( &self.root, @@ -391,11 +395,11 @@ where let viewport = Rectangle::with_size(self.bounds); - let base_cursor = if let Some(overlay) = self.root.as_widget().overlay( - &mut self.state, - Layout::new(&self.base), - renderer, - ) { + let base_cursor = if let Some(overlay) = self + .root + .as_widget_mut() + .overlay(&mut self.state, Layout::new(&self.base), renderer) + { let overlay_layout = self .overlay .take() @@ -448,7 +452,7 @@ where overlay .as_ref() .and_then(|layout| { - root.as_widget() + root.as_widget_mut() .overlay(&mut self.state, Layout::new(base), renderer) .map(|overlay| { let overlay_interaction = overlay.mouse_interaction( @@ -492,14 +496,19 @@ where operation, ); - if let Some(layout) = self.overlay.as_ref() { - if let Some(overlay) = self.root.as_widget().overlay( - &mut self.state, - Layout::new(&self.base), - renderer, - ) { - overlay.operate(Layout::new(layout), operation); + if let Some(mut overlay) = self.root.as_widget_mut().overlay( + &mut self.state, + Layout::new(&self.base), + renderer, + ) { + if self.overlay.is_none() { + self.overlay = Some(overlay.layout(renderer, self.bounds)); } + + overlay.operate( + Layout::new(self.overlay.as_ref().unwrap()), + operation, + ); } } diff --git a/native/src/widget.rs b/native/src/widget.rs index 8890b8e7..a4b46ed4 100644 --- a/native/src/widget.rs +++ b/native/src/widget.rs @@ -107,12 +107,12 @@ use crate::{Clipboard, Layout, Length, Point, Rectangle, Shell}; /// - [`geometry`], a custom widget showcasing how to draw geometry with the /// `Mesh2D` primitive in [`iced_wgpu`]. /// -/// [examples]: https://github.com/iced-rs/iced/tree/0.4/examples -/// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.4/examples/bezier_tool -/// [`custom_widget`]: https://github.com/iced-rs/iced/tree/0.4/examples/custom_widget -/// [`geometry`]: https://github.com/iced-rs/iced/tree/0.4/examples/geometry +/// [examples]: https://github.com/iced-rs/iced/tree/0.6/examples +/// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.6/examples/bezier_tool +/// [`custom_widget`]: https://github.com/iced-rs/iced/tree/0.6/examples/custom_widget +/// [`geometry`]: https://github.com/iced-rs/iced/tree/0.6/examples/geometry /// [`lyon`]: https://github.com/nical/lyon -/// [`iced_wgpu`]: https://github.com/iced-rs/iced/tree/0.4/wgpu +/// [`iced_wgpu`]: https://github.com/iced-rs/iced/tree/0.6/wgpu pub trait Widget where Renderer: crate::Renderer, @@ -208,7 +208,7 @@ where /// Returns the overlay of the [`Widget`], if there is any. fn overlay<'a>( - &'a self, + &'a mut self, _state: &'a mut Tree, _layout: Layout<'_>, _renderer: &Renderer, diff --git a/native/src/widget/action.rs b/native/src/widget/action.rs index 766e902b..9aa79dec 100644 --- a/native/src/widget/action.rs +++ b/native/src/widget/action.rs @@ -1,8 +1,10 @@ -use crate::widget::operation::{self, Operation}; +use crate::widget::operation::{self, Focusable, Operation, Scrollable}; use crate::widget::Id; use iced_futures::MaybeSend; +use std::rc::Rc; + /// An operation to be performed on the widget tree. #[allow(missing_debug_implementations)] pub struct Action(Box>); @@ -24,7 +26,7 @@ impl Action { { Action(Box::new(Map { operation: self.0, - f: Box::new(f), + f: Rc::new(f), })) } @@ -37,7 +39,7 @@ impl Action { #[allow(missing_debug_implementations)] struct Map { operation: Box>, - f: Box B>, + f: Rc B>, } impl Operation for Map @@ -50,30 +52,44 @@ where id: Option<&Id>, operate_on_children: &mut dyn FnMut(&mut dyn Operation), ) { - struct MapRef<'a, A, B> { + struct MapRef<'a, A> { operation: &'a mut dyn Operation, - f: &'a dyn Fn(A) -> B, } - impl<'a, A, B> Operation for MapRef<'a, A, B> { + impl<'a, A, B> Operation for MapRef<'a, A> { fn container( &mut self, id: Option<&Id>, operate_on_children: &mut dyn FnMut(&mut dyn Operation), ) { - let Self { operation, f } = self; + let Self { operation, .. } = self; operation.container(id, &mut |operation| { - operate_on_children(&mut MapRef { operation, f }); + operate_on_children(&mut MapRef { operation }); }); } + + fn scrollable( + &mut self, + state: &mut dyn Scrollable, + id: Option<&Id>, + ) { + self.operation.scrollable(state, id); + } + + fn focusable( + &mut self, + state: &mut dyn Focusable, + id: Option<&Id>, + ) { + self.operation.focusable(state, id); + } } - let Self { operation, f } = self; + let Self { operation, .. } = self; MapRef { operation: operation.as_mut(), - f, } .container(id, operate_on_children); } @@ -85,4 +101,35 @@ where ) { self.operation.focusable(state, id); } + + fn scrollable( + &mut self, + state: &mut dyn operation::Scrollable, + id: Option<&Id>, + ) { + self.operation.scrollable(state, id); + } + + fn text_input( + &mut self, + state: &mut dyn operation::TextInput, + id: Option<&Id>, + ) { + self.operation.text_input(state, id); + } + + fn finish(&self) -> operation::Outcome { + match self.operation.finish() { + operation::Outcome::None => operation::Outcome::None, + operation::Outcome::Some(output) => { + operation::Outcome::Some((self.f)(output)) + } + operation::Outcome::Chain(next) => { + operation::Outcome::Chain(Box::new(Map { + operation: next, + f: self.f.clone(), + })) + } + } + } } diff --git a/native/src/widget/button.rs b/native/src/widget/button.rs index 6c0b8f6e..bbd9451c 100644 --- a/native/src/widget/button.rs +++ b/native/src/widget/button.rs @@ -231,7 +231,7 @@ where cursor_position, self.on_press.is_some(), theme, - self.style, + &self.style, || tree.state.downcast_ref::(), ); @@ -260,12 +260,12 @@ where } fn overlay<'b>( - &'b self, + &'b mut self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option> { - self.content.as_widget().overlay( + self.content.as_widget_mut().overlay( &mut tree.children[0], layout.children().next().unwrap(), renderer, @@ -361,7 +361,7 @@ pub fn draw<'a, Renderer: crate::Renderer>( style_sheet: &dyn StyleSheet< Style = ::Style, >, - style: ::Style, + style: &::Style, state: impl FnOnce() -> &'a State, ) -> Appearance where @@ -393,7 +393,7 @@ where y: bounds.y + styling.shadow_offset.y, ..bounds }, - border_radius: styling.border_radius, + border_radius: styling.border_radius.into(), border_width: 0.0, border_color: Color::TRANSPARENT, }, @@ -404,7 +404,7 @@ where renderer.fill_quad( renderer::Quad { bounds, - border_radius: styling.border_radius, + border_radius: styling.border_radius.into(), border_width: styling.border_width, border_color: styling.border_color, }, @@ -426,13 +426,14 @@ pub fn layout( padding: Padding, layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, ) -> layout::Node { - let limits = limits.width(width).height(height).pad(padding); + let limits = limits.width(width).height(height); + + let mut content = layout_content(renderer, &limits.pad(padding)); + let padding = padding.fit(content.size(), limits.max()); + let size = limits.pad(padding).resolve(content.size()).pad(padding); - let mut content = layout_content(renderer, &limits); content.move_to(Point::new(padding.left.into(), padding.top.into())); - let size = limits.resolve(content.size()).pad(padding); - layout::Node::with_children(size, vec![content]) } diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs index dc3c0bd0..bec5c448 100644 --- a/native/src/widget/checkbox.rs +++ b/native/src/widget/checkbox.rs @@ -224,9 +224,9 @@ where let mut children = layout.children(); let custom_style = if is_mouse_over { - theme.hovered(self.style, self.is_checked) + theme.hovered(&self.style, self.is_checked) } else { - theme.active(self.style, self.is_checked) + theme.active(&self.style, self.is_checked) }; { @@ -236,7 +236,7 @@ where renderer.fill_quad( renderer::Quad { bounds, - border_radius: custom_style.border_radius, + border_radius: custom_style.border_radius.into(), border_width: custom_style.border_width, border_color: custom_style.border_color, }, diff --git a/native/src/widget/column.rs b/native/src/widget/column.rs index a8b0f183..8030778b 100644 --- a/native/src/widget/column.rs +++ b/native/src/widget/column.rs @@ -242,12 +242,12 @@ where } fn overlay<'b>( - &'b self, + &'b mut self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option> { - overlay::from_children(&self.children, tree, layout, renderer) + overlay::from_children(&mut self.children, tree, layout, renderer) } } diff --git a/native/src/widget/container.rs b/native/src/widget/container.rs index 2afad3f2..16d0cb61 100644 --- a/native/src/widget/container.rs +++ b/native/src/widget/container.rs @@ -228,7 +228,7 @@ where cursor_position: Point, viewport: &Rectangle, ) { - let style = theme.appearance(self.style); + let style = theme.appearance(&self.style); draw_background(renderer, &style, layout.bounds()); @@ -248,12 +248,12 @@ where } fn overlay<'b>( - &'b self, + &'b mut self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option> { - self.content.as_widget().overlay( + self.content.as_widget_mut().overlay( &mut tree.children[0], layout.children().next().unwrap(), renderer, @@ -293,11 +293,11 @@ pub fn layout( .max_width(max_width) .max_height(max_height) .width(width) - .height(height) - .pad(padding); + .height(height); - let mut content = layout_content(renderer, &limits.loose()); - let size = limits.resolve(content.size()); + let mut content = layout_content(renderer, &limits.pad(padding).loose()); + let padding = padding.fit(content.size(), limits.max()); + let size = limits.pad(padding).resolve(content.size()); content.move_to(Point::new(padding.left.into(), padding.top.into())); content.align( @@ -309,7 +309,7 @@ pub fn layout( layout::Node::with_children(size.pad(padding), vec![content]) } -/// Draws the background of a [`Container`] given its [`Style`] and its `bounds`. +/// Draws the background of a [`Container`] given its [`Appearance`] and its `bounds`. pub fn draw_background( renderer: &mut Renderer, appearance: &Appearance, @@ -321,7 +321,7 @@ pub fn draw_background( renderer.fill_quad( renderer::Quad { bounds, - border_radius: appearance.border_radius, + border_radius: appearance.border_radius.into(), border_width: appearance.border_width, border_color: appearance.border_color, }, diff --git a/native/src/widget/helpers.rs b/native/src/widget/helpers.rs index 79751878..0bde288f 100644 --- a/native/src/widget/helpers.rs +++ b/native/src/widget/helpers.rs @@ -1,4 +1,5 @@ //! Helper functions to create pure widgets. +use crate::overlay; use crate::widget; use crate::{Element, Length}; @@ -18,7 +19,7 @@ macro_rules! column { ); } -/// Creates a [Row`] with the given children. +/// Creates a [`Row`] with the given children. /// /// [`Row`]: widget::Row #[macro_export] @@ -84,6 +85,7 @@ pub fn button<'a, Message, Renderer>( where Renderer: crate::Renderer, Renderer::Theme: widget::button::StyleSheet, + ::Style: Default, { widget::Button::new(content) } @@ -208,7 +210,12 @@ where T: ToString + Eq + 'static, [T]: ToOwned>, Renderer: crate::text::Renderer, - Renderer::Theme: widget::pick_list::StyleSheet, + Renderer::Theme: widget::pick_list::StyleSheet + + widget::scrollable::StyleSheet + + overlay::menu::StyleSheet + + widget::container::StyleSheet, + ::Style: + From<::Style>, { widget::PickList::new(options, selected, on_selected) } @@ -278,6 +285,12 @@ where /// /// [`Svg`]: widget::Svg /// [`Handle`]: widget::svg::Handle -pub fn svg(handle: impl Into) -> widget::Svg { +pub fn svg( + handle: impl Into, +) -> widget::Svg +where + Renderer: crate::svg::Renderer, + Renderer::Theme: widget::svg::StyleSheet, +{ widget::Svg::new(handle) } diff --git a/native/src/widget/image.rs b/native/src/widget/image.rs index 91d68e34..8bd8ca1e 100644 --- a/native/src/widget/image.rs +++ b/native/src/widget/image.rs @@ -85,7 +85,7 @@ where { // The raw w/h of the underlying image let image_size = { - let (width, height) = renderer.dimensions(handle); + let Size { width, height } = renderer.dimensions(handle); Size::new(width as f32, height as f32) }; @@ -149,7 +149,7 @@ where _cursor_position: Point, _viewport: &Rectangle, ) { - let (width, height) = renderer.dimensions(&self.handle); + let Size { width, height } = renderer.dimensions(&self.handle); let image_size = Size::new(width as f32, height as f32); let bounds = layout.bounds(); diff --git a/native/src/widget/image/viewer.rs b/native/src/widget/image/viewer.rs index b1fe596c..9c83287e 100644 --- a/native/src/widget/image/viewer.rs +++ b/native/src/widget/image/viewer.rs @@ -108,7 +108,7 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let (width, height) = renderer.dimensions(&self.handle); + let Size { width, height } = renderer.dimensions(&self.handle); let mut size = limits .width(self.width) @@ -409,7 +409,7 @@ pub fn image_size( where Renderer: image::Renderer, { - let (width, height) = renderer.dimensions(handle); + let Size { width, height } = renderer.dimensions(handle); let (width, height) = { let dimensions = (width as f32, height as f32); diff --git a/native/src/widget/operation.rs b/native/src/widget/operation.rs index ef636aa2..a0aa4117 100644 --- a/native/src/widget/operation.rs +++ b/native/src/widget/operation.rs @@ -1,9 +1,11 @@ //! Query or update internal widget state. pub mod focusable; pub mod scrollable; +pub mod text_input; pub use focusable::Focusable; pub use scrollable::Scrollable; +pub use text_input::TextInput; use crate::widget::Id; @@ -28,6 +30,9 @@ pub trait Operation { /// Operates on a widget that can be scrolled. fn scrollable(&mut self, _state: &mut dyn Scrollable, _id: Option<&Id>) {} + /// Operates on a widget that has text input. + fn text_input(&mut self, _state: &mut dyn TextInput, _id: Option<&Id>) {} + /// Finishes the [`Operation`] and returns its [`Outcome`]. fn finish(&self) -> Outcome { Outcome::None @@ -58,3 +63,46 @@ where } } } + +/// Produces an [`Operation`] that applies the given [`Operation`] to the +/// children of a container with the given [`Id`]. +pub fn scoped( + target: Id, + operation: impl Operation + 'static, +) -> impl Operation { + struct ScopedOperation { + target: Id, + operation: Box>, + } + + impl Operation for ScopedOperation { + fn container( + &mut self, + id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + if id == Some(&self.target) { + operate_on_children(self.operation.as_mut()); + } else { + operate_on_children(self); + } + } + + fn finish(&self) -> Outcome { + match self.operation.finish() { + Outcome::Chain(next) => { + Outcome::Chain(Box::new(ScopedOperation { + target: self.target.clone(), + operation: next, + })) + } + outcome => outcome, + } + } + } + + ScopedOperation { + target, + operation: Box::new(operation), + } +} diff --git a/native/src/widget/operation/focusable.rs b/native/src/widget/operation/focusable.rs index f17bf178..0067006b 100644 --- a/native/src/widget/operation/focusable.rs +++ b/native/src/widget/operation/focusable.rs @@ -167,3 +167,37 @@ pub fn focus_next() -> impl Operation { count(|count| FocusNext { count, current: 0 }) } + +/// Produces an [`Operation`] that searches for the current focused widget +/// and stores its ID. This ignores widgets that do not have an ID. +pub fn find_focused() -> impl Operation { + struct FindFocused { + focused: Option, + } + + impl Operation for FindFocused { + fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) { + if state.is_focused() && id.is_some() { + self.focused = id.cloned(); + } + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self) + } + + fn finish(&self) -> Outcome { + if let Some(id) = &self.focused { + Outcome::Some(id.clone()) + } else { + Outcome::None + } + } + } + + FindFocused { focused: None } +} diff --git a/native/src/widget/operation/text_input.rs b/native/src/widget/operation/text_input.rs new file mode 100644 index 00000000..4c773e99 --- /dev/null +++ b/native/src/widget/operation/text_input.rs @@ -0,0 +1,131 @@ +//! Operate on widgets that have text input. +use crate::widget::operation::Operation; +use crate::widget::Id; + +/// The internal state of a widget that has text input. +pub trait TextInput { + /// Moves the cursor of the text input to the front of the input text. + fn move_cursor_to_front(&mut self); + /// Moves the cursor of the text input to the end of the input text. + fn move_cursor_to_end(&mut self); + /// Moves the cursor of the text input to an arbitrary location. + fn move_cursor_to(&mut self, position: usize); + /// Selects all the content of the text input. + fn select_all(&mut self); +} + +/// Produces an [`Operation`] that moves the cursor of the widget with the given [`Id`] to the +/// front. +pub fn move_cursor_to_front(target: Id) -> impl Operation { + struct MoveCursor { + target: Id, + } + + impl Operation for MoveCursor { + fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { + match id { + Some(id) if id == &self.target => { + state.move_cursor_to_front(); + } + _ => {} + } + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self) + } + } + + MoveCursor { target } +} + +/// Produces an [`Operation`] that moves the cursor of the widget with the given [`Id`] to the +/// end. +pub fn move_cursor_to_end(target: Id) -> impl Operation { + struct MoveCursor { + target: Id, + } + + impl Operation for MoveCursor { + fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { + match id { + Some(id) if id == &self.target => { + state.move_cursor_to_end(); + } + _ => {} + } + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self) + } + } + + MoveCursor { target } +} + +/// Produces an [`Operation`] that moves the cursor of the widget with the given [`Id`] to the +/// provided position. +pub fn move_cursor_to(target: Id, position: usize) -> impl Operation { + struct MoveCursor { + target: Id, + position: usize, + } + + impl Operation for MoveCursor { + fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { + match id { + Some(id) if id == &self.target => { + state.move_cursor_to(self.position); + } + _ => {} + } + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self) + } + } + + MoveCursor { target, position } +} + +/// Produces an [`Operation`] that selects all the content of the widget with the given [`Id`]. +pub fn select_all(target: Id) -> impl Operation { + struct MoveCursor { + target: Id, + } + + impl Operation for MoveCursor { + fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { + match id { + Some(id) if id == &self.target => { + state.select_all(); + } + _ => {} + } + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self) + } + } + + MoveCursor { target } +} diff --git a/native/src/widget/pane_grid.rs b/native/src/widget/pane_grid.rs index 96cf78ef..5de95c65 100644 --- a/native/src/widget/pane_grid.rs +++ b/native/src/widget/pane_grid.rs @@ -6,7 +6,7 @@ //! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing, //! drag and drop, and hotkey support. //! -//! [`pane_grid` example]: https://github.com/iced-rs/iced/tree/0.4/examples/pane_grid +//! [`pane_grid` example]: https://github.com/iced-rs/iced/tree/0.6/examples/pane_grid mod axis; mod configuration; mod content; @@ -38,6 +38,7 @@ use crate::mouse; use crate::overlay; use crate::renderer; use crate::touch; +use crate::widget; use crate::widget::container; use crate::widget::tree::{self, Tree}; use crate::{ @@ -85,7 +86,7 @@ use crate::{ /// let (mut state, _) = pane_grid::State::new(PaneState::SomePane); /// /// let pane_grid = -/// PaneGrid::new(&state, |pane, state| { +/// PaneGrid::new(&state, |pane, state, is_maximized| { /// pane_grid::Content::new(match state { /// PaneState::SomePane => text("This is some pane"), /// PaneState::AnotherKindOfPane => text("This is another kind of pane"), @@ -100,8 +101,7 @@ where Renderer: crate::Renderer, Renderer::Theme: StyleSheet + container::StyleSheet, { - state: &'a state::Internal, - elements: Vec<(Pane, Content<'a, Message, Renderer>)>, + contents: Contents<'a, Content<'a, Message, Renderer>>, width: Length, height: Length, spacing: u16, @@ -119,22 +119,35 @@ where /// Creates a [`PaneGrid`] with the given [`State`] and view function. /// /// The view function will be called to display each [`Pane`] present in the - /// [`State`]. + /// [`State`]. [`bool`] is set if the pane is maximized. pub fn new( state: &'a State, - view: impl Fn(Pane, &'a T) -> Content<'a, Message, Renderer>, + view: impl Fn(Pane, &'a T, bool) -> Content<'a, Message, Renderer>, ) -> Self { - let elements = { - state - .panes - .iter() - .map(|(pane, pane_state)| (*pane, view(*pane, pane_state))) - .collect() + let contents = if let Some((pane, pane_state)) = + state.maximized.and_then(|pane| { + state.panes.get(&pane).map(|pane_state| (pane, pane_state)) + }) { + Contents::Maximized( + pane, + view(pane, pane_state, true), + Node::Pane(pane), + ) + } else { + Contents::All( + state + .panes + .iter() + .map(|(pane, pane_state)| { + (*pane, view(*pane, pane_state, false)) + }) + .collect(), + &state.internal, + ) }; Self { - elements, - state: &state.internal, + contents, width: Length::Fill, height: Length::Fill, spacing: 0, @@ -208,6 +221,12 @@ where self.style = style.into(); self } + + fn drag_enabled(&self) -> bool { + (!self.contents.is_maximized()) + .then(|| self.on_drag.is_some()) + .unwrap_or_default() + } } impl<'a, Message, Renderer> Widget @@ -225,18 +244,25 @@ where } fn children(&self) -> Vec { - self.elements + self.contents .iter() .map(|(_, content)| content.state()) .collect() } fn diff(&self, tree: &mut Tree) { - tree.diff_children_custom( - &self.elements, - |state, (_, content)| content.diff(state), - |(_, content)| content.state(), - ) + match &self.contents { + Contents::All(contents, _) => tree.diff_children_custom( + contents, + |state, (_, content)| content.diff(state), + |(_, content)| content.state(), + ), + Contents::Maximized(_, content, _) => tree.diff_children_custom( + &[content], + |state, content| content.diff(state), + |content| content.state(), + ), + } } fn width(&self) -> Length { @@ -255,15 +281,32 @@ where layout( renderer, limits, - self.state, + self.contents.layout(), self.width, self.height, self.spacing, - self.elements.iter().map(|(pane, content)| (*pane, content)), - |element, renderer, limits| element.layout(renderer, limits), + self.contents.iter(), + |content, renderer, limits| content.layout(renderer, limits), ) } + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + operation: &mut dyn widget::Operation, + ) { + operation.container(None, &mut |operation| { + self.contents + .iter() + .zip(&mut tree.children) + .zip(layout.children()) + .for_each(|(((_pane, content), state), layout)| { + content.operate(state, layout, operation); + }) + }); + } + fn on_event( &mut self, tree: &mut Tree, @@ -276,28 +319,34 @@ where ) -> event::Status { let action = tree.state.downcast_mut::(); + let on_drag = if self.drag_enabled() { + &self.on_drag + } else { + &None + }; + let event_status = update( action, - self.state, + self.contents.layout(), &event, layout, cursor_position, shell, self.spacing, - self.elements.iter().map(|(pane, content)| (*pane, content)), + self.contents.iter(), &self.on_click, - &self.on_drag, + on_drag, &self.on_resize, ); let picked_pane = action.picked_pane().map(|(pane, _)| pane); - self.elements + self.contents .iter_mut() .zip(&mut tree.children) .zip(layout.children()) .map(|(((pane, content), tree), layout)| { - let is_picked = picked_pane == Some(*pane); + let is_picked = picked_pane == Some(pane); content.on_event( tree, @@ -323,14 +372,14 @@ where ) -> mouse::Interaction { mouse_interaction( tree.state.downcast_ref(), - self.state, + self.contents.layout(), layout, cursor_position, self.spacing, self.on_resize.as_ref().map(|(leeway, _)| *leeway), ) .unwrap_or_else(|| { - self.elements + self.contents .iter() .zip(&tree.children) .zip(layout.children()) @@ -341,7 +390,7 @@ where cursor_position, viewport, renderer, - self.on_drag.is_some(), + self.drag_enabled(), ) }) .max() @@ -361,7 +410,7 @@ where ) { draw( tree.state.downcast_ref(), - self.state, + self.contents.layout(), layout, cursor_position, renderer, @@ -370,11 +419,11 @@ where viewport, self.spacing, self.on_resize.as_ref().map(|(leeway, _)| *leeway), - self.style, - self.elements + &self.style, + self.contents .iter() .zip(&tree.children) - .map(|((pane, content), tree)| (*pane, (content, tree))), + .map(|((pane, content), tree)| (pane, (content, tree))), |(content, tree), renderer, style, @@ -395,13 +444,13 @@ where } fn overlay<'b>( - &'b self, + &'b mut self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option> { - self.elements - .iter() + self.contents + .iter_mut() .zip(&mut tree.children) .zip(layout.children()) .filter_map(|(((_, pane), tree), layout)| { @@ -429,24 +478,24 @@ where pub fn layout( renderer: &Renderer, limits: &layout::Limits, - state: &state::Internal, + node: &Node, width: Length, height: Length, spacing: u16, - elements: impl Iterator, - layout_element: impl Fn(T, &Renderer, &layout::Limits) -> layout::Node, + contents: impl Iterator, + layout_content: impl Fn(T, &Renderer, &layout::Limits) -> layout::Node, ) -> layout::Node { let limits = limits.width(width).height(height); let size = limits.resolve(Size::ZERO); - let regions = state.pane_regions(f32::from(spacing), size); - let children = elements - .filter_map(|(pane, element)| { + let regions = node.pane_regions(f32::from(spacing), size); + let children = contents + .filter_map(|(pane, content)| { let region = regions.get(&pane)?; let size = Size::new(region.width, region.height); - let mut node = layout_element( - element, + let mut node = layout_content( + content, renderer, &layout::Limits::new(size, size), ); @@ -464,13 +513,13 @@ pub fn layout( /// accordingly. pub fn update<'a, Message, T: Draggable>( action: &mut state::Action, - state: &state::Internal, + node: &Node, event: &Event, layout: Layout<'_>, cursor_position: Point, shell: &mut Shell<'_, Message>, spacing: u16, - elements: impl Iterator, + contents: impl Iterator, on_click: &Option Message + 'a>>, on_drag: &Option Message + 'a>>, on_resize: &Option<(u16, Box Message + 'a>)>, @@ -492,7 +541,7 @@ pub fn update<'a, Message, T: Draggable>( cursor_position.y - bounds.y, ); - let splits = state.split_regions( + let splits = node.split_regions( f32::from(spacing), Size::new(bounds.width, bounds.height), ); @@ -514,7 +563,7 @@ pub fn update<'a, Message, T: Draggable>( layout, cursor_position, shell, - elements, + contents, on_click, on_drag, ); @@ -526,7 +575,7 @@ pub fn update<'a, Message, T: Draggable>( layout, cursor_position, shell, - elements, + contents, on_click, on_drag, ); @@ -539,7 +588,7 @@ pub fn update<'a, Message, T: Draggable>( | Event::Touch(touch::Event::FingerLost { .. }) => { if let Some((pane, _)) = action.picked_pane() { if let Some(on_drag) = on_drag { - let mut dropped_region = elements + let mut dropped_region = contents .zip(layout.children()) .filter(|(_, layout)| { layout.bounds().contains(cursor_position) @@ -570,7 +619,7 @@ pub fn update<'a, Message, T: Draggable>( if let Some((split, _)) = action.picked_split() { let bounds = layout.bounds(); - let splits = state.split_regions( + let splits = node.split_regions( f32::from(spacing), Size::new(bounds.width, bounds.height), ); @@ -609,13 +658,13 @@ fn click_pane<'a, Message, T>( layout: Layout<'_>, cursor_position: Point, shell: &mut Shell<'_, Message>, - elements: impl Iterator, + contents: impl Iterator, on_click: &Option Message + 'a>>, on_drag: &Option Message + 'a>>, ) where T: Draggable, { - let mut clicked_region = elements + let mut clicked_region = contents .zip(layout.children()) .filter(|(_, layout)| layout.bounds().contains(cursor_position)); @@ -642,7 +691,7 @@ fn click_pane<'a, Message, T>( /// Returns the current [`mouse::Interaction`] of a [`PaneGrid`]. pub fn mouse_interaction( action: &state::Action, - state: &state::Internal, + node: &Node, layout: Layout<'_>, cursor_position: Point, spacing: u16, @@ -658,7 +707,7 @@ pub fn mouse_interaction( let bounds = layout.bounds(); let splits = - state.split_regions(f32::from(spacing), bounds.size()); + node.split_regions(f32::from(spacing), bounds.size()); let relative_cursor = Point::new( cursor_position.x - bounds.x, @@ -687,7 +736,7 @@ pub fn mouse_interaction( /// Draws a [`PaneGrid`]. pub fn draw( action: &state::Action, - state: &state::Internal, + node: &Node, layout: Layout<'_>, cursor_position: Point, renderer: &mut Renderer, @@ -696,8 +745,8 @@ pub fn draw( viewport: &Rectangle, spacing: u16, resize_leeway: Option, - style: ::Style, - elements: impl Iterator, + style: &::Style, + contents: impl Iterator, draw_pane: impl Fn( T, &mut Renderer, @@ -717,7 +766,7 @@ pub fn draw( .and_then(|(split, axis)| { let bounds = layout.bounds(); - let splits = state.split_regions(f32::from(spacing), bounds.size()); + let splits = node.split_regions(f32::from(spacing), bounds.size()); let (_axis, region, ratio) = splits.get(&split)?; @@ -736,7 +785,7 @@ pub fn draw( ); let splits = - state.split_regions(f32::from(spacing), bounds.size()); + node.split_regions(f32::from(spacing), bounds.size()); let (_split, axis, region) = hovered_split( splits.iter(), @@ -759,7 +808,7 @@ pub fn draw( let mut render_picked_pane = None; - for ((id, pane), layout) in elements.zip(layout.children()) { + for ((id, pane), layout) in contents.zip(layout.children()) { match picked_pane { Some((dragging, origin)) if id == dragging => { render_picked_pane = Some((pane, origin, layout)); @@ -828,7 +877,7 @@ pub fn draw( height: split_region.height, }, }, - border_radius: 0.0, + border_radius: 0.0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, }, @@ -897,3 +946,49 @@ fn hovered_split<'a>( }) .next() } + +/// The visible contents of the [`PaneGrid`] +#[derive(Debug)] +pub enum Contents<'a, T> { + /// All panes are visible + All(Vec<(Pane, T)>, &'a state::Internal), + /// A maximized pane is visible + Maximized(Pane, T, Node), +} + +impl<'a, T> Contents<'a, T> { + /// Returns the layout [`Node`] of the [`Contents`] + pub fn layout(&self) -> &Node { + match self { + Contents::All(_, state) => state.layout(), + Contents::Maximized(_, _, layout) => layout, + } + } + + /// Returns an iterator over the values of the [`Contents`] + pub fn iter(&self) -> Box + '_> { + match self { + Contents::All(contents, _) => Box::new( + contents.iter().map(|(pane, content)| (*pane, content)), + ), + Contents::Maximized(pane, content, _) => { + Box::new(std::iter::once((*pane, content))) + } + } + } + + fn iter_mut(&mut self) -> Box + '_> { + match self { + Contents::All(contents, _) => Box::new( + contents.iter_mut().map(|(pane, content)| (*pane, content)), + ), + Contents::Maximized(pane, content, _) => { + Box::new(std::iter::once((*pane, content))) + } + } + } + + fn is_maximized(&self) -> bool { + matches!(self, Self::Maximized(..)) + } +} diff --git a/native/src/widget/pane_grid/content.rs b/native/src/widget/pane_grid/content.rs index c236d820..5f269d1f 100644 --- a/native/src/widget/pane_grid/content.rs +++ b/native/src/widget/pane_grid/content.rs @@ -5,7 +5,7 @@ use crate::overlay; use crate::renderer; use crate::widget::container; use crate::widget::pane_grid::{Draggable, TitleBar}; -use crate::widget::Tree; +use crate::widget::{self, Tree}; use crate::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size}; /// The content of a [`Pane`]. @@ -87,7 +87,7 @@ where /// Draws the [`Content`] with the provided [`Renderer`] and [`Layout`]. /// - /// [`Renderer`]: iced_native::Renderer + /// [`Renderer`]: crate::Renderer pub fn draw( &self, tree: &Tree, @@ -103,7 +103,7 @@ where let bounds = layout.bounds(); { - let style = theme.appearance(self.style); + let style = theme.appearance(&self.style); container::draw_background(renderer, &style, bounds); } @@ -183,6 +183,33 @@ where } } + pub(crate) fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + operation: &mut dyn widget::Operation, + ) { + let body_layout = if let Some(title_bar) = &self.title_bar { + let mut children = layout.children(); + + title_bar.operate( + &mut tree.children[1], + children.next().unwrap(), + operation, + ); + + children.next().unwrap() + } else { + layout + }; + + self.body.as_widget().operate( + &mut tree.children[0], + body_layout, + operation, + ); + } + pub(crate) fn on_event( &mut self, tree: &mut Tree, @@ -278,12 +305,12 @@ where } pub(crate) fn overlay<'b>( - &'b self, + &'b mut self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option> { - if let Some(title_bar) = self.title_bar.as_ref() { + if let Some(title_bar) = self.title_bar.as_mut() { let mut children = layout.children(); let title_bar_layout = children.next()?; @@ -294,14 +321,14 @@ where match title_bar.overlay(title_bar_state, title_bar_layout, renderer) { Some(overlay) => Some(overlay), - None => self.body.as_widget().overlay( + None => self.body.as_widget_mut().overlay( body_state, children.next()?, renderer, ), } } else { - self.body.as_widget().overlay( + self.body.as_widget_mut().overlay( &mut tree.children[0], layout, renderer, diff --git a/native/src/widget/pane_grid/state.rs b/native/src/widget/pane_grid/state.rs index cdca6267..c4ae0a0e 100644 --- a/native/src/widget/pane_grid/state.rs +++ b/native/src/widget/pane_grid/state.rs @@ -4,9 +4,9 @@ use crate::widget::pane_grid::{ Axis, Configuration, Direction, Node, Pane, Split, }; -use crate::{Point, Rectangle, Size}; +use crate::{Point, Size}; -use std::collections::{BTreeMap, HashMap}; +use std::collections::HashMap; /// The state of a [`PaneGrid`]. /// @@ -31,6 +31,11 @@ pub struct State { /// /// [`PaneGrid`]: crate::widget::PaneGrid pub internal: Internal, + + /// The maximized [`Pane`] of the [`PaneGrid`]. + /// + /// [`PaneGrid`]: crate::widget::PaneGrid + pub(super) maximized: Option, } impl State { @@ -52,7 +57,11 @@ impl State { let internal = Internal::from_configuration(&mut panes, config.into(), 0); - State { panes, internal } + State { + panes, + internal, + maximized: None, + } } /// Returns the total amount of panes in the [`State`]. @@ -153,6 +162,7 @@ impl State { node.split(new_split, axis, new_pane); let _ = self.panes.insert(new_pane, state); + let _ = self.maximized.take(); Some((new_pane, new_split)) } @@ -194,12 +204,39 @@ impl State { /// Closes the given [`Pane`] and returns its internal state and its closest /// sibling, if it exists. pub fn close(&mut self, pane: &Pane) -> Option<(T, Pane)> { + if self.maximized == Some(*pane) { + let _ = self.maximized.take(); + } + if let Some(sibling) = self.internal.layout.remove(pane) { self.panes.remove(pane).map(|state| (state, sibling)) } else { None } } + + /// Maximize the given [`Pane`]. Only this pane will be rendered by the + /// [`PaneGrid`] until [`Self::restore()`] is called. + /// + /// [`PaneGrid`]: crate::widget::PaneGrid + pub fn maximize(&mut self, pane: &Pane) { + self.maximized = Some(*pane); + } + + /// Restore the currently maximized [`Pane`] to it's normal size. All panes + /// will be rendered by the [`PaneGrid`]. + /// + /// [`PaneGrid`]: crate::widget::PaneGrid + pub fn restore(&mut self) { + let _ = self.maximized.take(); + } + + /// Returns the maximized [`Pane`] of the [`PaneGrid`]. + /// + /// [`PaneGrid`]: crate::widget::PaneGrid + pub fn maximized(&self) -> Option { + self.maximized + } } /// The internal state of a [`PaneGrid`]. @@ -226,11 +263,13 @@ impl Internal { let Internal { layout: a, last_id: next_id, + .. } = Self::from_configuration(panes, *a, next_id); let Internal { layout: b, last_id: next_id, + .. } = Self::from_configuration(panes, *b, next_id); ( @@ -304,25 +343,8 @@ impl Action { } impl Internal { - /// Calculates the current [`Pane`] regions from the [`PaneGrid`] layout. - /// - /// [`PaneGrid`]: crate::widget::PaneGrid - pub fn pane_regions( - &self, - spacing: f32, - size: Size, - ) -> BTreeMap { - self.layout.pane_regions(spacing, size) - } - - /// Calculates the current [`Split`] regions from the [`PaneGrid`] layout. - /// - /// [`PaneGrid`]: crate::widget::PaneGrid - pub fn split_regions( - &self, - spacing: f32, - size: Size, - ) -> BTreeMap { - self.layout.split_regions(spacing, size) + /// The layout [`Node`] of the [`Internal`] state + pub fn layout(&self) -> &Node { + &self.layout } } diff --git a/native/src/widget/pane_grid/title_bar.rs b/native/src/widget/pane_grid/title_bar.rs index eb85f924..28e4670f 100644 --- a/native/src/widget/pane_grid/title_bar.rs +++ b/native/src/widget/pane_grid/title_bar.rs @@ -4,7 +4,7 @@ use crate::mouse; use crate::overlay; use crate::renderer; use crate::widget::container; -use crate::widget::Tree; +use crate::widget::{self, Tree}; use crate::{ Clipboard, Element, Layout, Padding, Point, Rectangle, Shell, Size, }; @@ -114,7 +114,7 @@ where /// Draws the [`TitleBar`] with the provided [`Renderer`] and [`Layout`]. /// - /// [`Renderer`]: iced_native::Renderer + /// [`Renderer`]: crate::Renderer pub fn draw( &self, tree: &Tree, @@ -129,7 +129,7 @@ where use container::StyleSheet; let bounds = layout.bounds(); - let style = theme.appearance(self.style); + let style = theme.appearance(&self.style); let inherited_style = renderer::Style { text_color: style.text_color.unwrap_or(inherited_style.text_color), }; @@ -257,6 +257,44 @@ where layout::Node::with_children(node.size().pad(self.padding), vec![node]) } + pub(crate) fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + operation: &mut dyn widget::Operation, + ) { + let mut children = layout.children(); + let padded = children.next().unwrap(); + + let mut children = padded.children(); + let title_layout = children.next().unwrap(); + let mut show_title = true; + + if let Some(controls) = &self.controls { + let controls_layout = children.next().unwrap(); + + if title_layout.bounds().width + controls_layout.bounds().width + > padded.bounds().width + { + show_title = false; + } + + controls.as_widget().operate( + &mut tree.children[1], + controls_layout, + operation, + ) + }; + + if show_title { + self.content.as_widget().operate( + &mut tree.children[0], + title_layout, + operation, + ) + } + } + pub(crate) fn on_event( &mut self, tree: &mut Tree, @@ -357,7 +395,7 @@ where } pub(crate) fn overlay<'b>( - &'b self, + &'b mut self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, @@ -377,13 +415,13 @@ where let controls_state = states.next().unwrap(); content - .as_widget() + .as_widget_mut() .overlay(title_state, title_layout, renderer) .or_else(move || { - controls.as_ref().and_then(|controls| { + controls.as_mut().and_then(|controls| { let controls_layout = children.next()?; - controls.as_widget().overlay( + controls.as_widget_mut().overlay( controls_state, controls_layout, renderer, diff --git a/native/src/widget/pick_list.rs b/native/src/widget/pick_list.rs index 896f5b35..52cb1ad1 100644 --- a/native/src/widget/pick_list.rs +++ b/native/src/widget/pick_list.rs @@ -9,6 +9,8 @@ use crate::overlay::menu::{self, Menu}; use crate::renderer; use crate::text::{self, Text}; use crate::touch; +use crate::widget::container; +use crate::widget::scrollable; use crate::widget::tree::{self, Tree}; use crate::{ Clipboard, Element, Layout, Length, Padding, Point, Rectangle, Shell, Size, @@ -42,7 +44,12 @@ where T: ToString + Eq, [T]: ToOwned>, Renderer: text::Renderer, - Renderer::Theme: StyleSheet, + Renderer::Theme: StyleSheet + + scrollable::StyleSheet + + menu::StyleSheet + + container::StyleSheet, + ::Style: + From<::Style>, { /// The default padding of a [`PickList`]. pub const DEFAULT_PADDING: Padding = Padding::new(5); @@ -114,7 +121,12 @@ where [T]: ToOwned>, Message: 'a, Renderer: text::Renderer + 'a, - Renderer::Theme: StyleSheet, + Renderer::Theme: StyleSheet + + scrollable::StyleSheet + + menu::StyleSheet + + container::StyleSheet, + ::Style: + From<::Style>, { fn tag(&self) -> tree::Tag { tree::Tag::of::>() @@ -202,12 +214,12 @@ where &self.font, self.placeholder.as_deref(), self.selected.as_ref(), - self.style, + &self.style, ) } fn overlay<'b>( - &'b self, + &'b mut self, tree: &'b mut Tree, layout: Layout<'_>, _renderer: &Renderer, @@ -221,7 +233,7 @@ where self.text_size, self.font.clone(), &self.options, - self.style, + self.style.clone(), ) } } @@ -233,7 +245,12 @@ where [T]: ToOwned>, Message: 'a, Renderer: text::Renderer + 'a, - Renderer::Theme: StyleSheet, + Renderer::Theme: StyleSheet + + scrollable::StyleSheet + + menu::StyleSheet + + container::StyleSheet, + ::Style: + From<::Style>, { fn from(pick_list: PickList<'a, T, Message, Renderer>) -> Self { Self::new(pick_list) @@ -456,7 +473,12 @@ where T: Clone + ToString, Message: 'a, Renderer: text::Renderer + 'a, - Renderer::Theme: StyleSheet, + Renderer::Theme: StyleSheet + + scrollable::StyleSheet + + menu::StyleSheet + + container::StyleSheet, + ::Style: + From<::Style>, { if state.is_open { let bounds = layout.bounds(); @@ -493,7 +515,7 @@ pub fn draw( font: &Renderer::Font, placeholder: Option<&str>, selected: Option<&T>, - style: ::Style, + style: &::Style, ) where Renderer: text::Renderer, Renderer::Theme: StyleSheet, @@ -514,7 +536,7 @@ pub fn draw( bounds, border_color: style.border_color, border_width: style.border_width, - border_radius: style.border_radius, + border_radius: style.border_radius.into(), }, style.background, ); diff --git a/native/src/widget/progress_bar.rs b/native/src/widget/progress_bar.rs index 8a945433..f059026f 100644 --- a/native/src/widget/progress_bar.rs +++ b/native/src/widget/progress_bar.rs @@ -124,12 +124,12 @@ where / (range_end - range_start) }; - let style = theme.appearance(self.style); + let style = theme.appearance(&self.style); renderer.fill_quad( renderer::Quad { bounds: Rectangle { ..bounds }, - border_radius: style.border_radius, + border_radius: style.border_radius.into(), border_width: 0.0, border_color: Color::TRANSPARENT, }, @@ -143,7 +143,7 @@ where width: active_progress_width, ..bounds }, - border_radius: style.border_radius, + border_radius: style.border_radius.into(), border_width: 0.0, border_color: Color::TRANSPARENT, }, diff --git a/native/src/widget/radio.rs b/native/src/widget/radio.rs index cb83f745..b95ccc5b 100644 --- a/native/src/widget/radio.rs +++ b/native/src/widget/radio.rs @@ -230,9 +230,9 @@ where let mut children = layout.children(); let custom_style = if is_mouse_over { - theme.hovered(self.style, self.is_selected) + theme.hovered(&self.style, self.is_selected) } else { - theme.active(self.style, self.is_selected) + theme.active(&self.style, self.is_selected) }; { @@ -245,7 +245,7 @@ where renderer.fill_quad( renderer::Quad { bounds, - border_radius: size / 2.0, + border_radius: (size / 2.0).into(), border_width: custom_style.border_width, border_color: custom_style.border_color, }, @@ -261,7 +261,7 @@ where width: bounds.width - dot_size, height: bounds.height - dot_size, }, - border_radius: dot_size / 2.0, + border_radius: (dot_size / 2.0).into(), border_width: 0.0, border_color: Color::TRANSPARENT, }, diff --git a/native/src/widget/row.rs b/native/src/widget/row.rs index eda7c2d3..c689ac13 100644 --- a/native/src/widget/row.rs +++ b/native/src/widget/row.rs @@ -229,12 +229,12 @@ where } fn overlay<'b>( - &'b self, + &'b mut self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option> { - overlay::from_children(&self.children, tree, layout, renderer) + overlay::from_children(&mut self.children, tree, layout, renderer) } } diff --git a/native/src/widget/rule.rs b/native/src/widget/rule.rs index 56f8c80d..2dc7b6f0 100644 --- a/native/src/widget/rule.rs +++ b/native/src/widget/rule.rs @@ -88,7 +88,7 @@ where _viewport: &Rectangle, ) { let bounds = layout.bounds(); - let style = theme.style(self.style); + let style = theme.appearance(&self.style); let bounds = if self.is_horizontal { let line_y = (bounds.y + (bounds.height / 2.0) @@ -123,7 +123,7 @@ where renderer.fill_quad( renderer::Quad { bounds, - border_radius: style.radius, + border_radius: style.radius.into(), border_width: 0.0, border_color: Color::TRANSPARENT, }, diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index 4ebb07a0..a5e0e0e3 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -233,7 +233,7 @@ where self.scrollbar_width, self.scrollbar_margin, self.scroller_width, - self.style, + &self.style, |renderer, layout, cursor_position, viewport| { self.content.as_widget().draw( &tree.children[0], @@ -276,13 +276,13 @@ where } fn overlay<'b>( - &'b self, + &'b mut self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option> { self.content - .as_widget() + .as_widget_mut() .overlay( &mut tree.children[0], layout.children().next().unwrap(), @@ -334,6 +334,12 @@ impl Id { } } +impl From for widget::Id { + fn from(id: Id) -> Self { + id.0 + } +} + /// Produces a [`Command`] that snaps the [`Scrollable`] with the given [`Id`] /// to the provided `percentage`. pub fn snap_to(id: Id, percentage: f32) -> Command { @@ -627,7 +633,7 @@ pub fn draw( scrollbar_width: u16, scrollbar_margin: u16, scroller_width: u16, - style: ::Style, + style: &::Style, draw_content: impl FnOnce(&mut Renderer, Layout<'_>, Point, &Rectangle), ) where Renderer: crate::Renderer, @@ -698,7 +704,7 @@ pub fn draw( renderer.fill_quad( renderer::Quad { bounds: scrollbar.bounds, - border_radius: style.border_radius, + border_radius: style.border_radius.into(), border_width: style.border_width, border_color: style.border_color, }, @@ -708,14 +714,13 @@ pub fn draw( ); } - if is_mouse_over - || state.is_scroller_grabbed() - || is_scrollbar_visible + if (is_mouse_over || state.is_scroller_grabbed()) + && is_scrollbar_visible { renderer.fill_quad( renderer::Quad { bounds: scrollbar.scroller.bounds, - border_radius: style.scroller.border_radius, + border_radius: style.scroller.border_radius.into(), border_width: style.scroller.border_width, border_color: style.scroller.border_color, }, diff --git a/native/src/widget/slider.rs b/native/src/widget/slider.rs index 33d87014..bf3383d9 100644 --- a/native/src/widget/slider.rs +++ b/native/src/widget/slider.rs @@ -238,7 +238,7 @@ where self.value, &self.range, theme, - self.style, + &self.style, self.orientation, ) } @@ -394,7 +394,7 @@ pub fn draw( value: T, range: &RangeInclusive, style_sheet: &dyn StyleSheet