diff options
67 files changed, 3471 insertions, 897 deletions
diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml index 9d2ed825..d4c94fcd 100644 --- a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml +++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml @@ -69,8 +69,8 @@ body: - type: dropdown id: os attributes: - label: Operative System - description: Which operative system are you using? + label: Operating System + description: Which operating system are you using? options: - Windows - macOS diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index f0cc952e..5d49699e 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -64,8 +64,8 @@ impl text::Renderer for Null { _font: Font, _bounds: Size, _shaping: text::Shaping, - ) -> (f32, f32) { - (0.0, 20.0) + ) -> Size { + Size::new(0.0, 20.0) } fn hit_test( diff --git a/core/src/text.rs b/core/src/text.rs index c154cc27..fc8aa20e 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -163,7 +163,7 @@ pub trait Renderer: crate::Renderer { font: Self::Font, bounds: Size, shaping: Shaping, - ) -> (f32, f32); + ) -> Size; /// Measures the width of the text as if it were laid out in a single line. fn measure_width( @@ -173,7 +173,7 @@ pub trait Renderer: crate::Renderer { font: Self::Font, shaping: Shaping, ) -> f32 { - let (width, _) = self.measure( + let bounds = self.measure( content, size, LineHeight::Absolute(Pixels(size)), @@ -182,7 +182,7 @@ pub trait Renderer: crate::Renderer { shaping, ); - width + bounds.width } /// Tests whether the provided point is within the boundaries of text diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index e934a2f5..79df2b02 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -5,7 +5,7 @@ use crate::mouse; use crate::renderer; use crate::text; use crate::widget::Tree; -use crate::{Color, Element, Layout, Length, Pixels, Rectangle, Size, Widget}; +use crate::{Color, Element, Layout, Length, Pixels, Rectangle, Widget}; use std::borrow::Cow; @@ -139,18 +139,16 @@ where let size = self.size.unwrap_or_else(|| renderer.default_size()); - let bounds = limits.max(); - - let (width, height) = renderer.measure( + let bounds = renderer.measure( &self.content, size, self.line_height, self.font.unwrap_or_else(|| renderer.default_font()), - bounds, + limits.max(), self.shaping, ); - let size = limits.resolve(Size::new(width, height)); + let size = limits.resolve(bounds); layout::Node::new(size) } diff --git a/examples/geometry/Cargo.toml b/examples/geometry/Cargo.toml index 79fe52d5..6068d651 100644 --- a/examples/geometry/Cargo.toml +++ b/examples/geometry/Cargo.toml @@ -7,4 +7,3 @@ publish = false [dependencies] iced = { path = "../..", features = ["advanced"] } -iced_graphics = { path = "../../graphics" } diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index 29f78ea1..3bc7f46b 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -1,8 +1,6 @@ //! This example showcases a simple native custom widget that renders using //! arbitrary low-level geometry. mod rainbow { - use iced_graphics::primitive::{ColoredVertex2D, Primitive}; - use iced::advanced::graphics::color; use iced::advanced::layout::{self, Layout}; use iced::advanced::renderer; @@ -46,8 +44,8 @@ mod rainbow { cursor: mouse::Cursor, _viewport: &Rectangle, ) { + use iced::advanced::graphics::mesh::{self, Mesh, SolidVertex2D}; use iced::advanced::Renderer as _; - use iced_graphics::primitive::Mesh2D; let bounds = layout.bounds(); @@ -78,43 +76,43 @@ mod rainbow { let posn_bl = [0.0, bounds.height]; let posn_l = [0.0, bounds.height / 2.0]; - let mesh = Primitive::SolidMesh { + let mesh = Mesh::Solid { size: bounds.size(), - buffers: Mesh2D { + buffers: mesh::Indexed { vertices: vec![ - ColoredVertex2D { + SolidVertex2D { position: posn_center, color: color::pack([1.0, 1.0, 1.0, 1.0]), }, - ColoredVertex2D { + SolidVertex2D { position: posn_tl, color: color::pack(color_r), }, - ColoredVertex2D { + SolidVertex2D { position: posn_t, color: color::pack(color_o), }, - ColoredVertex2D { + SolidVertex2D { position: posn_tr, color: color::pack(color_y), }, - ColoredVertex2D { + SolidVertex2D { position: posn_r, color: color::pack(color_g), }, - ColoredVertex2D { + SolidVertex2D { position: posn_br, color: color::pack(color_gb), }, - ColoredVertex2D { + SolidVertex2D { position: posn_b, color: color::pack(color_b), }, - ColoredVertex2D { + SolidVertex2D { position: posn_bl, color: color::pack(color_i), }, - ColoredVertex2D { + SolidVertex2D { position: posn_l, color: color::pack(color_v), }, @@ -135,7 +133,7 @@ mod rainbow { renderer.with_translation( Vector::new(bounds.x, bounds.y), |renderer| { - renderer.draw_primitive(mesh); + renderer.draw_mesh(mesh); }, ); } diff --git a/examples/loading_spinners/Cargo.toml b/examples/loading_spinners/Cargo.toml new file mode 100644 index 00000000..ee9a48aa --- /dev/null +++ b/examples/loading_spinners/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "loading_spinners" +version = "0.1.0" +authors = ["Nick Senger <dev@nsenger.com>"] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../..", features = ["advanced", "canvas"] } +lyon_algorithms = "1" +once_cell = "1" diff --git a/examples/loading_spinners/README.md b/examples/loading_spinners/README.md new file mode 100644 index 00000000..3573c6f6 --- /dev/null +++ b/examples/loading_spinners/README.md @@ -0,0 +1,14 @@ +## Loading Spinners + +Example implementation of animated indeterminate loading spinners. + +<div align="center"> + <a href="https://gfycat.com/importantdevotedhammerheadbird"> + <img src="https://thumbs.gfycat.com/ImportantDevotedHammerheadbird-small.gif"> + </a> +</div> + +You can run it with `cargo run`: +``` +cargo run --package loading_spinners +``` diff --git a/examples/loading_spinners/src/circular.rs b/examples/loading_spinners/src/circular.rs new file mode 100644 index 00000000..3a35e029 --- /dev/null +++ b/examples/loading_spinners/src/circular.rs @@ -0,0 +1,421 @@ +//! Show a circular progress indicator. +use iced::advanced::layout; +use iced::advanced::renderer; +use iced::advanced::widget::tree::{self, Tree}; +use iced::advanced::{Clipboard, Layout, Renderer, Shell, Widget}; +use iced::event; +use iced::mouse; +use iced::time::Instant; +use iced::widget::canvas; +use iced::window::{self, RedrawRequest}; +use iced::{ + Background, Color, Element, Event, Length, Rectangle, Size, Vector, +}; + +use super::easing::{self, Easing}; + +use std::f32::consts::PI; +use std::time::Duration; + +const MIN_RADIANS: f32 = PI / 8.0; +const WRAP_RADIANS: f32 = 2.0 * PI - PI / 4.0; +const BASE_ROTATION_SPEED: u32 = u32::MAX / 80; + +#[allow(missing_debug_implementations)] +pub struct Circular<'a, Theme> +where + Theme: StyleSheet, +{ + size: f32, + bar_height: f32, + style: <Theme as StyleSheet>::Style, + easing: &'a Easing, + cycle_duration: Duration, + rotation_duration: Duration, +} + +impl<'a, Theme> Circular<'a, Theme> +where + Theme: StyleSheet, +{ + /// Creates a new [`Circular`] with the given content. + pub fn new() -> Self { + Circular { + size: 40.0, + bar_height: 4.0, + style: <Theme as StyleSheet>::Style::default(), + easing: &easing::STANDARD, + cycle_duration: Duration::from_millis(600), + rotation_duration: Duration::from_secs(2), + } + } + + /// Sets the size of the [`Circular`]. + pub fn size(mut self, size: f32) -> Self { + self.size = size; + self + } + + /// Sets the bar height of the [`Circular`]. + pub fn bar_height(mut self, bar_height: f32) -> Self { + self.bar_height = bar_height; + self + } + + /// Sets the style variant of this [`Circular`]. + pub fn style(mut self, style: <Theme as StyleSheet>::Style) -> Self { + self.style = style; + self + } + + /// Sets the easing of this [`Circular`]. + pub fn easing(mut self, easing: &'a Easing) -> Self { + self.easing = easing; + self + } + + /// Sets the cycle duration of this [`Circular`]. + pub fn cycle_duration(mut self, duration: Duration) -> Self { + self.cycle_duration = duration / 2; + self + } + + /// Sets the base rotation duration of this [`Circular`]. This is the duration that a full + /// rotation would take if the cycle rotation were set to 0.0 (no expanding or contracting) + pub fn rotation_duration(mut self, duration: Duration) -> Self { + self.rotation_duration = duration; + self + } +} + +impl<'a, Theme> Default for Circular<'a, Theme> +where + Theme: StyleSheet, +{ + fn default() -> Self { + Self::new() + } +} + +#[derive(Clone, Copy)] +enum Animation { + Expanding { + start: Instant, + progress: f32, + rotation: u32, + last: Instant, + }, + Contracting { + start: Instant, + progress: f32, + rotation: u32, + last: Instant, + }, +} + +impl Default for Animation { + fn default() -> Self { + Self::Expanding { + start: Instant::now(), + progress: 0.0, + rotation: 0, + last: Instant::now(), + } + } +} + +impl Animation { + fn next(&self, additional_rotation: u32, now: Instant) -> Self { + match self { + Self::Expanding { rotation, .. } => Self::Contracting { + start: now, + progress: 0.0, + rotation: rotation.wrapping_add(additional_rotation), + last: now, + }, + Self::Contracting { rotation, .. } => Self::Expanding { + start: now, + progress: 0.0, + rotation: rotation.wrapping_add( + BASE_ROTATION_SPEED.wrapping_add( + ((WRAP_RADIANS / (2.0 * PI)) * u32::MAX as f32) as u32, + ), + ), + last: now, + }, + } + } + + fn start(&self) -> Instant { + match self { + Self::Expanding { start, .. } | Self::Contracting { start, .. } => { + *start + } + } + } + + fn last(&self) -> Instant { + match self { + Self::Expanding { last, .. } | Self::Contracting { last, .. } => { + *last + } + } + } + + fn timed_transition( + &self, + cycle_duration: Duration, + rotation_duration: Duration, + now: Instant, + ) -> Self { + let elapsed = now.duration_since(self.start()); + let additional_rotation = ((now - self.last()).as_secs_f32() + / rotation_duration.as_secs_f32() + * (u32::MAX) as f32) as u32; + + match elapsed { + elapsed if elapsed > cycle_duration => { + self.next(additional_rotation, now) + } + _ => self.with_elapsed( + cycle_duration, + additional_rotation, + elapsed, + now, + ), + } + } + + fn with_elapsed( + &self, + cycle_duration: Duration, + additional_rotation: u32, + elapsed: Duration, + now: Instant, + ) -> Self { + let progress = elapsed.as_secs_f32() / cycle_duration.as_secs_f32(); + match self { + Self::Expanding { + start, rotation, .. + } => Self::Expanding { + start: *start, + progress, + rotation: rotation.wrapping_add(additional_rotation), + last: now, + }, + Self::Contracting { + start, rotation, .. + } => Self::Contracting { + start: *start, + progress, + rotation: rotation.wrapping_add(additional_rotation), + last: now, + }, + } + } + + fn rotation(&self) -> f32 { + match self { + Self::Expanding { rotation, .. } + | Self::Contracting { rotation, .. } => { + *rotation as f32 / u32::MAX as f32 + } + } + } +} + +#[derive(Default)] +struct State { + animation: Animation, + cache: canvas::Cache, +} + +impl<'a, Message, Theme> Widget<Message, iced::Renderer<Theme>> + for Circular<'a, Theme> +where + Message: 'a + Clone, + Theme: StyleSheet, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State>() + } + + fn state(&self) -> tree::State { + tree::State::new(State::default()) + } + + fn width(&self) -> Length { + Length::Fixed(self.size) + } + + fn height(&self) -> Length { + Length::Fixed(self.size) + } + + fn layout( + &self, + _renderer: &iced::Renderer<Theme>, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.size).height(self.size); + let size = limits.resolve(Size::ZERO); + + layout::Node::new(size) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + _layout: Layout<'_>, + _cursor: mouse::Cursor, + _renderer: &iced::Renderer<Theme>, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + const FRAME_RATE: u64 = 60; + + let state = tree.state.downcast_mut::<State>(); + + if let Event::Window(window::Event::RedrawRequested(now)) = event { + state.animation = state.animation.timed_transition( + self.cycle_duration, + self.rotation_duration, + now, + ); + + state.cache.clear(); + shell.request_redraw(RedrawRequest::At( + now + Duration::from_millis(1000 / FRAME_RATE), + )); + } + + event::Status::Ignored + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut iced::Renderer<Theme>, + theme: &Theme, + _style: &renderer::Style, + layout: Layout<'_>, + _cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + let state = tree.state.downcast_ref::<State>(); + let bounds = layout.bounds(); + let custom_style = + <Theme as StyleSheet>::appearance(theme, &self.style); + + let geometry = state.cache.draw(renderer, bounds.size(), |frame| { + let track_radius = frame.width() / 2.0 - self.bar_height; + let track_path = canvas::Path::circle(frame.center(), track_radius); + + frame.stroke( + &track_path, + canvas::Stroke::default() + .with_color(custom_style.track_color) + .with_width(self.bar_height), + ); + + let mut builder = canvas::path::Builder::new(); + + let start = state.animation.rotation() * 2.0 * PI; + + match state.animation { + Animation::Expanding { progress, .. } => { + builder.arc(canvas::path::Arc { + center: frame.center(), + radius: track_radius, + start_angle: start, + end_angle: start + + MIN_RADIANS + + WRAP_RADIANS * (self.easing.y_at_x(progress)), + }); + } + Animation::Contracting { progress, .. } => { + builder.arc(canvas::path::Arc { + center: frame.center(), + radius: track_radius, + start_angle: start + + WRAP_RADIANS * (self.easing.y_at_x(progress)), + end_angle: start + MIN_RADIANS + WRAP_RADIANS, + }); + } + } + + let bar_path = builder.build(); + + frame.stroke( + &bar_path, + canvas::Stroke::default() + .with_color(custom_style.bar_color) + .with_width(self.bar_height), + ); + }); + + renderer.with_translation( + Vector::new(bounds.x, bounds.y), + |renderer| { + use iced::advanced::graphics::geometry::Renderer as _; + + renderer.draw(vec![geometry]); + }, + ); + } +} + +impl<'a, Message, Theme> From<Circular<'a, Theme>> + for Element<'a, Message, iced::Renderer<Theme>> +where + Message: Clone + 'a, + Theme: StyleSheet + 'a, +{ + fn from(circular: Circular<'a, Theme>) -> Self { + Self::new(circular) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Appearance { + /// The [`Background`] of the progress indicator. + pub background: Option<Background>, + /// The track [`Color`] of the progress indicator. + pub track_color: Color, + /// The bar [`Color`] of the progress indicator. + pub bar_color: Color, +} + +impl std::default::Default for Appearance { + fn default() -> Self { + Self { + background: None, + track_color: Color::TRANSPARENT, + bar_color: Color::BLACK, + } + } +} + +/// A set of rules that dictate the style of an indicator. +pub trait StyleSheet { + /// The supported style of the [`StyleSheet`]. + type Style: Default; + + /// Produces the active [`Appearance`] of a indicator. + fn appearance(&self, style: &Self::Style) -> Appearance; +} + +impl StyleSheet for iced::Theme { + type Style = (); + + fn appearance(&self, _style: &Self::Style) -> Appearance { + let palette = self.extended_palette(); + + Appearance { + background: None, + track_color: palette.background.weak.color, + bar_color: palette.primary.base.color, + } + } +} diff --git a/examples/loading_spinners/src/easing.rs b/examples/loading_spinners/src/easing.rs new file mode 100644 index 00000000..665b3329 --- /dev/null +++ b/examples/loading_spinners/src/easing.rs @@ -0,0 +1,133 @@ +use iced::Point; + +use lyon_algorithms::measure::PathMeasurements; +use lyon_algorithms::path::{builder::NoAttributes, path::BuilderImpl, Path}; +use once_cell::sync::Lazy; + +pub static EMPHASIZED: Lazy<Easing> = Lazy::new(|| { + Easing::builder() + .cubic_bezier_to([0.05, 0.0], [0.133333, 0.06], [0.166666, 0.4]) + .cubic_bezier_to([0.208333, 0.82], [0.25, 1.0], [1.0, 1.0]) + .build() +}); + +pub static EMPHASIZED_DECELERATE: Lazy<Easing> = Lazy::new(|| { + Easing::builder() + .cubic_bezier_to([0.05, 0.7], [0.1, 1.0], [1.0, 1.0]) + .build() +}); + +pub static EMPHASIZED_ACCELERATE: Lazy<Easing> = Lazy::new(|| { + Easing::builder() + .cubic_bezier_to([0.3, 0.0], [0.8, 0.15], [1.0, 1.0]) + .build() +}); + +pub static STANDARD: Lazy<Easing> = Lazy::new(|| { + Easing::builder() + .cubic_bezier_to([0.2, 0.0], [0.0, 1.0], [1.0, 1.0]) + .build() +}); + +pub static STANDARD_DECELERATE: Lazy<Easing> = Lazy::new(|| { + Easing::builder() + .cubic_bezier_to([0.0, 0.0], [0.0, 1.0], [1.0, 1.0]) + .build() +}); + +pub static STANDARD_ACCELERATE: Lazy<Easing> = Lazy::new(|| { + Easing::builder() + .cubic_bezier_to([0.3, 0.0], [1.0, 1.0], [1.0, 1.0]) + .build() +}); + +pub struct Easing { + path: Path, + measurements: PathMeasurements, +} + +impl Easing { + pub fn builder() -> Builder { + Builder::new() + } + + pub fn y_at_x(&self, x: f32) -> f32 { + let mut sampler = self.measurements.create_sampler( + &self.path, + lyon_algorithms::measure::SampleType::Normalized, + ); + let sample = sampler.sample(x); + + sample.position().y + } +} + +pub struct Builder(NoAttributes<BuilderImpl>); + +impl Builder { + pub fn new() -> Self { + let mut builder = Path::builder(); + builder.begin(lyon_algorithms::geom::point(0.0, 0.0)); + + Self(builder) + } + + /// Adds a line segment. Points must be between 0,0 and 1,1 + pub fn line_to(mut self, to: impl Into<Point>) -> Self { + self.0.line_to(Self::point(to)); + + self + } + + /// Adds a quadratic bézier curve. Points must be between 0,0 and 1,1 + pub fn quadratic_bezier_to( + mut self, + ctrl: impl Into<Point>, + to: impl Into<Point>, + ) -> Self { + self.0 + .quadratic_bezier_to(Self::point(ctrl), Self::point(to)); + + self + } + + /// Adds a cubic bézier curve. Points must be between 0,0 and 1,1 + pub fn cubic_bezier_to( + mut self, + ctrl1: impl Into<Point>, + ctrl2: impl Into<Point>, + to: impl Into<Point>, + ) -> Self { + self.0.cubic_bezier_to( + Self::point(ctrl1), + Self::point(ctrl2), + Self::point(to), + ); + + self + } + + pub fn build(mut self) -> Easing { + self.0.line_to(lyon_algorithms::geom::point(1.0, 1.0)); + self.0.end(false); + + let path = self.0.build(); + let measurements = PathMeasurements::from_path(&path, 0.0); + + Easing { path, measurements } + } + + fn point(p: impl Into<Point>) -> lyon_algorithms::geom::Point<f32> { + let p: Point = p.into(); + lyon_algorithms::geom::point( + p.x.min(1.0).max(0.0), + p.y.min(1.0).max(0.0), + ) + } +} + +impl Default for Builder { + fn default() -> Self { + Self::new() + } +} diff --git a/examples/loading_spinners/src/linear.rs b/examples/loading_spinners/src/linear.rs new file mode 100644 index 00000000..3d95729b --- /dev/null +++ b/examples/loading_spinners/src/linear.rs @@ -0,0 +1,326 @@ +//! Show a linear progress indicator. +use iced::advanced::layout; +use iced::advanced::renderer::{self, Quad}; +use iced::advanced::widget::tree::{self, Tree}; +use iced::advanced::{Clipboard, Layout, Shell, Widget}; +use iced::event; +use iced::mouse; +use iced::time::Instant; +use iced::window::{self, RedrawRequest}; +use iced::{Background, Color, Element, Event, Length, Rectangle, Size}; + +use super::easing::{self, Easing}; + +use std::time::Duration; + +#[allow(missing_debug_implementations)] +pub struct Linear<'a, Renderer> +where + Renderer: iced::advanced::Renderer, + Renderer::Theme: StyleSheet, +{ + width: Length, + height: Length, + style: <Renderer::Theme as StyleSheet>::Style, + easing: &'a Easing, + cycle_duration: Duration, +} + +impl<'a, Renderer> Linear<'a, Renderer> +where + Renderer: iced::advanced::Renderer, + Renderer::Theme: StyleSheet, +{ + /// Creates a new [`Linear`] with the given content. + pub fn new() -> Self { + Linear { + width: Length::Fixed(100.0), + height: Length::Fixed(4.0), + style: <Renderer::Theme as StyleSheet>::Style::default(), + easing: &easing::STANDARD, + cycle_duration: Duration::from_millis(600), + } + } + + /// Sets the width of the [`Linear`]. + pub fn width(mut self, width: impl Into<Length>) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Linear`]. + pub fn height(mut self, height: impl Into<Length>) -> Self { + self.height = height.into(); + self + } + + /// Sets the style variant of this [`Linear`]. + pub fn style( + mut self, + style: <Renderer::Theme as StyleSheet>::Style, + ) -> Self { + self.style = style; + self + } + + /// Sets the motion easing of this [`Linear`]. + pub fn easing(mut self, easing: &'a Easing) -> Self { + self.easing = easing; + self + } + + /// Sets the cycle duration of this [`Linear`]. + pub fn cycle_duration(mut self, duration: Duration) -> Self { + self.cycle_duration = duration / 2; + self + } +} + +impl<'a, Renderer> Default for Linear<'a, Renderer> +where + Renderer: iced::advanced::Renderer, + Renderer::Theme: StyleSheet, +{ + fn default() -> Self { + Self::new() + } +} + +#[derive(Clone, Copy)] +enum State { + Expanding { start: Instant, progress: f32 }, + Contracting { start: Instant, progress: f32 }, +} + +impl Default for State { + fn default() -> Self { + Self::Expanding { + start: Instant::now(), + progress: 0.0, + } + } +} + +impl State { + fn next(&self, now: Instant) -> Self { + match self { + Self::Expanding { .. } => Self::Contracting { + start: now, + progress: 0.0, + }, + Self::Contracting { .. } => Self::Expanding { + start: now, + progress: 0.0, + }, + } + } + + fn start(&self) -> Instant { + match self { + Self::Expanding { start, .. } | Self::Contracting { start, .. } => { + *start + } + } + } + + fn timed_transition(&self, cycle_duration: Duration, now: Instant) -> Self { + let elapsed = now.duration_since(self.start()); + + match elapsed { + elapsed if elapsed > cycle_duration => self.next(now), + _ => self.with_elapsed(cycle_duration, elapsed), + } + } + + fn with_elapsed( + &self, + cycle_duration: Duration, + elapsed: Duration, + ) -> Self { + let progress = elapsed.as_secs_f32() / cycle_duration.as_secs_f32(); + match self { + Self::Expanding { start, .. } => Self::Expanding { + start: *start, + progress, + }, + Self::Contracting { start, .. } => Self::Contracting { + start: *start, + progress, + }, + } + } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> for Linear<'a, Renderer> +where + Message: 'a + Clone, + Renderer: 'a + iced::advanced::Renderer, + Renderer::Theme: StyleSheet, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State>() + } + + fn state(&self) -> tree::State { + tree::State::new(State::default()) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + let size = limits.resolve(Size::ZERO); + + layout::Node::new(size) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + _layout: Layout<'_>, + _cursor: mouse::Cursor, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + const FRAME_RATE: u64 = 60; + + let state = tree.state.downcast_mut::<State>(); + + if let Event::Window(window::Event::RedrawRequested(now)) = event { + *state = state.timed_transition(self.cycle_duration, now); + + shell.request_redraw(RedrawRequest::At( + now + Duration::from_millis(1000 / FRAME_RATE), + )); + } + + event::Status::Ignored + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + _cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + let custom_style = theme.appearance(&self.style); + let state = tree.state.downcast_ref::<State>(); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: bounds.y, + width: bounds.width, + height: bounds.height, + }, + border_radius: 0.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + Background::Color(custom_style.track_color), + ); + + match state { + State::Expanding { progress, .. } => renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: bounds.y, + width: self.easing.y_at_x(*progress) * bounds.width, + height: bounds.height, + }, + border_radius: 0.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + Background::Color(custom_style.bar_color), + ), + + State::Contracting { progress, .. } => renderer.fill_quad( + Quad { + bounds: Rectangle { + x: bounds.x + + self.easing.y_at_x(*progress) * bounds.width, + y: bounds.y, + width: (1.0 - self.easing.y_at_x(*progress)) + * bounds.width, + height: bounds.height, + }, + border_radius: 0.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + Background::Color(custom_style.bar_color), + ), + } + } +} + +impl<'a, Message, Renderer> From<Linear<'a, Renderer>> + for Element<'a, Message, Renderer> +where + Message: Clone + 'a, + Renderer: iced::advanced::Renderer + 'a, + Renderer::Theme: StyleSheet, +{ + fn from(linear: Linear<'a, Renderer>) -> Self { + Self::new(linear) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Appearance { + /// The track [`Color`] of the progress indicator. + pub track_color: Color, + /// The bar [`Color`] of the progress indicator. + pub bar_color: Color, +} + +impl std::default::Default for Appearance { + fn default() -> Self { + Self { + track_color: Color::TRANSPARENT, + bar_color: Color::BLACK, + } + } +} + +/// A set of rules that dictate the style of an indicator. +pub trait StyleSheet { + /// The supported style of the [`StyleSheet`]. + type Style: Default; + + /// Produces the active [`Appearance`] of a indicator. + fn appearance(&self, style: &Self::Style) -> Appearance; +} + +impl StyleSheet for iced::Theme { + type Style = (); + + fn appearance(&self, _style: &Self::Style) -> Appearance { + let palette = self.extended_palette(); + + Appearance { + track_color: palette.background.weak.color, + bar_color: palette.primary.base.color, + } + } +} diff --git a/examples/loading_spinners/src/main.rs b/examples/loading_spinners/src/main.rs new file mode 100644 index 00000000..a78e9590 --- /dev/null +++ b/examples/loading_spinners/src/main.rs @@ -0,0 +1,118 @@ +use iced::executor; +use iced::widget::{column, container, row, slider, text}; +use iced::{Application, Command, Element, Length, Settings, Theme}; + +use std::time::Duration; + +mod circular; +mod easing; +mod linear; + +use circular::Circular; +use linear::Linear; + +pub fn main() -> iced::Result { + LoadingSpinners::run(Settings { + antialiasing: true, + ..Default::default() + }) +} + +struct LoadingSpinners { + cycle_duration: f32, +} + +impl Default for LoadingSpinners { + fn default() -> Self { + Self { + cycle_duration: 2.0, + } + } +} + +#[derive(Debug, Clone, Copy)] +enum Message { + CycleDurationChanged(f32), +} + +impl Application for LoadingSpinners { + type Message = Message; + type Flags = (); + type Executor = executor::Default; + type Theme = Theme; + + fn new(_flags: Self::Flags) -> (Self, Command<Message>) { + (Self::default(), Command::none()) + } + + fn title(&self) -> String { + String::from("Loading Spinners - Iced") + } + + fn update(&mut self, message: Message) -> Command<Message> { + match message { + Message::CycleDurationChanged(duration) => { + self.cycle_duration = duration; + } + } + + Command::none() + } + + fn view(&self) -> Element<Message> { + let column = [ + &easing::EMPHASIZED, + &easing::EMPHASIZED_DECELERATE, + &easing::EMPHASIZED_ACCELERATE, + &easing::STANDARD, + &easing::STANDARD_DECELERATE, + &easing::STANDARD_ACCELERATE, + ] + .iter() + .zip([ + "Emphasized:", + "Emphasized Decelerate:", + "Emphasized Accelerate:", + "Standard:", + "Standard Decelerate:", + "Standard Accelerate:", + ]) + .fold(column![], |column, (easing, label)| { + column.push( + row![ + text(label).width(250), + Linear::new().easing(easing).cycle_duration( + Duration::from_secs_f32(self.cycle_duration) + ), + Circular::new().easing(easing).cycle_duration( + Duration::from_secs_f32(self.cycle_duration) + ) + ] + .align_items(iced::Alignment::Center) + .spacing(20.0), + ) + }) + .spacing(20); + + container( + column.push( + row(vec![ + text("Cycle duration:").into(), + slider(1.0..=1000.0, self.cycle_duration * 100.0, |x| { + Message::CycleDurationChanged(x / 100.0) + }) + .width(200.0) + .into(), + text(format!("{:.2}s", self.cycle_duration)).into(), + ]) + .align_items(iced::Alignment::Center) + .spacing(20.0), + ), + ) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } +} diff --git a/examples/screenshot/Cargo.toml b/examples/screenshot/Cargo.toml new file mode 100644 index 00000000..b79300b7 --- /dev/null +++ b/examples/screenshot/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "screenshot" +version = "0.1.0" +authors = ["Bingus <shankern@protonmail.com>"] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../..", features = ["debug", "image", "advanced"] } +image = { version = "0.24.6", features = ["png"]} +env_logger = "0.10.0" diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs new file mode 100644 index 00000000..83824535 --- /dev/null +++ b/examples/screenshot/src/main.rs @@ -0,0 +1,320 @@ +use iced::alignment; +use iced::keyboard::KeyCode; +use iced::theme::{Button, Container}; +use iced::widget::{button, column, container, image, row, text, text_input}; +use iced::window::screenshot::{self, Screenshot}; +use iced::{ + event, executor, keyboard, subscription, Alignment, Application, Command, + ContentFit, Element, Event, Length, Rectangle, Renderer, Subscription, + Theme, +}; + +use ::image as img; +use ::image::ColorType; + +fn main() -> iced::Result { + env_logger::builder().format_timestamp(None).init(); + + Example::run(iced::Settings::default()) +} + +struct Example { + screenshot: Option<Screenshot>, + saved_png_path: Option<Result<String, PngError>>, + png_saving: bool, + crop_error: Option<screenshot::CropError>, + x_input_value: Option<u32>, + y_input_value: Option<u32>, + width_input_value: Option<u32>, + height_input_value: Option<u32>, +} + +#[derive(Clone, Debug)] +enum Message { + Crop, + Screenshot, + ScreenshotData(Screenshot), + Png, + PngSaved(Result<String, PngError>), + XInputChanged(Option<u32>), + YInputChanged(Option<u32>), + WidthInputChanged(Option<u32>), + HeightInputChanged(Option<u32>), +} + +impl Application for Example { + type Executor = executor::Default; + type Message = Message; + type Theme = Theme; + type Flags = (); + + fn new(_flags: Self::Flags) -> (Self, Command<Self::Message>) { + ( + Example { + screenshot: None, + saved_png_path: None, + png_saving: false, + crop_error: None, + x_input_value: None, + y_input_value: None, + width_input_value: None, + height_input_value: None, + }, + Command::none(), + ) + } + + fn title(&self) -> String { + "Screenshot".to_string() + } + + fn update(&mut self, message: Self::Message) -> Command<Self::Message> { + match message { + Message::Screenshot => { + return iced::window::screenshot(Message::ScreenshotData); + } + Message::ScreenshotData(screenshot) => { + self.screenshot = Some(screenshot); + } + Message::Png => { + if let Some(screenshot) = &self.screenshot { + self.png_saving = true; + + return Command::perform( + save_to_png(screenshot.clone()), + Message::PngSaved, + ); + } + } + Message::PngSaved(res) => { + self.png_saving = false; + self.saved_png_path = Some(res); + } + Message::XInputChanged(new_value) => { + self.x_input_value = new_value; + } + Message::YInputChanged(new_value) => { + self.y_input_value = new_value; + } + Message::WidthInputChanged(new_value) => { + self.width_input_value = new_value; + } + Message::HeightInputChanged(new_value) => { + self.height_input_value = new_value; + } + Message::Crop => { + if let Some(screenshot) = &self.screenshot { + let cropped = screenshot.crop(Rectangle::<u32> { + x: self.x_input_value.unwrap_or(0), + y: self.y_input_value.unwrap_or(0), + width: self.width_input_value.unwrap_or(0), + height: self.height_input_value.unwrap_or(0), + }); + + match cropped { + Ok(screenshot) => { + self.screenshot = Some(screenshot); + self.crop_error = None; + } + Err(crop_error) => { + self.crop_error = Some(crop_error); + } + } + } + } + } + + Command::none() + } + + fn view(&self) -> Element<'_, Self::Message, Renderer<Self::Theme>> { + let image: Element<Message> = if let Some(screenshot) = &self.screenshot + { + image(image::Handle::from_pixels( + screenshot.size.width, + screenshot.size.height, + screenshot.clone(), + )) + .content_fit(ContentFit::Contain) + .width(Length::Fill) + .height(Length::Fill) + .into() + } else { + text("Press the button to take a screenshot!").into() + }; + + let image = container(image) + .padding(10) + .style(Container::Box) + .width(Length::FillPortion(2)) + .height(Length::Fill) + .center_x() + .center_y(); + + let crop_origin_controls = row![ + text("X:") + .vertical_alignment(alignment::Vertical::Center) + .width(30), + numeric_input("0", self.x_input_value).map(Message::XInputChanged), + text("Y:") + .vertical_alignment(alignment::Vertical::Center) + .width(30), + numeric_input("0", self.y_input_value).map(Message::YInputChanged) + ] + .spacing(10) + .align_items(Alignment::Center); + + let crop_dimension_controls = row![ + text("W:") + .vertical_alignment(alignment::Vertical::Center) + .width(30), + numeric_input("0", self.width_input_value) + .map(Message::WidthInputChanged), + text("H:") + .vertical_alignment(alignment::Vertical::Center) + .width(30), + numeric_input("0", self.height_input_value) + .map(Message::HeightInputChanged) + ] + .spacing(10) + .align_items(Alignment::Center); + + let mut crop_controls = + column![crop_origin_controls, crop_dimension_controls] + .spacing(10) + .align_items(Alignment::Center); + + if let Some(crop_error) = &self.crop_error { + crop_controls = crop_controls + .push(text(format!("Crop error! \n{}", crop_error))); + } + + let mut controls = column![ + column![ + button(centered_text("Screenshot!")) + .padding([10, 20, 10, 20]) + .width(Length::Fill) + .on_press(Message::Screenshot), + if !self.png_saving { + button(centered_text("Save as png")).on_press_maybe( + self.screenshot.is_some().then(|| Message::Png), + ) + } else { + button(centered_text("Saving...")).style(Button::Secondary) + } + .style(Button::Secondary) + .padding([10, 20, 10, 20]) + .width(Length::Fill) + ] + .spacing(10), + column![ + crop_controls, + button(centered_text("Crop")) + .on_press(Message::Crop) + .style(Button::Destructive) + .padding([10, 20, 10, 20]) + .width(Length::Fill), + ] + .spacing(10) + .align_items(Alignment::Center), + ] + .spacing(40); + + if let Some(png_result) = &self.saved_png_path { + let msg = match png_result { + Ok(path) => format!("Png saved as: {:?}!", path), + Err(msg) => { + format!("Png could not be saved due to:\n{:?}", msg) + } + }; + + controls = controls.push(text(msg)); + } + + let side_content = container(controls) + .align_x(alignment::Horizontal::Center) + .width(Length::FillPortion(1)) + .height(Length::Fill) + .center_y() + .center_x(); + + let content = row![side_content, image] + .spacing(10) + .width(Length::Fill) + .height(Length::Fill) + .align_items(Alignment::Center); + + container(content) + .width(Length::Fill) + .height(Length::Fill) + .padding(10) + .center_x() + .center_y() + .into() + } + + fn subscription(&self) -> Subscription<Self::Message> { + subscription::events_with(|event, status| { + if let event::Status::Captured = status { + return None; + } + + if let Event::Keyboard(keyboard::Event::KeyPressed { + key_code: KeyCode::F5, + .. + }) = event + { + Some(Message::Screenshot) + } else { + None + } + }) + } +} + +async fn save_to_png(screenshot: Screenshot) -> Result<String, PngError> { + let path = "screenshot.png".to_string(); + img::save_buffer( + &path, + &screenshot.bytes, + screenshot.size.width, + screenshot.size.height, + ColorType::Rgba8, + ) + .map(|_| path) + .map_err(|err| PngError(format!("{:?}", err))) +} + +#[derive(Clone, Debug)] +struct PngError(String); + +fn numeric_input( + placeholder: &str, + value: Option<u32>, +) -> Element<'_, Option<u32>> { + text_input( + placeholder, + &value + .as_ref() + .map(ToString::to_string) + .unwrap_or_else(String::new), + ) + .on_input(move |text| { + if text.is_empty() { + None + } else if let Ok(new_value) = text.parse() { + Some(new_value) + } else { + value + } + }) + .width(40) + .into() +} + +fn centered_text(content: &str) -> Element<'_, Message> { + text(content) + .width(Length::Fill) + .horizontal_alignment(alignment::Horizontal::Center) + .into() +} diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index 3038661e..4104871f 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -5,6 +5,7 @@ use iced::widget::{ }; use iced::{executor, theme, Alignment, Color}; use iced::{Application, Command, Element, Length, Settings, Theme}; + use once_cell::sync::Lazy; static SCROLLABLE_ID: Lazy<scrollable::Id> = Lazy::new(scrollable::Id::unique); @@ -199,12 +200,12 @@ impl Application for ScrollableDemo { .spacing(40), ) .height(Length::Fill) - .vertical_scroll( + .direction(scrollable::Direction::Vertical( Properties::new() .width(self.scrollbar_width) .margin(self.scrollbar_margin) .scroller_width(self.scroller_width), - ) + )) .id(SCROLLABLE_ID.clone()) .on_scroll(Message::Scrolled), Direction::Horizontal => scrollable( @@ -223,12 +224,12 @@ impl Application for ScrollableDemo { .spacing(40), ) .height(Length::Fill) - .horizontal_scroll( + .direction(scrollable::Direction::Horizontal( Properties::new() .width(self.scrollbar_width) .margin(self.scrollbar_margin) .scroller_width(self.scroller_width), - ) + )) .style(theme::Scrollable::custom(ScrollbarCustomStyle)) .id(SCROLLABLE_ID.clone()) .on_scroll(Message::Scrolled), @@ -264,18 +265,17 @@ impl Application for ScrollableDemo { .spacing(40), ) .height(Length::Fill) - .vertical_scroll( - Properties::new() - .width(self.scrollbar_width) - .margin(self.scrollbar_margin) - .scroller_width(self.scroller_width), - ) - .horizontal_scroll( - Properties::new() + .direction({ + let properties = Properties::new() .width(self.scrollbar_width) .margin(self.scrollbar_margin) - .scroller_width(self.scroller_width), - ) + .scroller_width(self.scroller_width); + + scrollable::Direction::Both { + horizontal: properties, + vertical: properties, + } + }) .style(theme::Scrollable::Custom(Box::new( ScrollbarCustomStyle, ))) diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index e2015bac..f8a4c80a 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -127,7 +127,9 @@ impl Sandbox for Styling { let content = column![ choose_theme, horizontal_rule(38), - row![text_input, button].spacing(10), + row![text_input, button] + .spacing(10) + .align_items(Alignment::Center), slider, progress_bar, row![ diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index 0e22227d..7a9e6aee 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -18,6 +18,7 @@ web-colors = [] [dependencies] glam = "0.24" +half = "2.2.1" log = "0.4" raw-window-handle = "0.5" thiserror = "1.0" @@ -31,10 +32,6 @@ features = ["derive"] version = "0.9" path = "../core" -[dependencies.tiny-skia] -version = "0.9" -optional = true - [dependencies.image] version = "0.24" optional = true diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs index ae89da06..59e95bf8 100644 --- a/graphics/src/backend.rs +++ b/graphics/src/backend.rs @@ -10,6 +10,9 @@ use std::borrow::Cow; /// /// [`Renderer`]: crate::Renderer pub trait Backend { + /// The custom kind of primitives this [`Backend`] supports. + type Primitive; + /// Trims the measurements cache. /// /// This method is currently necessary to properly trim the text cache in @@ -50,7 +53,7 @@ pub trait Text { font: Font, bounds: Size, shaping: text::Shaping, - ) -> (f32, f32); + ) -> Size; /// Tests whether the provided point is within the boundaries of [`Text`] /// laid out with the given parameters, returning information about diff --git a/graphics/src/compositor.rs b/graphics/src/compositor.rs index d55e801a..f7b86045 100644 --- a/graphics/src/compositor.rs +++ b/graphics/src/compositor.rs @@ -59,6 +59,19 @@ pub trait Compositor: Sized { background_color: Color, overlay: &[T], ) -> Result<(), SurfaceError>; + + /// Screenshots the current [`Renderer`] primitives to an offscreen texture, and returns the bytes of + /// the texture ordered as `RGBA` in the sRGB color space. + /// + /// [`Renderer`]: Self::Renderer; + fn screenshot<T: AsRef<str>>( + &mut self, + renderer: &mut Self::Renderer, + surface: &mut Self::Surface, + viewport: &Viewport, + background_color: Color, + overlay: &[T], + ) -> Vec<u8>; } /// Result of an unsuccessful call to [`Compositor::present`]. @@ -82,7 +95,7 @@ pub enum SurfaceError { OutOfMemory, } -/// Contains informations about the graphics (e.g. graphics adapter, graphics backend). +/// Contains information about the graphics (e.g. graphics adapter, graphics backend). #[derive(Debug)] pub struct Information { /// Contains the graphics adapter. diff --git a/graphics/src/damage.rs b/graphics/src/damage.rs index c6b0f759..2f29956e 100644 --- a/graphics/src/damage.rs +++ b/graphics/src/damage.rs @@ -1,11 +1,66 @@ //! Track and compute the damage of graphical primitives. +use crate::core::alignment; use crate::core::{Rectangle, Size}; use crate::Primitive; use std::sync::Arc; -/// Computes the damage regions between the two given primitives. -pub fn regions(a: &Primitive, b: &Primitive) -> Vec<Rectangle> { +/// A type that has some damage bounds. +pub trait Damage: PartialEq { + /// Returns the bounds of the [`Damage`]. + fn bounds(&self) -> Rectangle; +} + +impl<T: Damage> Damage for Primitive<T> { + fn bounds(&self) -> Rectangle { + match self { + Self::Text { + bounds, + horizontal_alignment, + vertical_alignment, + .. + } => { + let mut bounds = *bounds; + + bounds.x = match horizontal_alignment { + alignment::Horizontal::Left => bounds.x, + alignment::Horizontal::Center => { + bounds.x - bounds.width / 2.0 + } + alignment::Horizontal::Right => bounds.x - bounds.width, + }; + + bounds.y = match vertical_alignment { + alignment::Vertical::Top => bounds.y, + alignment::Vertical::Center => { + bounds.y - bounds.height / 2.0 + } + alignment::Vertical::Bottom => bounds.y - bounds.height, + }; + + bounds.expand(1.5) + } + Self::Quad { bounds, .. } + | Self::Image { bounds, .. } + | Self::Svg { bounds, .. } => bounds.expand(1.0), + Self::Clip { bounds, .. } => bounds.expand(1.0), + Self::Group { primitives } => primitives + .iter() + .map(Self::bounds) + .fold(Rectangle::with_size(Size::ZERO), |a, b| { + Rectangle::union(&a, &b) + }), + Self::Translate { + translation, + content, + } => content.bounds() + *translation, + Self::Cache { content } => content.bounds(), + Self::Custom(custom) => custom.bounds(), + } + } +} + +fn regions<T: Damage>(a: &Primitive<T>, b: &Primitive<T>) -> Vec<Rectangle> { match (a, b) { ( Primitive::Group { @@ -76,7 +131,10 @@ pub fn regions(a: &Primitive, b: &Primitive) -> Vec<Rectangle> { } /// Computes the damage regions between the two given lists of primitives. -pub fn list(previous: &[Primitive], current: &[Primitive]) -> Vec<Rectangle> { +pub fn list<T: Damage>( + previous: &[Primitive<T>], + current: &[Primitive<T>], +) -> Vec<Rectangle> { let damage = previous .iter() .zip(current) @@ -93,7 +151,7 @@ pub fn list(previous: &[Primitive], current: &[Primitive]) -> Vec<Rectangle> { // Extend damage by the added/removed primitives damage - .chain(bigger[smaller.len()..].iter().map(Primitive::bounds)) + .chain(bigger[smaller.len()..].iter().map(Damage::bounds)) .collect() } } diff --git a/graphics/src/geometry.rs b/graphics/src/geometry.rs index 729c3d44..7cd3dd3a 100644 --- a/graphics/src/geometry.rs +++ b/graphics/src/geometry.rs @@ -14,20 +14,11 @@ pub use text::Text; pub use crate::gradient::{self, Gradient}; -use crate::Primitive; - -/// A bunch of shapes that can be drawn. -#[derive(Debug, Clone)] -pub struct Geometry(pub Primitive); - -impl From<Geometry> for Primitive { - fn from(geometry: Geometry) -> Self { - geometry.0 - } -} - /// A renderer capable of drawing some [`Geometry`]. pub trait Renderer: crate::core::Renderer { + /// The kind of geometry this renderer can draw. + type Geometry; + /// Draws the given layers of [`Geometry`]. - fn draw(&mut self, layers: Vec<Geometry>); + fn draw(&mut self, layers: Vec<Self::Geometry>); } diff --git a/graphics/src/gradient.rs b/graphics/src/gradient.rs index d26b5665..4db565d8 100644 --- a/graphics/src/gradient.rs +++ b/graphics/src/gradient.rs @@ -7,6 +7,8 @@ use crate::color; use crate::core::gradient::ColorStop; use crate::core::{self, Color, Point, Rectangle}; +use bytemuck::{Pod, Zeroable}; +use half::f16; use std::cmp::Ordering; #[derive(Debug, Clone, PartialEq)] @@ -99,61 +101,96 @@ impl Linear { /// Packs the [`Gradient`] for use in shader code. pub fn pack(&self) -> Packed { - let mut data: [f32; 44] = [0.0; 44]; + let mut colors = [[0u32; 2]; 8]; + let mut offsets = [f16::from(0u8); 8]; for (index, stop) in self.stops.iter().enumerate() { let [r, g, b, a] = color::pack(stop.map_or(Color::default(), |s| s.color)) .components(); - data[index * 4] = r; - data[(index * 4) + 1] = g; - data[(index * 4) + 2] = b; - data[(index * 4) + 3] = a; + colors[index] = [ + pack_f16s([f16::from_f32(r), f16::from_f32(g)]), + pack_f16s([f16::from_f32(b), f16::from_f32(a)]), + ]; - data[32 + index] = stop.map_or(2.0, |s| s.offset); + offsets[index] = + stop.map_or(f16::from_f32(2.0), |s| f16::from_f32(s.offset)); } - data[40] = self.start.x; - data[41] = self.start.y; - data[42] = self.end.x; - data[43] = self.end.y; + let offsets = [ + pack_f16s([offsets[0], offsets[1]]), + pack_f16s([offsets[2], offsets[3]]), + pack_f16s([offsets[4], offsets[5]]), + pack_f16s([offsets[6], offsets[7]]), + ]; - Packed(data) + let direction = [self.start.x, self.start.y, self.end.x, self.end.y]; + + Packed { + colors, + offsets, + direction, + } } } /// Packed [`Gradient`] data for use in shader code. -#[derive(Debug, Copy, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq, Zeroable, Pod)] #[repr(C)] -pub struct Packed([f32; 44]); +pub struct Packed { + // 8 colors, each channel = 16 bit float, 2 colors packed into 1 u32 + colors: [[u32; 2]; 8], + // 8 offsets, 8x 16 bit floats packed into 4 u32s + offsets: [u32; 4], + direction: [f32; 4], +} /// Creates a new [`Packed`] gradient for use in shader code. pub fn pack(gradient: &core::Gradient, bounds: Rectangle) -> Packed { match gradient { core::Gradient::Linear(linear) => { - let mut data: [f32; 44] = [0.0; 44]; + let mut colors = [[0u32; 2]; 8]; + let mut offsets = [f16::from(0u8); 8]; for (index, stop) in linear.stops.iter().enumerate() { let [r, g, b, a] = color::pack(stop.map_or(Color::default(), |s| s.color)) .components(); - data[index * 4] = r; - data[(index * 4) + 1] = g; - data[(index * 4) + 2] = b; - data[(index * 4) + 3] = a; - data[32 + index] = stop.map_or(2.0, |s| s.offset); + colors[index] = [ + pack_f16s([f16::from_f32(r), f16::from_f32(g)]), + pack_f16s([f16::from_f32(b), f16::from_f32(a)]), + ]; + + offsets[index] = stop + .map_or(f16::from_f32(2.0), |s| f16::from_f32(s.offset)); } + let offsets = [ + pack_f16s([offsets[0], offsets[1]]), + pack_f16s([offsets[2], offsets[3]]), + pack_f16s([offsets[4], offsets[5]]), + pack_f16s([offsets[6], offsets[7]]), + ]; + let (start, end) = linear.angle.to_distance(&bounds); - data[40] = start.x; - data[41] = start.y; - data[42] = end.x; - data[43] = end.y; + let direction = [start.x, start.y, end.x, end.y]; - Packed(data) + Packed { + colors, + offsets, + direction, + } } } } + +/// Packs two f16s into one u32. +fn pack_f16s(f: [f16; 2]) -> u32 { + let one = (f[0].to_bits() as u32) << 16; + let two = f[1].to_bits() as u32; + + one | two +} diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index f6bc87fb..af374a2f 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -23,6 +23,7 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] mod antialiasing; mod error; +mod primitive; mod transformation; mod viewport; @@ -31,7 +32,7 @@ pub mod color; pub mod compositor; pub mod damage; pub mod gradient; -pub mod primitive; +pub mod mesh; pub mod renderer; #[cfg(feature = "geometry")] @@ -43,14 +44,13 @@ pub mod image; pub use antialiasing::Antialiasing; pub use backend::Backend; pub use compositor::Compositor; +pub use damage::Damage; pub use error::Error; pub use gradient::Gradient; +pub use mesh::Mesh; pub use primitive::Primitive; pub use renderer::Renderer; pub use transformation::Transformation; pub use viewport::Viewport; -#[cfg(feature = "geometry")] -pub use geometry::Geometry; - pub use iced_core as core; diff --git a/graphics/src/mesh.rs b/graphics/src/mesh.rs new file mode 100644 index 00000000..cfb5a60f --- /dev/null +++ b/graphics/src/mesh.rs @@ -0,0 +1,76 @@ +//! Draw triangles! +use crate::color; +use crate::core::{Rectangle, Size}; +use crate::gradient; +use crate::Damage; + +use bytemuck::{Pod, Zeroable}; + +/// A low-level primitive to render a mesh of triangles. +#[derive(Debug, Clone, PartialEq)] +pub enum Mesh { + /// A mesh with a solid color. + Solid { + /// The vertices and indices of the mesh. + buffers: Indexed<SolidVertex2D>, + + /// The size of the drawable region of the mesh. + /// + /// Any geometry that falls out of this region will be clipped. + size: Size, + }, + /// A mesh with a gradient. + Gradient { + /// The vertices and indices of the mesh. + buffers: Indexed<GradientVertex2D>, + + /// The size of the drawable region of the mesh. + /// + /// Any geometry that falls out of this region will be clipped. + size: Size, + }, +} + +impl Damage for Mesh { + fn bounds(&self) -> Rectangle { + match self { + Self::Solid { size, .. } | Self::Gradient { size, .. } => { + Rectangle::with_size(*size) + } + } + } +} + +/// A set of [`Vertex2D`] and indices representing a list of triangles. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Indexed<T> { + /// The vertices of the mesh + pub vertices: Vec<T>, + + /// The list of vertex indices that defines the triangles of the mesh. + /// + /// Therefore, this list should always have a length that is a multiple of 3. + pub indices: Vec<u32>, +} + +/// A two-dimensional vertex with a color. +#[derive(Copy, Clone, Debug, PartialEq, Zeroable, Pod)] +#[repr(C)] +pub struct SolidVertex2D { + /// The vertex position in 2D space. + pub position: [f32; 2], + + /// The color of the vertex in __linear__ RGBA. + pub color: color::Packed, +} + +/// A vertex which contains 2D position & packed gradient data. +#[derive(Copy, Clone, Debug, PartialEq, Zeroable, Pod)] +#[repr(C)] +pub struct GradientVertex2D { + /// The vertex position in 2D space. + pub position: [f32; 2], + + /// The packed vertex data of the gradient. + pub gradient: gradient::Packed, +} diff --git a/graphics/src/primitive.rs b/graphics/src/primitive.rs index 187c6c6c..7592a410 100644 --- a/graphics/src/primitive.rs +++ b/graphics/src/primitive.rs @@ -1,19 +1,15 @@ //! Draw using different graphical primitives. -use crate::color; use crate::core::alignment; use crate::core::image; use crate::core::svg; use crate::core::text; -use crate::core::{Background, Color, Font, Rectangle, Size, Vector}; -use crate::gradient; +use crate::core::{Background, Color, Font, Rectangle, Vector}; -use bytemuck::{Pod, Zeroable}; use std::sync::Arc; /// A rendering primitive. #[derive(Debug, Clone, PartialEq)] -#[non_exhaustive] -pub enum Primitive { +pub enum Primitive<T> { /// A text primitive Text { /// The contents of the text @@ -66,65 +62,17 @@ pub enum Primitive { /// The bounds of the viewport bounds: Rectangle, }, - /// A low-level primitive to render a mesh of triangles with a solid color. - /// - /// It can be used to render many kinds of geometry freely. - SolidMesh { - /// The vertices and indices of the mesh. - buffers: Mesh2D<ColoredVertex2D>, - - /// 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: Mesh2D<GradientVertex2D>, - - /// The size of the drawable region of the mesh. - /// - /// Any geometry that falls out of this region will be clipped. - size: Size, - }, - /// A [`tiny_skia`] path filled with some paint. - #[cfg(feature = "tiny-skia")] - Fill { - /// The path to fill. - path: tiny_skia::Path, - /// The paint to use. - paint: tiny_skia::Paint<'static>, - /// The fill rule to follow. - rule: tiny_skia::FillRule, - /// The transform to apply to the path. - transform: tiny_skia::Transform, - }, - /// A [`tiny_skia`] path stroked with some paint. - #[cfg(feature = "tiny-skia")] - Stroke { - /// The path to stroke. - path: tiny_skia::Path, - /// The paint to use. - paint: tiny_skia::Paint<'static>, - /// The stroke settings. - stroke: tiny_skia::Stroke, - /// The transform to apply to the path. - transform: tiny_skia::Transform, - }, /// A group of primitives Group { /// The primitives of the group - primitives: Vec<Primitive>, + primitives: Vec<Primitive<T>>, }, /// A clip primitive Clip { /// The bounds of the clip bounds: Rectangle, /// The content of the clip - content: Box<Primitive>, + content: Box<Primitive<T>>, }, /// A primitive that applies a translation Translate { @@ -132,7 +80,7 @@ pub enum Primitive { translation: Vector, /// The primitive to translate - content: Box<Primitive>, + content: Box<Primitive<T>>, }, /// A cached primitive. /// @@ -140,11 +88,13 @@ pub enum Primitive { /// generation is expensive. Cache { /// The cached primitive - content: Arc<Primitive>, + content: Arc<Primitive<T>>, }, + /// A backend-specific primitive. + Custom(T), } -impl Primitive { +impl<T> Primitive<T> { /// Creates a [`Primitive::Group`]. pub fn group(primitives: Vec<Self>) -> Self { Self::Group { primitives } @@ -165,112 +115,4 @@ impl Primitive { content: Box::new(self), } } - - /// Returns the bounds of the [`Primitive`]. - pub fn bounds(&self) -> Rectangle { - match self { - Self::Text { - bounds, - horizontal_alignment, - vertical_alignment, - .. - } => { - let mut bounds = *bounds; - - bounds.x = match horizontal_alignment { - alignment::Horizontal::Left => bounds.x, - alignment::Horizontal::Center => { - bounds.x - bounds.width / 2.0 - } - alignment::Horizontal::Right => bounds.x - bounds.width, - }; - - bounds.y = match vertical_alignment { - alignment::Vertical::Top => bounds.y, - alignment::Vertical::Center => { - bounds.y - bounds.height / 2.0 - } - alignment::Vertical::Bottom => bounds.y - bounds.height, - }; - - bounds.expand(1.5) - } - Self::Quad { bounds, .. } - | Self::Image { bounds, .. } - | Self::Svg { bounds, .. } => bounds.expand(1.0), - Self::Clip { bounds, .. } => bounds.expand(1.0), - Self::SolidMesh { size, .. } | Self::GradientMesh { size, .. } => { - Rectangle::with_size(*size) - } - #[cfg(feature = "tiny-skia")] - Self::Fill { path, .. } | Self::Stroke { path, .. } => { - let bounds = path.bounds(); - - Rectangle { - x: bounds.x(), - y: bounds.y(), - width: bounds.width(), - height: bounds.height(), - } - .expand(1.0) - } - Self::Group { primitives } => primitives - .iter() - .map(Self::bounds) - .fold(Rectangle::with_size(Size::ZERO), |a, b| { - Rectangle::union(&a, &b) - }), - Self::Translate { - translation, - content, - } => content.bounds() + *translation, - Self::Cache { content } => content.bounds(), - } - } -} - -/// A set of [`Vertex2D`] and indices representing a list of triangles. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Mesh2D<T> { - /// The vertices of the mesh - pub vertices: Vec<T>, - - /// The list of vertex indices that defines the triangles of the mesh. - /// - /// Therefore, this list should always have a length that is a multiple of 3. - pub indices: Vec<u32>, -} - -/// A two-dimensional vertex with a color. -#[derive(Copy, Clone, Debug, PartialEq, 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: color::Packed, -} - -/// A vertex which contains 2D position & packed gradient data. -#[derive(Copy, Clone, Debug, PartialEq)] -#[repr(C)] -pub struct GradientVertex2D { - /// The vertex position in 2D space. - pub position: [f32; 2], - - /// The packed vertex data of the gradient. - pub gradient: gradient::Packed, -} - -#[allow(unsafe_code)] -unsafe impl Zeroable for GradientVertex2D {} - -#[allow(unsafe_code)] -unsafe impl Pod for GradientVertex2D {} - -impl From<()> for Primitive { - fn from(_: ()) -> Self { - Self::Group { primitives: vec![] } - } } diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index de905503..c0cec60a 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -18,7 +18,7 @@ use std::marker::PhantomData; #[derive(Debug)] pub struct Renderer<B: Backend, Theme> { backend: B, - primitives: Vec<Primitive>, + primitives: Vec<Primitive<B::Primitive>>, theme: PhantomData<Theme>, } @@ -38,7 +38,7 @@ impl<B: Backend, T> Renderer<B, T> { } /// Enqueues the given [`Primitive`] in the [`Renderer`] for drawing. - pub fn draw_primitive(&mut self, primitive: Primitive) { + pub fn draw_primitive(&mut self, primitive: Primitive<B::Primitive>) { self.primitives.push(primitive); } @@ -46,16 +46,46 @@ impl<B: Backend, T> Renderer<B, T> { /// of the [`Renderer`]. pub fn with_primitives<O>( &mut self, - f: impl FnOnce(&mut B, &[Primitive]) -> O, + f: impl FnOnce(&mut B, &[Primitive<B::Primitive>]) -> O, ) -> O { f(&mut self.backend, &self.primitives) } + + /// Starts recording a new layer. + pub fn start_layer(&mut self) -> Vec<Primitive<B::Primitive>> { + std::mem::take(&mut self.primitives) + } + + /// Ends the recording of a layer. + pub fn end_layer( + &mut self, + primitives: Vec<Primitive<B::Primitive>>, + bounds: Rectangle, + ) { + let layer = std::mem::replace(&mut self.primitives, primitives); + + self.primitives.push(Primitive::group(layer).clip(bounds)); + } + + /// Starts recording a translation. + pub fn start_translation(&mut self) -> Vec<Primitive<B::Primitive>> { + std::mem::take(&mut self.primitives) + } + + /// Ends the recording of a translation. + pub fn end_translation( + &mut self, + primitives: Vec<Primitive<B::Primitive>>, + translation: Vector, + ) { + let layer = std::mem::replace(&mut self.primitives, primitives); + + self.primitives + .push(Primitive::group(layer).translate(translation)); + } } -impl<B, T> iced_core::Renderer for Renderer<B, T> -where - B: Backend, -{ +impl<B: Backend, T> iced_core::Renderer for Renderer<B, T> { type Theme = T; fn layout<Message>( @@ -63,21 +93,17 @@ where element: &Element<'_, Message, Self>, limits: &layout::Limits, ) -> layout::Node { - let layout = element.as_widget().layout(self, limits); - self.backend.trim_measurements(); - layout + element.as_widget().layout(self, limits) } fn with_layer(&mut self, bounds: Rectangle, f: impl FnOnce(&mut Self)) { - let current = std::mem::take(&mut self.primitives); + let current = self.start_layer(); f(self); - let layer = std::mem::replace(&mut self.primitives, current); - - self.primitives.push(Primitive::group(layer).clip(bounds)); + self.end_layer(current, bounds); } fn with_translation( @@ -85,14 +111,11 @@ where translation: Vector, f: impl FnOnce(&mut Self), ) { - let current = std::mem::take(&mut self.primitives); + let current = self.start_translation(); f(self); - let layer = std::mem::replace(&mut self.primitives, current); - - self.primitives - .push(Primitive::group(layer).translate(translation)); + self.end_translation(current, translation); } fn fill_quad( @@ -140,7 +163,7 @@ where font: Font, bounds: Size, shaping: text::Shaping, - ) -> (f32, f32) { + ) -> Size { self.backend().measure( content, size, @@ -229,14 +252,3 @@ where }) } } - -#[cfg(feature = "geometry")] -impl<B, T> crate::geometry::Renderer for Renderer<B, T> -where - B: Backend, -{ - fn draw(&mut self, layers: Vec<crate::Geometry>) { - self.primitives - .extend(layers.into_iter().map(crate::Geometry::into)); - } -} diff --git a/renderer/Cargo.toml b/renderer/Cargo.toml index ddfb6445..fda2bc7b 100644 --- a/renderer/Cargo.toml +++ b/renderer/Cargo.toml @@ -14,6 +14,7 @@ web-colors = ["iced_wgpu?/web-colors"] [dependencies] raw-window-handle = "0.5" thiserror = "1" +log = "0.4" [dependencies.iced_graphics] version = "0.8" diff --git a/renderer/src/backend.rs b/renderer/src/backend.rs index c9d79851..3f229b52 100644 --- a/renderer/src/backend.rs +++ b/renderer/src/backend.rs @@ -21,12 +21,6 @@ macro_rules! delegate { }; } -impl iced_graphics::Backend for Backend { - fn trim_measurements(&mut self) { - delegate!(self, backend, backend.trim_measurements()); - } -} - impl backend::Text for Backend { const ICON_FONT: Font = Font::with_name("Iced-Icons"); const CHECKMARK_ICON: char = '\u{f00c}'; @@ -48,7 +42,7 @@ impl backend::Text for Backend { font: Font, bounds: Size, shaping: text::Shaping, - ) -> (f32, f32) { + ) -> Size { delegate!( self, backend, diff --git a/renderer/src/compositor.rs b/renderer/src/compositor.rs index a353b8e4..8b17a4b0 100644 --- a/renderer/src/compositor.rs +++ b/renderer/src/compositor.rs @@ -100,26 +100,28 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { background_color: Color, overlay: &[T], ) -> Result<(), SurfaceError> { - renderer.with_primitives(|backend, primitives| { - match (self, backend, surface) { - ( - Self::TinySkia(_compositor), - crate::Backend::TinySkia(backend), - Surface::TinySkia(surface), - ) => iced_tiny_skia::window::compositor::present( + match (self, renderer, surface) { + ( + Self::TinySkia(_compositor), + crate::Renderer::TinySkia(renderer), + Surface::TinySkia(surface), + ) => renderer.with_primitives(|backend, primitives| { + iced_tiny_skia::window::compositor::present( backend, surface, primitives, viewport, background_color, overlay, - ), - #[cfg(feature = "wgpu")] - ( - Self::Wgpu(compositor), - crate::Backend::Wgpu(backend), - Surface::Wgpu(surface), - ) => iced_wgpu::window::compositor::present( + ) + }), + #[cfg(feature = "wgpu")] + ( + Self::Wgpu(compositor), + crate::Renderer::Wgpu(renderer), + Surface::Wgpu(surface), + ) => renderer.with_primitives(|backend, primitives| { + iced_wgpu::window::compositor::present( compositor, backend, surface, @@ -127,14 +129,60 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { viewport, background_color, overlay, - ), - #[allow(unreachable_patterns)] - _ => panic!( - "The provided renderer or surface are not compatible \ + ) + }), + #[allow(unreachable_patterns)] + _ => panic!( + "The provided renderer or surface are not compatible \ with the compositor." - ), - } - }) + ), + } + } + + fn screenshot<T: AsRef<str>>( + &mut self, + renderer: &mut Self::Renderer, + surface: &mut Self::Surface, + viewport: &Viewport, + background_color: Color, + overlay: &[T], + ) -> Vec<u8> { + match (self, renderer, surface) { + ( + Self::TinySkia(_compositor), + Renderer::TinySkia(renderer), + Surface::TinySkia(surface), + ) => renderer.with_primitives(|backend, primitives| { + iced_tiny_skia::window::compositor::screenshot( + surface, + backend, + primitives, + viewport, + background_color, + overlay, + ) + }), + #[cfg(feature = "wgpu")] + ( + Self::Wgpu(compositor), + Renderer::Wgpu(renderer), + Surface::Wgpu(_), + ) => renderer.with_primitives(|backend, primitives| { + iced_wgpu::window::compositor::screenshot( + compositor, + backend, + primitives, + viewport, + background_color, + overlay, + ) + }), + #[allow(unreachable_patterns)] + _ => panic!( + "The provided renderer or backend are not compatible \ + with the compositor." + ), + } } } @@ -185,7 +233,7 @@ impl Candidate { Ok(( Compositor::TinySkia(compositor), - Renderer::new(crate::Backend::TinySkia(backend)), + Renderer::TinySkia(iced_tiny_skia::Renderer::new(backend)), )) } #[cfg(feature = "wgpu")] @@ -202,7 +250,7 @@ impl Candidate { Ok(( Compositor::Wgpu(compositor), - Renderer::new(crate::Backend::Wgpu(backend)), + Renderer::Wgpu(iced_wgpu::Renderer::new(backend)), )) } #[cfg(not(feature = "wgpu"))] diff --git a/renderer/src/geometry.rs b/renderer/src/geometry.rs index 26e2fed0..04b5d9e6 100644 --- a/renderer/src/geometry.rs +++ b/renderer/src/geometry.rs @@ -3,8 +3,8 @@ mod cache; pub use cache::Cache; use crate::core::{Point, Rectangle, Size, Vector}; -use crate::graphics::geometry::{Fill, Geometry, Path, Stroke, Text}; -use crate::Backend; +use crate::graphics::geometry::{Fill, Path, Stroke, Text}; +use crate::Renderer; pub enum Frame { TinySkia(iced_tiny_skia::geometry::Frame), @@ -12,6 +12,12 @@ pub enum Frame { Wgpu(iced_wgpu::geometry::Frame), } +pub enum Geometry { + TinySkia(iced_tiny_skia::Primitive), + #[cfg(feature = "wgpu")] + Wgpu(iced_wgpu::Primitive), +} + macro_rules! delegate { ($frame:expr, $name:ident, $body:expr) => { match $frame { @@ -23,13 +29,13 @@ macro_rules! delegate { } impl Frame { - pub fn new<Theme>(renderer: &crate::Renderer<Theme>, size: Size) -> Self { - match renderer.backend() { - Backend::TinySkia(_) => { + pub fn new<Theme>(renderer: &Renderer<Theme>, size: Size) -> Self { + match renderer { + Renderer::TinySkia(_) => { Frame::TinySkia(iced_tiny_skia::geometry::Frame::new(size)) } #[cfg(feature = "wgpu")] - Backend::Wgpu(_) => { + Renderer::Wgpu(_) => { Frame::Wgpu(iced_wgpu::geometry::Frame::new(size)) } } @@ -169,6 +175,10 @@ impl Frame { } pub fn into_geometry(self) -> Geometry { - Geometry(delegate!(self, frame, frame.into_primitive())) + match self { + Self::TinySkia(frame) => Geometry::TinySkia(frame.into_primitive()), + #[cfg(feature = "wgpu")] + Self::Wgpu(frame) => Geometry::Wgpu(frame.into_primitive()), + } } } diff --git a/renderer/src/geometry/cache.rs b/renderer/src/geometry/cache.rs index 2a3534d0..d82e7f69 100644 --- a/renderer/src/geometry/cache.rs +++ b/renderer/src/geometry/cache.rs @@ -1,6 +1,5 @@ use crate::core::Size; use crate::geometry::{Frame, Geometry}; -use crate::graphics::Primitive; use crate::Renderer; use std::cell::RefCell; @@ -21,10 +20,17 @@ enum State { Empty, Filled { bounds: Size, - primitive: Arc<Primitive>, + primitive: Internal, }, } +#[derive(Debug, Clone)] +enum Internal { + TinySkia(Arc<iced_tiny_skia::Primitive>), + #[cfg(feature = "wgpu")] + Wgpu(Arc<iced_wgpu::Primitive>), +} + impl Cache { /// Creates a new empty [`Cache`]. pub fn new() -> Self { @@ -62,9 +68,21 @@ impl Cache { } = self.state.borrow().deref() { if *cached_bounds == bounds { - return Geometry(Primitive::Cache { - content: primitive.clone(), - }); + match primitive { + Internal::TinySkia(primitive) => { + return Geometry::TinySkia( + iced_tiny_skia::Primitive::Cache { + content: primitive.clone(), + }, + ); + } + #[cfg(feature = "wgpu")] + Internal::Wgpu(primitive) => { + return Geometry::Wgpu(iced_wgpu::Primitive::Cache { + content: primitive.clone(), + }); + } + } } } @@ -74,7 +92,15 @@ impl Cache { let primitive = { let geometry = frame.into_geometry(); - Arc::new(geometry.0) + match geometry { + Geometry::TinySkia(primitive) => { + Internal::TinySkia(Arc::new(primitive)) + } + #[cfg(feature = "wgpu")] + Geometry::Wgpu(primitive) => { + Internal::Wgpu(Arc::new(primitive)) + } + } }; *self.state.borrow_mut() = State::Filled { @@ -82,6 +108,18 @@ impl Cache { primitive: primitive.clone(), }; - Geometry(Primitive::Cache { content: primitive }) + match primitive { + Internal::TinySkia(primitive) => { + Geometry::TinySkia(iced_tiny_skia::Primitive::Cache { + content: primitive, + }) + } + #[cfg(feature = "wgpu")] + Internal::Wgpu(primitive) => { + Geometry::Wgpu(iced_wgpu::Primitive::Cache { + content: primitive, + }) + } + } } } diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index 22ec7bd1..7d1a02c2 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -3,17 +3,267 @@ pub mod compositor; #[cfg(feature = "geometry")] pub mod geometry; -mod backend; mod settings; pub use iced_graphics as graphics; pub use iced_graphics::core; -pub use backend::Backend; pub use compositor::Compositor; pub use settings::Settings; +#[cfg(feature = "geometry")] +pub use geometry::Geometry; + +use crate::core::renderer; +use crate::core::text::{self, Text}; +use crate::core::{Background, Font, Point, Rectangle, Size, Vector}; +use crate::graphics::Mesh; + +use std::borrow::Cow; + /// The default graphics renderer for [`iced`]. /// /// [`iced`]: https://github.com/iced-rs/iced -pub type Renderer<Theme> = iced_graphics::Renderer<Backend, Theme>; +pub enum Renderer<Theme> { + TinySkia(iced_tiny_skia::Renderer<Theme>), + #[cfg(feature = "wgpu")] + Wgpu(iced_wgpu::Renderer<Theme>), +} + +macro_rules! delegate { + ($renderer:expr, $name:ident, $body:expr) => { + match $renderer { + Self::TinySkia($name) => $body, + #[cfg(feature = "wgpu")] + Self::Wgpu($name) => $body, + } + }; +} + +impl<T> Renderer<T> { + pub fn draw_mesh(&mut self, mesh: Mesh) { + match self { + Self::TinySkia(_) => { + log::warn!("Unsupported mesh primitive: {:?}", mesh) + } + #[cfg(feature = "wgpu")] + Self::Wgpu(renderer) => { + renderer.draw_primitive(iced_wgpu::Primitive::Custom( + iced_wgpu::primitive::Custom::Mesh(mesh), + )); + } + } + } +} + +impl<T> core::Renderer for Renderer<T> { + type Theme = T; + + fn with_layer(&mut self, bounds: Rectangle, f: impl FnOnce(&mut Self)) { + match self { + Self::TinySkia(renderer) => { + let primitives = renderer.start_layer(); + + f(self); + + match self { + Self::TinySkia(renderer) => { + renderer.end_layer(primitives, bounds); + } + #[cfg(feature = "wgpu")] + _ => unreachable!(), + } + } + #[cfg(feature = "wgpu")] + Self::Wgpu(renderer) => { + let primitives = renderer.start_layer(); + + f(self); + + match self { + #[cfg(feature = "wgpu")] + Self::Wgpu(renderer) => { + renderer.end_layer(primitives, bounds); + } + _ => unreachable!(), + } + } + } + } + + fn with_translation( + &mut self, + translation: Vector, + f: impl FnOnce(&mut Self), + ) { + match self { + Self::TinySkia(renderer) => { + let primitives = renderer.start_translation(); + + f(self); + + match self { + Self::TinySkia(renderer) => { + renderer.end_translation(primitives, translation); + } + #[cfg(feature = "wgpu")] + _ => unreachable!(), + } + } + #[cfg(feature = "wgpu")] + Self::Wgpu(renderer) => { + let primitives = renderer.start_translation(); + + f(self); + + match self { + #[cfg(feature = "wgpu")] + Self::Wgpu(renderer) => { + renderer.end_translation(primitives, translation); + } + _ => unreachable!(), + } + } + } + } + + fn fill_quad( + &mut self, + quad: renderer::Quad, + background: impl Into<Background>, + ) { + delegate!(self, renderer, renderer.fill_quad(quad, background)); + } + + fn clear(&mut self) { + delegate!(self, renderer, renderer.clear()); + } +} + +impl<T> text::Renderer for Renderer<T> { + type Font = Font; + + const ICON_FONT: Font = iced_tiny_skia::Renderer::<T>::ICON_FONT; + const CHECKMARK_ICON: char = iced_tiny_skia::Renderer::<T>::CHECKMARK_ICON; + const ARROW_DOWN_ICON: char = + iced_tiny_skia::Renderer::<T>::ARROW_DOWN_ICON; + + fn default_font(&self) -> Self::Font { + delegate!(self, renderer, renderer.default_font()) + } + + fn default_size(&self) -> f32 { + delegate!(self, renderer, renderer.default_size()) + } + + fn measure( + &self, + content: &str, + size: f32, + line_height: text::LineHeight, + font: Font, + bounds: Size, + shaping: text::Shaping, + ) -> Size { + delegate!( + self, + renderer, + renderer.measure(content, size, line_height, font, bounds, shaping) + ) + } + + fn hit_test( + &self, + content: &str, + size: f32, + line_height: text::LineHeight, + font: Font, + bounds: Size, + shaping: text::Shaping, + point: Point, + nearest_only: bool, + ) -> Option<text::Hit> { + delegate!( + self, + renderer, + renderer.hit_test( + content, + size, + line_height, + font, + bounds, + shaping, + point, + nearest_only + ) + ) + } + + fn load_font(&mut self, bytes: Cow<'static, [u8]>) { + delegate!(self, renderer, renderer.load_font(bytes)); + } + + fn fill_text(&mut self, text: Text<'_, Self::Font>) { + delegate!(self, renderer, renderer.fill_text(text)); + } +} + +#[cfg(feature = "image")] +impl<T> crate::core::image::Renderer for Renderer<T> { + type Handle = crate::core::image::Handle; + + fn dimensions(&self, handle: &crate::core::image::Handle) -> Size<u32> { + delegate!(self, renderer, renderer.dimensions(handle)) + } + + fn draw(&mut self, handle: crate::core::image::Handle, bounds: Rectangle) { + delegate!(self, renderer, renderer.draw(handle, bounds)); + } +} + +#[cfg(feature = "svg")] +impl<T> crate::core::svg::Renderer for Renderer<T> { + fn dimensions(&self, handle: &crate::core::svg::Handle) -> Size<u32> { + delegate!(self, renderer, renderer.dimensions(handle)) + } + + fn draw( + &mut self, + handle: crate::core::svg::Handle, + color: Option<crate::core::Color>, + bounds: Rectangle, + ) { + delegate!(self, renderer, renderer.draw(handle, color, bounds)) + } +} + +#[cfg(feature = "geometry")] +impl<T> crate::graphics::geometry::Renderer for Renderer<T> { + type Geometry = crate::Geometry; + + fn draw(&mut self, layers: Vec<Self::Geometry>) { + match self { + Self::TinySkia(renderer) => { + for layer in layers { + match layer { + crate::Geometry::TinySkia(primitive) => { + renderer.draw_primitive(primitive); + } + _ => unreachable!(), + } + } + } + #[cfg(feature = "wgpu")] + Self::Wgpu(renderer) => { + for layer in layers { + match layer { + crate::Geometry::Wgpu(primitive) => { + renderer.draw_primitive(primitive); + } + _ => unreachable!(), + } + } + } + } + } +} diff --git a/runtime/src/program/state.rs b/runtime/src/program/state.rs index d83e3f54..35df6078 100644 --- a/runtime/src/program/state.rs +++ b/runtime/src/program/state.rs @@ -1,6 +1,7 @@ use crate::core::event::{self, Event}; use crate::core::mouse; use crate::core::renderer; +use crate::core::widget::operation::{self, Operation}; use crate::core::{Clipboard, Size}; use crate::user_interface::{self, UserInterface}; use crate::{Command, Debug, Program}; @@ -173,6 +174,43 @@ where (uncaptured_events, command) } + + /// Applies [`widget::Operation`]s to the [`State`] + pub fn operate( + &mut self, + renderer: &mut P::Renderer, + operations: impl Iterator<Item = Box<dyn Operation<P::Message>>>, + bounds: Size, + debug: &mut Debug, + ) { + let mut user_interface = build_user_interface( + &mut self.program, + self.cache.take().unwrap(), + renderer, + bounds, + debug, + ); + + for operation in operations { + let mut current_operation = Some(operation); + + while let Some(mut operation) = current_operation.take() { + user_interface.operate(renderer, operation.as_mut()); + + match operation.finish() { + operation::Outcome::None => {} + operation::Outcome::Some(message) => { + self.queued_messages.push(message) + } + operation::Outcome::Chain(next) => { + current_operation = Some(next); + } + }; + } + } + + self.cache = Some(user_interface.into_cache()); + } } fn build_user_interface<'a, P: Program>( diff --git a/runtime/src/window.rs b/runtime/src/window.rs index 9356581a..5219fbfd 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -1,7 +1,10 @@ //! Build window-based GUI applications. mod action; +pub mod screenshot; + pub use action::Action; +pub use screenshot::Screenshot; use crate::command::{self, Command}; use crate::core::time::Instant; @@ -123,3 +126,10 @@ pub fn fetch_id<Message>( pub fn change_icon<Message>(icon: Icon) -> Command<Message> { Command::single(command::Action::Window(Action::ChangeIcon(icon))) } + +/// Captures a [`Screenshot`] from the window. +pub fn screenshot<Message>( + f: impl FnOnce(Screenshot) -> Message + Send + 'static, +) -> Command<Message> { + Command::single(command::Action::Window(Action::Screenshot(Box::new(f)))) +} diff --git a/runtime/src/window/action.rs b/runtime/src/window/action.rs index d0137895..b6964e36 100644 --- a/runtime/src/window/action.rs +++ b/runtime/src/window/action.rs @@ -1,6 +1,7 @@ use crate::core::window::{Icon, Level, Mode, UserAttention}; use crate::core::Size; use crate::futures::MaybeSend; +use crate::window::Screenshot; use std::fmt; @@ -87,6 +88,8 @@ pub enum Action<T> { /// - **X11:** Has no universal guidelines for icon sizes, so you're at the whims of the WM. That /// said, it's usually in the same ballpark as on Windows. ChangeIcon(Icon), + /// Screenshot the viewport of the window. + Screenshot(Box<dyn FnOnce(Screenshot) -> T + 'static>), } impl<T> Action<T> { @@ -117,6 +120,11 @@ impl<T> Action<T> { Self::ChangeLevel(level) => Action::ChangeLevel(level), Self::FetchId(o) => Action::FetchId(Box::new(move |s| f(o(s)))), Self::ChangeIcon(icon) => Action::ChangeIcon(icon), + Self::Screenshot(tag) => { + Action::Screenshot(Box::new(move |screenshot| { + f(tag(screenshot)) + })) + } } } } @@ -152,6 +160,7 @@ impl<T> fmt::Debug for Action<T> { Self::ChangeIcon(_icon) => { write!(f, "Action::ChangeIcon(icon)") } + Self::Screenshot(_) => write!(f, "Action::Screenshot"), } } } diff --git a/runtime/src/window/screenshot.rs b/runtime/src/window/screenshot.rs new file mode 100644 index 00000000..c84286b6 --- /dev/null +++ b/runtime/src/window/screenshot.rs @@ -0,0 +1,92 @@ +//! Take screenshots of a window. +use crate::core::{Rectangle, Size}; + +use std::fmt::{Debug, Formatter}; +use std::sync::Arc; + +/// Data of a screenshot, captured with `window::screenshot()`. +/// +/// The `bytes` of this screenshot will always be ordered as `RGBA` in the sRGB color space. +#[derive(Clone)] +pub struct Screenshot { + /// The bytes of the [`Screenshot`]. + pub bytes: Arc<Vec<u8>>, + /// The size of the [`Screenshot`]. + pub size: Size<u32>, +} + +impl Debug for Screenshot { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Screenshot: {{ \n bytes: {}\n size: {:?} }}", + self.bytes.len(), + self.size + ) + } +} + +impl Screenshot { + /// Creates a new [`Screenshot`]. + pub fn new(bytes: Vec<u8>, size: Size<u32>) -> Self { + Self { + bytes: Arc::new(bytes), + size, + } + } + + /// Crops a [`Screenshot`] to the provided `region`. This will always be relative to the + /// top-left corner of the [`Screenshot`]. + pub fn crop(&self, region: Rectangle<u32>) -> Result<Self, CropError> { + if region.width == 0 || region.height == 0 { + return Err(CropError::Zero); + } + + if region.x + region.width > self.size.width + || region.y + region.height > self.size.height + { + return Err(CropError::OutOfBounds); + } + + // Image is always RGBA8 = 4 bytes per pixel + const PIXEL_SIZE: usize = 4; + + let bytes_per_row = self.size.width as usize * PIXEL_SIZE; + let row_range = region.y as usize..(region.y + region.height) as usize; + let column_range = region.x as usize * PIXEL_SIZE + ..(region.x + region.width) as usize * PIXEL_SIZE; + + let chopped = self.bytes.chunks(bytes_per_row).enumerate().fold( + vec![], + |mut acc, (row, bytes)| { + if row_range.contains(&row) { + acc.extend(&bytes[column_range.clone()]); + } + + acc + }, + ); + + Ok(Self { + bytes: Arc::new(chopped), + size: Size::new(region.width, region.height), + }) + } +} + +impl AsRef<[u8]> for Screenshot { + fn as_ref(&self) -> &[u8] { + &self.bytes + } +} + +#[derive(Debug, thiserror::Error)] +/// Errors that can occur when cropping a [`Screenshot`]. +pub enum CropError { + #[error("The cropped region is out of bounds.")] + /// The cropped region's size is out of bounds. + OutOfBounds, + #[error("The cropped region is not visible.")] + /// The cropped region's size is zero. + Zero, +} @@ -278,6 +278,7 @@ pub mod widget { mod native {} mod renderer {} mod style {} + mod runtime {} } pub use application::Application; diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml index 1571afb8..431f324b 100644 --- a/tiny_skia/Cargo.toml +++ b/tiny_skia/Cargo.toml @@ -20,11 +20,10 @@ log = "0.4" [dependencies.iced_graphics] version = "0.8" path = "../graphics" -features = ["tiny-skia"] [dependencies.cosmic-text] git = "https://github.com/hecrj/cosmic-text.git" -rev = "e8b10fd675832cb9c1cc9de30922beb4cf883876" +rev = "c3cd24dc972bb8fd55d016c81ac9fa637e0a4ada" [dependencies.twox-hash] version = "1.6" diff --git a/tiny_skia/fonts/Iced-Icons.ttf b/tiny_skia/fonts/Iced-Icons.ttf Binary files differnew file mode 100644 index 00000000..e3273141 --- /dev/null +++ b/tiny_skia/fonts/Iced-Icons.ttf diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 9d0fc527..e0134220 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -2,7 +2,8 @@ use crate::core::text; use crate::core::Gradient; use crate::core::{Background, Color, Font, Point, Rectangle, Size, Vector}; use crate::graphics::backend; -use crate::graphics::{Primitive, Viewport}; +use crate::graphics::{Damage, Viewport}; +use crate::primitive::{self, Primitive}; use crate::Settings; use std::borrow::Cow; @@ -174,7 +175,18 @@ impl Backend { ) .post_scale(scale_factor, scale_factor); - let path = rounded_rectangle(*bounds, *border_radius); + // Make sure the border radius is not larger than the bounds + let border_width = border_width + .min(bounds.width / 2.0) + .min(bounds.height / 2.0); + + let mut fill_border_radius = *border_radius; + for radius in &mut fill_border_radius { + *radius = (*radius) + .min(bounds.width / 2.0) + .min(bounds.height / 2.0); + } + let path = rounded_rectangle(*bounds, fill_border_radius); pixels.fill_path( &path, @@ -236,23 +248,120 @@ impl Backend { clip_mask, ); - if *border_width > 0.0 { - pixels.stroke_path( - &path, - &tiny_skia::Paint { - shader: tiny_skia::Shader::SolidColor(into_color( - *border_color, - )), - anti_alias: true, - ..tiny_skia::Paint::default() - }, - &tiny_skia::Stroke { - width: *border_width, - ..tiny_skia::Stroke::default() - }, - transform, - clip_mask, - ); + if border_width > 0.0 { + // Border path is offset by half the border width + let border_bounds = Rectangle { + x: bounds.x + border_width / 2.0, + y: bounds.y + border_width / 2.0, + width: bounds.width - border_width, + height: bounds.height - border_width, + }; + + // Make sure the border radius is correct + let mut border_radius = *border_radius; + let mut is_simple_border = true; + + for radius in &mut border_radius { + *radius = if *radius == 0.0 { + // Path should handle this fine + 0.0 + } else if *radius > border_width / 2.0 { + *radius - border_width / 2.0 + } else { + is_simple_border = false; + 0.0 + } + .min(border_bounds.width / 2.0) + .min(border_bounds.height / 2.0); + } + + // Stroking a path works well in this case + if is_simple_border { + let border_path = + rounded_rectangle(border_bounds, border_radius); + + pixels.stroke_path( + &border_path, + &tiny_skia::Paint { + shader: tiny_skia::Shader::SolidColor( + into_color(*border_color), + ), + anti_alias: true, + ..tiny_skia::Paint::default() + }, + &tiny_skia::Stroke { + width: border_width, + ..tiny_skia::Stroke::default() + }, + transform, + clip_mask, + ); + } else { + // Draw corners that have too small border radii as having no border radius, + // but mask them with the rounded rectangle with the correct border radius. + let mut temp_pixmap = tiny_skia::Pixmap::new( + bounds.width as u32, + bounds.height as u32, + ) + .unwrap(); + + let mut quad_mask = tiny_skia::Mask::new( + bounds.width as u32, + bounds.height as u32, + ) + .unwrap(); + + let zero_bounds = Rectangle { + x: 0.0, + y: 0.0, + width: bounds.width, + height: bounds.height, + }; + let path = + rounded_rectangle(zero_bounds, fill_border_radius); + + quad_mask.fill_path( + &path, + tiny_skia::FillRule::EvenOdd, + true, + transform, + ); + let path_bounds = Rectangle { + x: border_width / 2.0, + y: border_width / 2.0, + width: bounds.width - border_width, + height: bounds.height - border_width, + }; + + let border_radius_path = + rounded_rectangle(path_bounds, border_radius); + + temp_pixmap.stroke_path( + &border_radius_path, + &tiny_skia::Paint { + shader: tiny_skia::Shader::SolidColor( + into_color(*border_color), + ), + anti_alias: true, + ..tiny_skia::Paint::default() + }, + &tiny_skia::Stroke { + width: border_width, + ..tiny_skia::Stroke::default() + }, + transform, + Some(&quad_mask), + ); + + pixels.draw_pixmap( + bounds.x as i32, + bounds.y as i32, + temp_pixmap.as_ref(), + &tiny_skia::PixmapPaint::default(), + transform, + clip_mask, + ); + } } } Primitive::Text { @@ -311,6 +420,13 @@ impl Backend { self.raster_pipeline .draw(handle, *bounds, pixels, transform, clip_mask); } + #[cfg(not(feature = "image"))] + Primitive::Image { .. } => { + log::warn!( + "Unsupported primitive in `iced_tiny_skia`: {:?}", + primitive + ); + } #[cfg(feature = "svg")] Primitive::Svg { handle, @@ -334,12 +450,19 @@ impl Backend { clip_mask, ); } - Primitive::Fill { + #[cfg(not(feature = "svg"))] + Primitive::Svg { .. } => { + log::warn!( + "Unsupported primitive in `iced_tiny_skia`: {:?}", + primitive + ); + } + Primitive::Custom(primitive::Custom::Fill { path, paint, rule, transform, - } => { + }) => { let bounds = path.bounds(); let physical_bounds = (Rectangle { @@ -367,12 +490,12 @@ impl Backend { clip_mask, ); } - Primitive::Stroke { + Primitive::Custom(primitive::Custom::Stroke { path, paint, stroke, transform, - } => { + }) => { let bounds = path.bounds(); let physical_bounds = (Rectangle { @@ -472,21 +595,6 @@ impl Backend { translation, ); } - Primitive::SolidMesh { .. } | Primitive::GradientMesh { .. } => { - // Not supported! - // TODO: Draw a placeholder (?) - log::warn!( - "Unsupported primitive in `iced_tiny_skia`: {:?}", - primitive - ); - } - _ => { - // Not supported! - log::warn!( - "Unsupported primitive in `iced_tiny_skia`: {:?}", - primitive - ); - } } } } @@ -659,9 +767,7 @@ fn adjust_clip_mask(clip_mask: &mut tiny_skia::Mask, bounds: Rectangle) { } impl iced_graphics::Backend for Backend { - fn trim_measurements(&mut self) { - self.text_pipeline.trim_measurement_cache(); - } + type Primitive = primitive::Custom; } impl backend::Text for Backend { @@ -685,7 +791,7 @@ impl backend::Text for Backend { font: Font, bounds: Size, shaping: text::Shaping, - ) -> (f32, f32) { + ) -> Size { self.text_pipeline.measure( contents, size, diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs index ee347c73..9bd47556 100644 --- a/tiny_skia/src/geometry.rs +++ b/tiny_skia/src/geometry.rs @@ -3,7 +3,7 @@ use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::stroke::{self, Stroke}; use crate::graphics::geometry::{Path, Style, Text}; use crate::graphics::Gradient; -use crate::graphics::Primitive; +use crate::primitive::{self, Primitive}; pub struct Frame { size: Size, @@ -42,12 +42,13 @@ impl Frame { let Some(path) = convert_path(path) else { return }; let fill = fill.into(); - self.primitives.push(Primitive::Fill { - path, - paint: into_paint(fill.style), - rule: into_fill_rule(fill.rule), - transform: self.transform, - }); + self.primitives + .push(Primitive::Custom(primitive::Custom::Fill { + path, + paint: into_paint(fill.style), + rule: into_fill_rule(fill.rule), + transform: self.transform, + })); } pub fn fill_rectangle( @@ -59,15 +60,16 @@ impl Frame { let Some(path) = convert_path(&Path::rectangle(top_left, size)) else { return }; let fill = fill.into(); - self.primitives.push(Primitive::Fill { - path, - paint: tiny_skia::Paint { - anti_alias: false, - ..into_paint(fill.style) - }, - rule: into_fill_rule(fill.rule), - transform: self.transform, - }); + self.primitives + .push(Primitive::Custom(primitive::Custom::Fill { + path, + paint: tiny_skia::Paint { + anti_alias: false, + ..into_paint(fill.style) + }, + rule: into_fill_rule(fill.rule), + transform: self.transform, + })); } pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>) { @@ -76,12 +78,13 @@ impl Frame { let stroke = stroke.into(); let skia_stroke = into_stroke(&stroke); - self.primitives.push(Primitive::Stroke { - path, - paint: into_paint(stroke.style), - stroke: skia_stroke, - transform: self.transform, - }); + self.primitives + .push(Primitive::Custom(primitive::Custom::Stroke { + path, + paint: into_paint(stroke.style), + stroke: skia_stroke, + transform: self.transform, + })); } pub fn fill_text(&mut self, text: impl Into<Text>) { diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index 83baef1c..15de6ce2 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -1,6 +1,7 @@ pub mod window; mod backend; +mod primitive; mod settings; mod text; @@ -17,6 +18,7 @@ pub use iced_graphics as graphics; pub use iced_graphics::core; pub use backend::Backend; +pub use primitive::Primitive; pub use settings::Settings; /// A [`tiny-skia`] graphics renderer for [`iced`]. diff --git a/tiny_skia/src/primitive.rs b/tiny_skia/src/primitive.rs new file mode 100644 index 00000000..0ed24969 --- /dev/null +++ b/tiny_skia/src/primitive.rs @@ -0,0 +1,48 @@ +use crate::core::Rectangle; +use crate::graphics::Damage; + +pub type Primitive = crate::graphics::Primitive<Custom>; + +#[derive(Debug, Clone, PartialEq)] +pub enum Custom { + /// A path filled with some paint. + Fill { + /// The path to fill. + path: tiny_skia::Path, + /// The paint to use. + paint: tiny_skia::Paint<'static>, + /// The fill rule to follow. + rule: tiny_skia::FillRule, + /// The transform to apply to the path. + transform: tiny_skia::Transform, + }, + /// A path stroked with some paint. + Stroke { + /// The path to stroke. + path: tiny_skia::Path, + /// The paint to use. + paint: tiny_skia::Paint<'static>, + /// The stroke settings. + stroke: tiny_skia::Stroke, + /// The transform to apply to the path. + transform: tiny_skia::Transform, + }, +} + +impl Damage for Custom { + fn bounds(&self) -> Rectangle { + match self { + Self::Fill { path, .. } | Self::Stroke { path, .. } => { + let bounds = path.bounds(); + + Rectangle { + x: bounds.x(), + y: bounds.y(), + width: bounds.width(), + height: bounds.height(), + } + .expand(1.0) + } + } + } +} diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index a34c7317..8f494650 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -14,8 +14,7 @@ use std::sync::Arc; pub struct Pipeline { font_system: RefCell<cosmic_text::FontSystem>, glyph_cache: GlyphCache, - measurement_cache: RefCell<Cache>, - render_cache: Cache, + cache: RefCell<Cache>, } impl Pipeline { @@ -23,14 +22,12 @@ impl Pipeline { Pipeline { font_system: RefCell::new(cosmic_text::FontSystem::new_with_fonts( [cosmic_text::fontdb::Source::Binary(Arc::new( - include_bytes!("../../wgpu/fonts/Iced-Icons.ttf") - .as_slice(), + include_bytes!("../fonts/Iced-Icons.ttf").as_slice(), ))] .into_iter(), )), glyph_cache: GlyphCache::new(), - measurement_cache: RefCell::new(Cache::new()), - render_cache: Cache::new(), + cache: RefCell::new(Cache::new()), } } @@ -38,6 +35,8 @@ impl Pipeline { self.font_system.get_mut().db_mut().load_font_source( cosmic_text::fontdb::Source::Binary(Arc::new(bytes.into_owned())), ); + + self.cache = RefCell::new(Cache::new()); } pub fn draw( @@ -55,20 +54,11 @@ impl Pipeline { pixels: &mut tiny_skia::PixmapMut<'_>, clip_mask: Option<&tiny_skia::Mask>, ) { - let line_height = - f32::from(line_height.to_absolute(Pixels(size))) * scale_factor; - - let bounds = bounds * scale_factor; - let size = size * scale_factor; + let line_height = f32::from(line_height.to_absolute(Pixels(size))); let font_system = self.font_system.get_mut(); let key = Key { - bounds: { - let size = bounds.size(); - - // TODO: Reuse buffers from layouting - Size::new(size.width.ceil(), size.height.ceil()) - }, + bounds: bounds.size(), content, font, size, @@ -76,16 +66,12 @@ impl Pipeline { shaping, }; - let (_, buffer) = self.render_cache.allocate(font_system, key); + let (_, entry) = self.cache.get_mut().allocate(font_system, key); - let (total_lines, max_width) = buffer - .layout_runs() - .enumerate() - .fold((0, 0.0), |(_, max), (i, buffer)| { - (i + 1, buffer.line_w.max(max)) - }); + let max_width = entry.bounds.width * scale_factor; + let total_height = entry.bounds.height * scale_factor; - let total_height = total_lines as f32 * line_height; + let bounds = bounds * scale_factor; let x = match horizontal_alignment { alignment::Horizontal::Left => bounds.x, @@ -99,16 +85,14 @@ impl Pipeline { alignment::Vertical::Bottom => bounds.y - total_height, }; - // TODO: Subpixel glyph positioning - let x = x.round() as i32; - let y = y.round() as i32; - let mut swash = cosmic_text::SwashCache::new(); - for run in buffer.layout_runs() { + for run in entry.buffer.layout_runs() { for glyph in run.glyphs { + let physical_glyph = glyph.physical((x, y), scale_factor); + if let Some((buffer, placement)) = self.glyph_cache.allocate( - glyph.cache_key, + physical_glyph.cache_key, color, font_system, &mut swash, @@ -121,8 +105,9 @@ impl Pipeline { .expect("Create glyph pixel map"); pixels.draw_pixmap( - x + glyph.x_int + placement.left, - y - glyph.y_int - placement.top + run.line_y as i32, + physical_glyph.x + placement.left, + physical_glyph.y - placement.top + + (run.line_y * scale_factor).round() as i32, pixmap, &tiny_skia::PixmapPaint::default(), tiny_skia::Transform::identity(), @@ -134,7 +119,7 @@ impl Pipeline { } pub fn trim_cache(&mut self) { - self.render_cache.trim(); + self.cache.get_mut().trim(); self.glyph_cache.trim(); } @@ -146,12 +131,12 @@ impl Pipeline { font: Font, bounds: Size, shaping: Shaping, - ) -> (f32, f32) { - let mut measurement_cache = self.measurement_cache.borrow_mut(); + ) -> Size { + let mut measurement_cache = self.cache.borrow_mut(); let line_height = f32::from(line_height.to_absolute(Pixels(size))); - let (_, paragraph) = measurement_cache.allocate( + let (_, entry) = measurement_cache.allocate( &mut self.font_system.borrow_mut(), Key { content, @@ -163,14 +148,7 @@ impl Pipeline { }, ); - let (total_lines, max_width) = paragraph - .layout_runs() - .enumerate() - .fold((0, 0.0), |(_, max), (i, buffer)| { - (i + 1, buffer.line_w.max(max)) - }); - - (max_width, line_height * total_lines as f32) + entry.bounds } pub fn hit_test( @@ -184,11 +162,11 @@ impl Pipeline { point: Point, _nearest_only: bool, ) -> Option<Hit> { - let mut measurement_cache = self.measurement_cache.borrow_mut(); + let mut measurement_cache = self.cache.borrow_mut(); let line_height = f32::from(line_height.to_absolute(Pixels(size))); - let (_, paragraph) = measurement_cache.allocate( + let (_, entry) = measurement_cache.allocate( &mut self.font_system.borrow_mut(), Key { content, @@ -200,14 +178,20 @@ impl Pipeline { }, ); - let cursor = paragraph.hit(point.x, point.y)?; + let cursor = entry.buffer.hit(point.x, point.y)?; Some(Hit::CharOffset(cursor.index)) } +} - pub fn trim_measurement_cache(&mut self) { - self.measurement_cache.borrow_mut().trim(); - } +fn measure(buffer: &cosmic_text::Buffer) -> Size { + let (width, total_lines) = buffer + .layout_runs() + .fold((0.0, 0usize), |(width, total_lines), run| { + (run.line_w.max(width), total_lines + 1) + }); + + Size::new(width, total_lines as f32 * buffer.metrics().line_height) } fn to_family(family: font::Family) -> cosmic_text::Family<'static> { @@ -366,12 +350,18 @@ impl GlyphCache { } struct Cache { - entries: FxHashMap<KeyHash, cosmic_text::Buffer>, + entries: FxHashMap<KeyHash, Entry>, + measurements: FxHashMap<KeyHash, KeyHash>, recently_used: FxHashSet<KeyHash>, hasher: HashBuilder, trim_count: usize, } +struct Entry { + buffer: cosmic_text::Buffer, + bounds: Size, +} + #[cfg(not(target_arch = "wasm32"))] type HashBuilder = twox_hash::RandomXxHashBuilder64; @@ -384,6 +374,7 @@ impl Cache { fn new() -> Self { Self { entries: FxHashMap::default(), + measurements: FxHashMap::default(), recently_used: FxHashSet::default(), hasher: HashBuilder::default(), trim_count: 0, @@ -394,20 +385,14 @@ impl Cache { &mut self, font_system: &mut cosmic_text::FontSystem, key: Key<'_>, - ) -> (KeyHash, &mut cosmic_text::Buffer) { - let hash = { - let mut hasher = self.hasher.build_hasher(); - - key.content.hash(&mut hasher); - key.size.to_bits().hash(&mut hasher); - key.line_height.to_bits().hash(&mut hasher); - key.font.hash(&mut hasher); - key.bounds.width.to_bits().hash(&mut hasher); - key.bounds.height.to_bits().hash(&mut hasher); - key.shaping.hash(&mut hasher); - - hasher.finish() - }; + ) -> (KeyHash, &mut Entry) { + let hash = key.hash(self.hasher.build_hasher()); + + if let Some(hash) = self.measurements.get(&hash) { + let _ = self.recently_used.insert(*hash); + + return (*hash, self.entries.get_mut(hash).unwrap()); + } if let hash_map::Entry::Vacant(entry) = self.entries.entry(hash) { let metrics = cosmic_text::Metrics::new(key.size, key.size * 1.2); @@ -428,7 +413,24 @@ impl Cache { to_shaping(key.shaping), ); - let _ = entry.insert(buffer); + let bounds = measure(&buffer); + + let _ = entry.insert(Entry { buffer, bounds }); + + for bounds in [ + bounds, + Size { + width: key.bounds.width, + ..bounds + }, + ] { + if key.bounds != bounds { + let _ = self.measurements.insert( + Key { bounds, ..key }.hash(self.hasher.build_hasher()), + hash, + ); + } + } } let _ = self.recently_used.insert(hash); @@ -440,6 +442,8 @@ impl Cache { if self.trim_count > Self::TRIM_INTERVAL { self.entries .retain(|key, _| self.recently_used.contains(key)); + self.measurements + .retain(|_, value| self.recently_used.contains(value)); self.recently_used.clear(); @@ -460,4 +464,18 @@ struct Key<'a> { shaping: Shaping, } +impl Key<'_> { + fn hash<H: Hasher>(self, mut hasher: H) -> KeyHash { + self.content.hash(&mut hasher); + self.size.to_bits().hash(&mut hasher); + self.line_height.to_bits().hash(&mut hasher); + self.font.hash(&mut hasher); + self.bounds.width.to_bits().hash(&mut hasher); + self.bounds.height.to_bits().hash(&mut hasher); + self.shaping.hash(&mut hasher); + + hasher.finish() + } +} + type KeyHash = u64; diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 9999a188..775cf9e5 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -1,8 +1,8 @@ -use crate::core::{Color, Rectangle}; -use crate::graphics::compositor::{self, Information, SurfaceError}; +use crate::core::{Color, Rectangle, Size}; +use crate::graphics::compositor::{self, Information}; use crate::graphics::damage; -use crate::graphics::{Error, Primitive, Viewport}; -use crate::{Backend, Renderer, Settings}; +use crate::graphics::{Error, Viewport}; +use crate::{Backend, Primitive, Renderer, Settings}; use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; use std::marker::PhantomData; @@ -79,7 +79,7 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { viewport: &Viewport, background_color: Color, overlay: &[T], - ) -> Result<(), SurfaceError> { + ) -> Result<(), compositor::SurfaceError> { renderer.with_primitives(|backend, primitives| { present( backend, @@ -91,6 +91,26 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { ) }) } + + fn screenshot<T: AsRef<str>>( + &mut self, + renderer: &mut Self::Renderer, + surface: &mut Self::Surface, + viewport: &Viewport, + background_color: Color, + overlay: &[T], + ) -> Vec<u8> { + renderer.with_primitives(|backend, primitives| { + screenshot( + surface, + backend, + primitives, + viewport, + background_color, + overlay, + ) + }) + } } pub fn new<Theme>(settings: Settings) -> (Compositor<Theme>, Backend) { @@ -156,3 +176,53 @@ pub fn present<T: AsRef<str>>( Ok(()) } + +pub fn screenshot<T: AsRef<str>>( + surface: &mut Surface, + backend: &mut Backend, + primitives: &[Primitive], + viewport: &Viewport, + background_color: Color, + overlay: &[T], +) -> Vec<u8> { + let size = viewport.physical_size(); + + let mut offscreen_buffer: Vec<u32> = + vec![0; size.width as usize * size.height as usize]; + + backend.draw( + &mut tiny_skia::PixmapMut::from_bytes( + bytemuck::cast_slice_mut(&mut offscreen_buffer), + size.width, + size.height, + ) + .expect("Create offscreen pixel map"), + &mut surface.clip_mask, + primitives, + viewport, + &[Rectangle::with_size(Size::new( + size.width as f32, + size.height as f32, + ))], + background_color, + overlay, + ); + + offscreen_buffer.iter().fold( + Vec::with_capacity(offscreen_buffer.len() * 4), + |mut acc, pixel| { + const A_MASK: u32 = 0xFF_00_00_00; + const R_MASK: u32 = 0x00_FF_00_00; + const G_MASK: u32 = 0x00_00_FF_00; + const B_MASK: u32 = 0x00_00_00_FF; + + let a = ((A_MASK & pixel) >> 24) as u8; + let r = ((R_MASK & pixel) >> 16) as u8; + let g = ((G_MASK & pixel) >> 8) as u8; + let b = (B_MASK & pixel) as u8; + + acc.extend([r, g, b, a]); + acc + }, + ) +} diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index f3a83acb..15db5b5d 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -45,7 +45,7 @@ path = "../graphics" [dependencies.glyphon] version = "0.2" git = "https://github.com/hecrj/glyphon.git" -rev = "8dbf36020e5759fa9144517b321372266160113e" +rev = "8324f20158a62f8520bad4ed09f6aa5552f8f2a6" [dependencies.glam] version = "0.24" diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index b524c615..4a0c54f0 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -2,7 +2,8 @@ use crate::core; use crate::core::{Color, Font, Point, Size}; use crate::graphics::backend; use crate::graphics::color; -use crate::graphics::{Primitive, Transformation, Viewport}; +use crate::graphics::{Transformation, Viewport}; +use crate::primitive::{self, Primitive}; use crate::quad; use crate::text; use crate::triangle; @@ -334,9 +335,11 @@ impl Backend { } } -impl iced_graphics::Backend for Backend { +impl crate::graphics::Backend for Backend { + type Primitive = primitive::Custom; + fn trim_measurements(&mut self) { - self.text_pipeline.trim_measurement_cache() + self.text_pipeline.trim_measurements(); } } @@ -361,7 +364,7 @@ impl backend::Text for Backend { font: Font, bounds: Size, shaping: core::text::Shaping, - ) -> (f32, f32) { + ) -> Size { self.text_pipeline.measure( contents, size, diff --git a/wgpu/src/color.rs b/wgpu/src/color.rs new file mode 100644 index 00000000..a1025601 --- /dev/null +++ b/wgpu/src/color.rs @@ -0,0 +1,165 @@ +use std::borrow::Cow; + +pub fn convert( + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + source: wgpu::Texture, + format: wgpu::TextureFormat, +) -> wgpu::Texture { + if source.format() == format { + return source; + } + + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("iced_wgpu.offscreen.sampler"), + ..Default::default() + }); + + //sampler in 0 + let sampler_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("iced_wgpu.offscreen.blit.sampler_layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler( + wgpu::SamplerBindingType::NonFiltering, + ), + count: None, + }], + }); + + let sampler_bind_group = + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("iced_wgpu.offscreen.sampler.bind_group"), + layout: &sampler_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Sampler(&sampler), + }], + }); + + let texture_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("iced_wgpu.offscreen.blit.texture_layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { + filterable: false, + }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }], + }); + + let pipeline_layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("iced_wgpu.offscreen.blit.pipeline_layout"), + bind_group_layouts: &[&sampler_layout, &texture_layout], + push_constant_ranges: &[], + }); + + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("iced_wgpu.offscreen.blit.shader"), + source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!( + "shader/blit.wgsl" + ))), + }); + + let pipeline = + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("iced_wgpu.offscreen.blit.pipeline"), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "vs_main", + buffers: &[], + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs_main", + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState { + color: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::SrcAlpha, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + alpha: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + }), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + front_face: wgpu::FrontFace::Cw, + ..Default::default() + }, + depth_stencil: None, + multisample: Default::default(), + multiview: None, + }); + + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("iced_wgpu.offscreen.conversion.source_texture"), + size: source.size(), + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::COPY_SRC, + view_formats: &[], + }); + + let view = &texture.create_view(&wgpu::TextureViewDescriptor::default()); + + let texture_bind_group = + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("iced_wgpu.offscreen.blit.texture_bind_group"), + layout: &texture_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView( + &source + .create_view(&wgpu::TextureViewDescriptor::default()), + ), + }], + }); + + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("iced_wgpu.offscreen.blit.render_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: true, + }, + })], + depth_stencil_attachment: None, + }); + + pass.set_pipeline(&pipeline); + pass.set_bind_group(0, &sampler_bind_group, &[]); + pass.set_bind_group(1, &texture_bind_group, &[]); + pass.draw(0..6, 0..1); + + texture +} + +#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +#[repr(C)] +struct Vertex { + ndc: [f32; 2], + uv: [f32; 2], +} diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index f81b5b2f..e421e0b0 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -5,10 +5,10 @@ use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::{ LineCap, LineDash, LineJoin, Path, Stroke, Style, Text, }; -use crate::graphics::primitive::{self, Primitive}; -use crate::graphics::Gradient; +use crate::graphics::gradient::{self, Gradient}; +use crate::graphics::mesh::{self, Mesh}; +use crate::primitive::{self, Primitive}; -use iced_graphics::gradient; use lyon::geom::euclid; use lyon::tessellation; use std::borrow::Cow; @@ -25,8 +25,8 @@ pub struct Frame { } enum Buffer { - Solid(tessellation::VertexBuffers<primitive::ColoredVertex2D, u32>), - Gradient(tessellation::VertexBuffers<primitive::GradientVertex2D, u32>), + Solid(tessellation::VertexBuffers<mesh::SolidVertex2D, u32>), + Gradient(tessellation::VertexBuffers<mesh::GradientVertex2D, u32>), } struct BufferStack { @@ -464,24 +464,28 @@ impl Frame { match buffer { Buffer::Solid(buffer) => { if !buffer.indices.is_empty() { - self.primitives.push(Primitive::SolidMesh { - buffers: primitive::Mesh2D { - vertices: buffer.vertices, - indices: buffer.indices, - }, - size: self.size, - }) + self.primitives.push(Primitive::Custom( + primitive::Custom::Mesh(Mesh::Solid { + buffers: mesh::Indexed { + vertices: buffer.vertices, + indices: buffer.indices, + }, + size: self.size, + }), + )) } } Buffer::Gradient(buffer) => { if !buffer.indices.is_empty() { - self.primitives.push(Primitive::GradientMesh { - buffers: primitive::Mesh2D { - vertices: buffer.vertices, - indices: buffer.indices, - }, - size: self.size, - }) + self.primitives.push(Primitive::Custom( + primitive::Custom::Mesh(Mesh::Gradient { + buffers: mesh::Indexed { + vertices: buffer.vertices, + indices: buffer.indices, + }, + size: self.size, + }), + )) } } } @@ -495,32 +499,32 @@ struct GradientVertex2DBuilder { gradient: gradient::Packed, } -impl tessellation::FillVertexConstructor<primitive::GradientVertex2D> +impl tessellation::FillVertexConstructor<mesh::GradientVertex2D> for GradientVertex2DBuilder { fn new_vertex( &mut self, vertex: tessellation::FillVertex<'_>, - ) -> primitive::GradientVertex2D { + ) -> mesh::GradientVertex2D { let position = vertex.position(); - primitive::GradientVertex2D { + mesh::GradientVertex2D { position: [position.x, position.y], gradient: self.gradient, } } } -impl tessellation::StrokeVertexConstructor<primitive::GradientVertex2D> +impl tessellation::StrokeVertexConstructor<mesh::GradientVertex2D> for GradientVertex2DBuilder { fn new_vertex( &mut self, vertex: tessellation::StrokeVertex<'_, '_>, - ) -> primitive::GradientVertex2D { + ) -> mesh::GradientVertex2D { let position = vertex.position(); - primitive::GradientVertex2D { + mesh::GradientVertex2D { position: [position.x, position.y], gradient: self.gradient, } @@ -529,32 +533,32 @@ impl tessellation::StrokeVertexConstructor<primitive::GradientVertex2D> struct TriangleVertex2DBuilder(color::Packed); -impl tessellation::FillVertexConstructor<primitive::ColoredVertex2D> +impl tessellation::FillVertexConstructor<mesh::SolidVertex2D> for TriangleVertex2DBuilder { fn new_vertex( &mut self, vertex: tessellation::FillVertex<'_>, - ) -> primitive::ColoredVertex2D { + ) -> mesh::SolidVertex2D { let position = vertex.position(); - primitive::ColoredVertex2D { + mesh::SolidVertex2D { position: [position.x, position.y], color: self.0, } } } -impl tessellation::StrokeVertexConstructor<primitive::ColoredVertex2D> +impl tessellation::StrokeVertexConstructor<mesh::SolidVertex2D> for TriangleVertex2DBuilder { fn new_vertex( &mut self, vertex: tessellation::StrokeVertex<'_, '_>, - ) -> primitive::ColoredVertex2D { + ) -> mesh::SolidVertex2D { let position = vertex.position(); - primitive::ColoredVertex2D { + mesh::SolidVertex2D { position: [position.x, position.y], color: self.0, } diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index 71570e3d..b8f32db1 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -11,8 +11,10 @@ pub use text::Text; use crate::core; use crate::core::alignment; use crate::core::{Color, Font, Point, Rectangle, Size, Vector}; +use crate::graphics; use crate::graphics::color; -use crate::graphics::{Primitive, Viewport}; +use crate::graphics::Viewport; +use crate::primitive::{self, Primitive}; use crate::quad::{self, Quad}; /// A group of primitives that should be clipped together. @@ -179,40 +181,6 @@ impl<'a> Layer<'a> { bounds: *bounds + translation, }); } - Primitive::SolidMesh { buffers, size } => { - let layer = &mut layers[current_layer]; - - let bounds = Rectangle::new( - Point::new(translation.x, translation.y), - *size, - ); - - // Only draw visible content - if let Some(clip_bounds) = layer.bounds.intersection(&bounds) { - layer.meshes.push(Mesh::Solid { - origin: Point::new(translation.x, translation.y), - buffers, - clip_bounds, - }); - } - } - Primitive::GradientMesh { buffers, size } => { - 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, - }); - } - } Primitive::Group { primitives } => { // TODO: Inspect a bit and regroup (?) for primitive in primitives { @@ -262,13 +230,54 @@ impl<'a> Layer<'a> { current_layer, ); } - _ => { - // Not supported! - log::warn!( - "Unsupported primitive in `iced_wgpu`: {:?}", - primitive - ); - } + Primitive::Custom(custom) => match custom { + primitive::Custom::Mesh(mesh) => match mesh { + graphics::Mesh::Solid { buffers, size } => { + let layer = &mut layers[current_layer]; + + let bounds = Rectangle::new( + Point::new(translation.x, translation.y), + *size, + ); + + // Only draw visible content + if let Some(clip_bounds) = + layer.bounds.intersection(&bounds) + { + layer.meshes.push(Mesh::Solid { + origin: Point::new( + translation.x, + translation.y, + ), + buffers, + clip_bounds, + }); + } + } + graphics::Mesh::Gradient { buffers, size } => { + let layer = &mut layers[current_layer]; + + let bounds = Rectangle::new( + Point::new(translation.x, translation.y), + *size, + ); + + // Only draw visible content + if let Some(clip_bounds) = + layer.bounds.intersection(&bounds) + { + layer.meshes.push(Mesh::Gradient { + origin: Point::new( + translation.x, + translation.y, + ), + buffers, + clip_bounds, + }); + } + } + }, + }, } } } diff --git a/wgpu/src/layer/mesh.rs b/wgpu/src/layer/mesh.rs index b7dd9a0b..7c6206cd 100644 --- a/wgpu/src/layer/mesh.rs +++ b/wgpu/src/layer/mesh.rs @@ -1,6 +1,6 @@ //! A collection of triangle primitives. use crate::core::{Point, Rectangle}; -use crate::graphics::primitive; +use crate::graphics::mesh; /// A mesh of triangles. #[derive(Debug, Clone, Copy)] @@ -11,7 +11,7 @@ pub enum Mesh<'a> { origin: Point, /// The vertex and index buffers of the [`Mesh`]. - buffers: &'a primitive::Mesh2D<primitive::ColoredVertex2D>, + buffers: &'a mesh::Indexed<mesh::SolidVertex2D>, /// The clipping bounds of the [`Mesh`]. clip_bounds: Rectangle<f32>, @@ -22,7 +22,7 @@ pub enum Mesh<'a> { origin: Point, /// The vertex and index buffers of the [`Mesh`]. - buffers: &'a primitive::Mesh2D<primitive::GradientVertex2D>, + buffers: &'a mesh::Indexed<mesh::GradientVertex2D>, /// The clipping bounds of the [`Mesh`]. clip_bounds: Rectangle<f32>, diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 0a5726b5..deb223ef 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -38,6 +38,7 @@ #![allow(clippy::inherent_to_string, clippy::type_complexity)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] pub mod layer; +pub mod primitive; pub mod settings; pub mod window; @@ -46,6 +47,7 @@ pub mod geometry; mod backend; mod buffer; +mod color; mod quad; mod text; mod triangle; @@ -59,6 +61,7 @@ pub use wgpu; pub use backend::Backend; pub use layer::Layer; +pub use primitive::Primitive; pub use settings::Settings; #[cfg(any(feature = "image", feature = "svg"))] diff --git a/wgpu/src/primitive.rs b/wgpu/src/primitive.rs new file mode 100644 index 00000000..8dbf3008 --- /dev/null +++ b/wgpu/src/primitive.rs @@ -0,0 +1,21 @@ +//! Draw using different graphical primitives. +use crate::core::Rectangle; +use crate::graphics::{Damage, Mesh}; + +/// The graphical primitives supported by `iced_wgpu`. +pub type Primitive = crate::graphics::Primitive<Custom>; + +/// The custom primitives supported by `iced_wgpu`. +#[derive(Debug, Clone, PartialEq)] +pub enum Custom { + /// A mesh primitive. + Mesh(Mesh), +} + +impl Damage for Custom { + fn bounds(&self) -> Rectangle { + match self { + Self::Mesh(mesh) => mesh.bounds(), + } + } +} diff --git a/wgpu/src/quad/gradient.rs b/wgpu/src/quad/gradient.rs index 2b56d594..6db37252 100644 --- a/wgpu/src/quad/gradient.rs +++ b/wgpu/src/quad/gradient.rs @@ -96,36 +96,26 @@ impl Pipeline { as u64, step_mode: wgpu::VertexStepMode::Instance, attributes: &wgpu::vertex_attr_array!( - // Color 1 - 1 => Float32x4, - // Color 2 - 2 => Float32x4, - // Color 3 - 3 => Float32x4, - // Color 4 - 4 => Float32x4, - // Color 5 - 5 => Float32x4, - // Color 6 - 6 => Float32x4, - // Color 7 - 7 => Float32x4, - // Color 8 - 8 => Float32x4, - // Offsets 1-4 - 9 => Float32x4, - // Offsets 5-8 - 10 => Float32x4, + // Colors 1-2 + 1 => Uint32x4, + // Colors 3-4 + 2 => Uint32x4, + // Colors 5-6 + 3 => Uint32x4, + // Colors 7-8 + 4 => Uint32x4, + // Offsets 1-8 + 5 => Uint32x4, // Direction - 11 => Float32x4, + 6 => Float32x4, // Position & Scale - 12 => Float32x4, + 7 => Float32x4, // Border color - 13 => Float32x4, + 8 => Float32x4, // Border radius - 14 => Float32x4, + 9 => Float32x4, // Border width - 15 => Float32 + 10 => Float32 ), }, ], diff --git a/wgpu/src/shader/quad.wgsl b/wgpu/src/shader/quad.wgsl index 3232bdbe..fb402158 100644 --- a/wgpu/src/shader/quad.wgsl +++ b/wgpu/src/shader/quad.wgsl @@ -38,6 +38,13 @@ fn select_border_radius(radi: vec4<f32>, position: vec2<f32>, center: vec2<f32>) return rx; } +fn unpack_u32(color: vec2<u32>) -> vec4<f32> { + let rg: vec2<f32> = unpack2x16float(color.x); + let ba: vec2<f32> = unpack2x16float(color.y); + + return vec4<f32>(rg.y, rg.x, ba.y, ba.x); +} + struct SolidVertexInput { @location(0) v_pos: vec2<f32>, @location(1) color: vec4<f32>, @@ -140,40 +147,30 @@ fn solid_fs_main( struct GradientVertexInput { @location(0) v_pos: vec2<f32>, - @location(1) color_1: vec4<f32>, - @location(2) color_2: vec4<f32>, - @location(3) color_3: vec4<f32>, - @location(4) color_4: vec4<f32>, - @location(5) color_5: vec4<f32>, - @location(6) color_6: vec4<f32>, - @location(7) color_7: vec4<f32>, - @location(8) color_8: vec4<f32>, - @location(9) offsets_1: vec4<f32>, - @location(10) offsets_2: vec4<f32>, - @location(11) direction: vec4<f32>, - @location(12) position_and_scale: vec4<f32>, - @location(13) border_color: vec4<f32>, - @location(14) border_radius: vec4<f32>, - @location(15) border_width: f32 + @location(1) colors_1: vec4<u32>, + @location(2) colors_2: vec4<u32>, + @location(3) colors_3: vec4<u32>, + @location(4) colors_4: vec4<u32>, + @location(5) offsets: vec4<u32>, + @location(6) direction: vec4<f32>, + @location(7) position_and_scale: vec4<f32>, + @location(8) border_color: vec4<f32>, + @location(9) border_radius: vec4<f32>, + @location(10) border_width: f32, } struct GradientVertexOutput { @builtin(position) position: vec4<f32>, - @location(1) color_1: vec4<f32>, - @location(2) color_2: vec4<f32>, - @location(3) color_3: vec4<f32>, - @location(4) color_4: vec4<f32>, - @location(5) color_5: vec4<f32>, - @location(6) color_6: vec4<f32>, - @location(7) color_7: vec4<f32>, - @location(8) color_8: vec4<f32>, - @location(9) offsets_1: vec4<f32>, - @location(10) offsets_2: vec4<f32>, - @location(11) direction: vec4<f32>, - @location(12) position_and_scale: vec4<f32>, - @location(13) border_color: vec4<f32>, - @location(14) border_radius: vec4<f32>, - @location(15) border_width: f32 + @location(1) colors_1: vec4<u32>, + @location(2) colors_2: vec4<u32>, + @location(3) colors_3: vec4<u32>, + @location(4) colors_4: vec4<u32>, + @location(5) offsets: vec4<u32>, + @location(6) direction: vec4<f32>, + @location(7) position_and_scale: vec4<f32>, + @location(8) border_color: vec4<f32>, + @location(9) border_radius: vec4<f32>, + @location(10) border_width: f32, } @vertex @@ -199,16 +196,11 @@ fn gradient_vs_main(input: GradientVertexInput) -> GradientVertexOutput { ); out.position = globals.transform * transform * vec4<f32>(input.v_pos, 0.0, 1.0); - out.color_1 = input.color_1; - out.color_2 = input.color_2; - out.color_3 = input.color_3; - out.color_4 = input.color_4; - out.color_5 = input.color_5; - out.color_6 = input.color_6; - out.color_7 = input.color_7; - out.color_8 = input.color_8; - out.offsets_1 = input.offsets_1; - out.offsets_2 = input.offsets_2; + out.colors_1 = input.colors_1; + out.colors_2 = input.colors_2; + out.colors_3 = input.colors_3; + out.colors_4 = input.colors_4; + out.offsets = input.offsets; out.direction = input.direction * globals.scale; out.position_and_scale = vec4<f32>(pos, scale); out.border_color = input.border_color; @@ -274,25 +266,28 @@ fn gradient( @fragment fn gradient_fs_main(input: GradientVertexOutput) -> @location(0) vec4<f32> { let colors = array<vec4<f32>, 8>( - input.color_1, - input.color_2, - input.color_3, - input.color_4, - input.color_5, - input.color_6, - input.color_7, - input.color_8, + unpack_u32(input.colors_1.xy), + unpack_u32(input.colors_1.zw), + unpack_u32(input.colors_2.xy), + unpack_u32(input.colors_2.zw), + unpack_u32(input.colors_3.xy), + unpack_u32(input.colors_3.zw), + unpack_u32(input.colors_4.xy), + unpack_u32(input.colors_4.zw), ); + let offsets_1: vec4<f32> = unpack_u32(input.offsets.xy); + let offsets_2: vec4<f32> = unpack_u32(input.offsets.zw); + var offsets = array<f32, 8>( - input.offsets_1.x, - input.offsets_1.y, - input.offsets_1.z, - input.offsets_1.w, - input.offsets_2.x, - input.offsets_2.y, - input.offsets_2.z, - input.offsets_2.w, + offsets_1.x, + offsets_1.y, + offsets_1.z, + offsets_1.w, + offsets_2.x, + offsets_2.y, + offsets_2.z, + offsets_2.w, ); //TODO could just pass this in to the shader but is probably more performant to just check it here diff --git a/wgpu/src/shader/triangle.wgsl b/wgpu/src/shader/triangle.wgsl index 625fa46e..9f512d14 100644 --- a/wgpu/src/shader/triangle.wgsl +++ b/wgpu/src/shader/triangle.wgsl @@ -4,6 +4,13 @@ struct Globals { @group(0) @binding(0) var<uniform> globals: Globals; +fn unpack_u32(color: vec2<u32>) -> vec4<f32> { + let rg: vec2<f32> = unpack2x16float(color.x); + let ba: vec2<f32> = unpack2x16float(color.y); + + return vec4<f32>(rg.y, rg.x, ba.y, ba.x); +} + struct SolidVertexInput { @location(0) position: vec2<f32>, @location(1) color: vec4<f32>, @@ -29,52 +36,39 @@ fn solid_fs_main(input: SolidVertexOutput) -> @location(0) vec4<f32> { return input.color; } +struct GradientVertexInput { + @location(0) v_pos: vec2<f32>, + @location(1) colors_1: vec4<u32>, + @location(2) colors_2: vec4<u32>, + @location(3) colors_3: vec4<u32>, + @location(4) colors_4: vec4<u32>, + @location(5) offsets: vec4<u32>, + @location(6) direction: vec4<f32>, +} + struct GradientVertexOutput { @builtin(position) position: vec4<f32>, @location(0) raw_position: vec2<f32>, - @location(1) color_1: vec4<f32>, - @location(2) color_2: vec4<f32>, - @location(3) color_3: vec4<f32>, - @location(4) color_4: vec4<f32>, - @location(5) color_5: vec4<f32>, - @location(6) color_6: vec4<f32>, - @location(7) color_7: vec4<f32>, - @location(8) color_8: vec4<f32>, - @location(9) offsets_1: vec4<f32>, - @location(10) offsets_2: vec4<f32>, - @location(11) direction: vec4<f32>, + @location(1) colors_1: vec4<u32>, + @location(2) colors_2: vec4<u32>, + @location(3) colors_3: vec4<u32>, + @location(4) colors_4: vec4<u32>, + @location(5) offsets: vec4<u32>, + @location(6) direction: vec4<f32>, } @vertex -fn gradient_vs_main( - @location(0) input: vec2<f32>, - @location(1) color_1: vec4<f32>, - @location(2) color_2: vec4<f32>, - @location(3) color_3: vec4<f32>, - @location(4) color_4: vec4<f32>, - @location(5) color_5: vec4<f32>, - @location(6) color_6: vec4<f32>, - @location(7) color_7: vec4<f32>, - @location(8) color_8: vec4<f32>, - @location(9) offsets_1: vec4<f32>, - @location(10) offsets_2: vec4<f32>, - @location(11) direction: vec4<f32>, -) -> GradientVertexOutput { +fn gradient_vs_main(input: GradientVertexInput) -> GradientVertexOutput { var output: GradientVertexOutput; - output.position = globals.transform * vec4<f32>(input.xy, 0.0, 1.0); - output.raw_position = input; - output.color_1 = color_1; - output.color_2 = color_2; - output.color_3 = color_3; - output.color_4 = color_4; - output.color_5 = color_5; - output.color_6 = color_6; - output.color_7 = color_7; - output.color_8 = color_8; - output.offsets_1 = offsets_1; - output.offsets_2 = offsets_2; - output.direction = direction; + output.position = globals.transform * vec4<f32>(input.v_pos, 0.0, 1.0); + output.raw_position = input.v_pos; + output.colors_1 = input.colors_1; + output.colors_2 = input.colors_2; + output.colors_3 = input.colors_3; + output.colors_4 = input.colors_4; + output.offsets = input.offsets; + output.direction = input.direction; return output; } @@ -135,25 +129,28 @@ fn gradient( @fragment fn gradient_fs_main(input: GradientVertexOutput) -> @location(0) vec4<f32> { let colors = array<vec4<f32>, 8>( - input.color_1, - input.color_2, - input.color_3, - input.color_4, - input.color_5, - input.color_6, - input.color_7, - input.color_8, + unpack_u32(input.colors_1.xy), + unpack_u32(input.colors_1.zw), + unpack_u32(input.colors_2.xy), + unpack_u32(input.colors_2.zw), + unpack_u32(input.colors_3.xy), + unpack_u32(input.colors_3.zw), + unpack_u32(input.colors_4.xy), + unpack_u32(input.colors_4.zw), ); + let offsets_1: vec4<f32> = unpack_u32(input.offsets.xy); + let offsets_2: vec4<f32> = unpack_u32(input.offsets.zw); + var offsets = array<f32, 8>( - input.offsets_1.x, - input.offsets_1.y, - input.offsets_1.z, - input.offsets_1.w, - input.offsets_2.x, - input.offsets_2.y, - input.offsets_2.z, - input.offsets_2.w, + offsets_1.x, + offsets_1.y, + offsets_1.z, + offsets_1.w, + offsets_2.x, + offsets_2.y, + offsets_2.z, + offsets_2.w, ); var last_index = 7; diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 6a552270..65d3b818 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -18,8 +18,7 @@ pub struct Pipeline { renderers: Vec<glyphon::TextRenderer>, atlas: glyphon::TextAtlas, prepare_layer: usize, - measurement_cache: RefCell<Cache>, - render_cache: Cache, + cache: RefCell<Cache>, } impl Pipeline { @@ -47,8 +46,7 @@ impl Pipeline { }, ), prepare_layer: 0, - measurement_cache: RefCell::new(Cache::new()), - render_cache: Cache::new(), + cache: RefCell::new(Cache::new()), } } @@ -56,6 +54,8 @@ impl Pipeline { let _ = self.font_system.get_mut().db_mut().load_font_source( glyphon::fontdb::Source::Binary(Arc::new(bytes.into_owned())), ); + + self.cache = RefCell::new(Cache::new()); } pub fn prepare( @@ -78,28 +78,33 @@ impl Pipeline { let font_system = self.font_system.get_mut(); let renderer = &mut self.renderers[self.prepare_layer]; + let cache = self.cache.get_mut(); + + if self.prepare_layer == 0 { + cache.trim(Purpose::Drawing); + } let keys: Vec<_> = sections .iter() .map(|section| { - let (key, _) = self.render_cache.allocate( + let (key, _) = cache.allocate( font_system, Key { content: section.content, - size: section.size * scale_factor, + size: section.size, line_height: f32::from( section .line_height .to_absolute(Pixels(section.size)), - ) * scale_factor, + ), font: section.font, bounds: Size { - width: (section.bounds.width * scale_factor).ceil(), - height: (section.bounds.height * scale_factor) - .ceil(), + width: section.bounds.width, + height: section.bounds.height, }, shaping: section.shaping, }, + Purpose::Drawing, ); key @@ -113,14 +118,14 @@ impl Pipeline { .iter() .zip(keys.iter()) .filter_map(|(section, key)| { - let buffer = - self.render_cache.get(key).expect("Get cached buffer"); - - let (max_width, total_height) = measure(buffer); + let entry = cache.get(key).expect("Get cached buffer"); let x = section.bounds.x * scale_factor; let y = section.bounds.y * scale_factor; + let max_width = entry.bounds.width * scale_factor; + let total_height = entry.bounds.height * scale_factor; + let left = match section.horizontal_alignment { alignment::Horizontal::Left => x, alignment::Horizontal::Center => x - max_width / 2.0, @@ -142,14 +147,11 @@ impl Pipeline { let clip_bounds = bounds.intersection(§ion_bounds)?; - // TODO: Subpixel glyph positioning - let left = left.round() as i32; - let top = top.round() as i32; - Some(glyphon::TextArea { - buffer, + buffer: &entry.buffer, left, top, + scale: scale_factor, bounds: glyphon::TextBounds { left: clip_bounds.x as i32, top: clip_bounds.y as i32, @@ -227,11 +229,14 @@ impl Pipeline { pub fn end_frame(&mut self) { self.atlas.trim(); - self.render_cache.trim(); self.prepare_layer = 0; } + pub fn trim_measurements(&mut self) { + self.cache.get_mut().trim(Purpose::Measuring); + } + pub fn measure( &self, content: &str, @@ -240,12 +245,12 @@ impl Pipeline { font: Font, bounds: Size, shaping: Shaping, - ) -> (f32, f32) { - let mut measurement_cache = self.measurement_cache.borrow_mut(); + ) -> Size { + let mut cache = self.cache.borrow_mut(); let line_height = f32::from(line_height.to_absolute(Pixels(size))); - let (_, paragraph) = measurement_cache.allocate( + let (_, entry) = cache.allocate( &mut self.font_system.borrow_mut(), Key { content, @@ -255,9 +260,10 @@ impl Pipeline { bounds, shaping, }, + Purpose::Measuring, ); - measure(paragraph) + entry.bounds } pub fn hit_test( @@ -271,11 +277,11 @@ impl Pipeline { point: Point, _nearest_only: bool, ) -> Option<Hit> { - let mut measurement_cache = self.measurement_cache.borrow_mut(); + let mut cache = self.cache.borrow_mut(); let line_height = f32::from(line_height.to_absolute(Pixels(size))); - let (_, paragraph) = measurement_cache.allocate( + let (_, entry) = cache.allocate( &mut self.font_system.borrow_mut(), Key { content, @@ -285,26 +291,23 @@ impl Pipeline { bounds, shaping, }, + Purpose::Measuring, ); - let cursor = paragraph.hit(point.x, point.y)?; + let cursor = entry.buffer.hit(point.x, point.y)?; Some(Hit::CharOffset(cursor.index)) } - - pub fn trim_measurement_cache(&mut self) { - self.measurement_cache.borrow_mut().trim(); - } } -fn measure(buffer: &glyphon::Buffer) -> (f32, f32) { +fn measure(buffer: &glyphon::Buffer) -> Size { let (width, total_lines) = buffer .layout_runs() .fold((0.0, 0usize), |(width, total_lines), run| { (run.line_w.max(width), total_lines + 1) }); - (width, total_lines as f32 * buffer.metrics().line_height) + Size::new(width, total_lines as f32 * buffer.metrics().line_height) } fn to_family(family: font::Family) -> glyphon::Family<'static> { @@ -354,11 +357,24 @@ fn to_shaping(shaping: Shaping) -> glyphon::Shaping { } struct Cache { - entries: FxHashMap<KeyHash, glyphon::Buffer>, - recently_used: FxHashSet<KeyHash>, + entries: FxHashMap<KeyHash, Entry>, + aliases: FxHashMap<KeyHash, KeyHash>, + recently_measured: FxHashSet<KeyHash>, + recently_drawn: FxHashSet<KeyHash>, hasher: HashBuilder, } +struct Entry { + buffer: glyphon::Buffer, + bounds: Size, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Purpose { + Measuring, + Drawing, +} + #[cfg(not(target_arch = "wasm32"))] type HashBuilder = twox_hash::RandomXxHashBuilder64; @@ -369,12 +385,14 @@ impl Cache { fn new() -> Self { Self { entries: FxHashMap::default(), - recently_used: FxHashSet::default(), + aliases: FxHashMap::default(), + recently_measured: FxHashSet::default(), + recently_drawn: FxHashSet::default(), hasher: HashBuilder::default(), } } - fn get(&self, key: &KeyHash) -> Option<&glyphon::Buffer> { + fn get(&self, key: &KeyHash) -> Option<&Entry> { self.entries.get(key) } @@ -382,21 +400,21 @@ impl Cache { &mut self, font_system: &mut glyphon::FontSystem, key: Key<'_>, - ) -> (KeyHash, &mut glyphon::Buffer) { - let hash = { - let mut hasher = self.hasher.build_hasher(); - - key.content.hash(&mut hasher); - key.size.to_bits().hash(&mut hasher); - key.line_height.to_bits().hash(&mut hasher); - key.font.hash(&mut hasher); - key.bounds.width.to_bits().hash(&mut hasher); - key.bounds.height.to_bits().hash(&mut hasher); - key.shaping.hash(&mut hasher); - - hasher.finish() + purpose: Purpose, + ) -> (KeyHash, &mut Entry) { + let hash = key.hash(self.hasher.build_hasher()); + + let recently_used = match purpose { + Purpose::Measuring => &mut self.recently_measured, + Purpose::Drawing => &mut self.recently_drawn, }; + if let Some(hash) = self.aliases.get(&hash) { + let _ = recently_used.insert(*hash); + + return (*hash, self.entries.get_mut(hash).unwrap()); + } + if let hash_map::Entry::Vacant(entry) = self.entries.entry(hash) { let metrics = glyphon::Metrics::new(key.size, key.line_height); let mut buffer = glyphon::Buffer::new(font_system, metrics); @@ -416,19 +434,48 @@ impl Cache { to_shaping(key.shaping), ); - let _ = entry.insert(buffer); + let bounds = measure(&buffer); + let _ = entry.insert(Entry { buffer, bounds }); + + for bounds in [ + bounds, + Size { + width: key.bounds.width, + ..bounds + }, + ] { + if key.bounds != bounds { + let _ = self.aliases.insert( + Key { bounds, ..key }.hash(self.hasher.build_hasher()), + hash, + ); + } + } } - let _ = self.recently_used.insert(hash); + let _ = recently_used.insert(hash); (hash, self.entries.get_mut(&hash).unwrap()) } - fn trim(&mut self) { - self.entries - .retain(|key, _| self.recently_used.contains(key)); + fn trim(&mut self, purpose: Purpose) { + self.entries.retain(|key, _| { + self.recently_measured.contains(key) + || self.recently_drawn.contains(key) + }); + self.aliases.retain(|_, value| { + self.recently_measured.contains(value) + || self.recently_drawn.contains(value) + }); - self.recently_used.clear(); + match purpose { + Purpose::Measuring => { + self.recently_measured.clear(); + } + Purpose::Drawing => { + self.recently_drawn.clear(); + } + } } } @@ -442,4 +489,18 @@ struct Key<'a> { shaping: Shaping, } +impl Key<'_> { + fn hash<H: Hasher>(self, mut hasher: H) -> KeyHash { + self.content.hash(&mut hasher); + self.size.to_bits().hash(&mut hasher); + self.line_height.to_bits().hash(&mut hasher); + self.font.hash(&mut hasher); + self.bounds.width.to_bits().hash(&mut hasher); + self.bounds.height.to_bits().hash(&mut hasher); + self.shaping.hash(&mut hasher); + + hasher.finish() + } +} + type KeyHash = u64; diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index 6f32f182..d8b23dfe 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -393,7 +393,7 @@ impl Uniforms { } mod solid { - use crate::graphics::primitive; + use crate::graphics::mesh; use crate::graphics::Antialiasing; use crate::triangle; use crate::Buffer; @@ -406,7 +406,7 @@ mod solid { #[derive(Debug)] pub struct Layer { - pub vertices: Buffer<primitive::ColoredVertex2D>, + pub vertices: Buffer<mesh::SolidVertex2D>, pub uniforms: Buffer<triangle::Uniforms>, pub constants: wgpu::BindGroup, } @@ -493,38 +493,40 @@ mod solid { ), }); - let pipeline = device.create_render_pipeline( - &wgpu::RenderPipelineDescriptor { - label: Some("iced_wgpu::triangle::solid pipeline"), - layout: Some(&layout), - vertex: wgpu::VertexState { - module: &shader, - entry_point: "solid_vs_main", - buffers: &[wgpu::VertexBufferLayout { - array_stride: std::mem::size_of::< - primitive::ColoredVertex2D, - >() - as u64, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &wgpu::vertex_attr_array!( - // Position - 0 => Float32x2, - // Color - 1 => Float32x4, - ), - }], + let pipeline = + device.create_render_pipeline( + &wgpu::RenderPipelineDescriptor { + label: Some("iced_wgpu::triangle::solid pipeline"), + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "solid_vs_main", + buffers: &[wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::< + mesh::SolidVertex2D, + >( + ) + as u64, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &wgpu::vertex_attr_array!( + // Position + 0 => Float32x2, + // Color + 1 => Float32x4, + ), + }], + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "solid_fs_main", + targets: &[triangle::fragment_target(format)], + }), + primitive: triangle::primitive_state(), + depth_stencil: None, + multisample: triangle::multisample_state(antialiasing), + multiview: None, }, - fragment: Some(wgpu::FragmentState { - module: &shader, - entry_point: "solid_fs_main", - targets: &[triangle::fragment_target(format)], - }), - primitive: triangle::primitive_state(), - depth_stencil: None, - multisample: triangle::multisample_state(antialiasing), - multiview: None, - }, - ); + ); Self { pipeline, @@ -535,7 +537,8 @@ mod solid { } mod gradient { - use crate::graphics::{primitive, Antialiasing}; + use crate::graphics::mesh; + use crate::graphics::Antialiasing; use crate::triangle; use crate::Buffer; @@ -547,7 +550,7 @@ mod gradient { #[derive(Debug)] pub struct Layer { - pub vertices: Buffer<primitive::GradientVertex2D>, + pub vertices: Buffer<mesh::GradientVertex2D>, pub uniforms: Buffer<triangle::Uniforms>, pub constants: wgpu::BindGroup, } @@ -645,35 +648,25 @@ mod gradient { entry_point: "gradient_vs_main", buffers: &[wgpu::VertexBufferLayout { array_stride: std::mem::size_of::< - primitive::GradientVertex2D, + mesh::GradientVertex2D, >() as u64, step_mode: wgpu::VertexStepMode::Vertex, attributes: &wgpu::vertex_attr_array!( // Position 0 => Float32x2, - // Color 1 - 1 => Float32x4, - // Color 2 - 2 => Float32x4, - // Color 3 - 3 => Float32x4, - // Color 4 - 4 => Float32x4, - // Color 5 - 5 => Float32x4, - // Color 6 - 6 => Float32x4, - // Color 7 - 7 => Float32x4, - // Color 8 - 8 => Float32x4, - // Offsets 1-4 - 9 => Float32x4, - // Offsets 5-8 - 10 => Float32x4, + // Colors 1-2 + 1 => Uint32x4, + // Colors 3-4 + 2 => Uint32x4, + // Colors 5-6 + 3 => Uint32x4, + // Colors 7-8 + 4 => Uint32x4, + // Offsets + 5 => Uint32x4, // Direction - 11 => Float32x4 + 6 => Float32x4 ), }], }, diff --git a/wgpu/src/triangle/msaa.rs b/wgpu/src/triangle/msaa.rs index 4afbdb32..320b5b12 100644 --- a/wgpu/src/triangle/msaa.rs +++ b/wgpu/src/triangle/msaa.rs @@ -16,15 +16,8 @@ impl Blit { format: wgpu::TextureFormat, antialiasing: graphics::Antialiasing, ) -> Blit { - let sampler = device.create_sampler(&wgpu::SamplerDescriptor { - address_mode_u: wgpu::AddressMode::ClampToEdge, - address_mode_v: wgpu::AddressMode::ClampToEdge, - address_mode_w: wgpu::AddressMode::ClampToEdge, - mag_filter: wgpu::FilterMode::Nearest, - min_filter: wgpu::FilterMode::Nearest, - mipmap_filter: wgpu::FilterMode::Nearest, - ..Default::default() - }); + let sampler = + device.create_sampler(&wgpu::SamplerDescriptor::default()); let constant_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 2eaafde0..cd5b20cc 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -1,10 +1,10 @@ //! Connect a window with a renderer. -use crate::core::Color; +use crate::core::{Color, Size}; use crate::graphics; use crate::graphics::color; use crate::graphics::compositor; -use crate::graphics::{Error, Primitive, Viewport}; -use crate::{Backend, Renderer, Settings}; +use crate::graphics::{Error, Viewport}; +use crate::{Backend, Primitive, Renderer, Settings}; use futures::stream::{self, StreamExt}; @@ -283,4 +283,154 @@ impl<Theme> graphics::Compositor for Compositor<Theme> { ) }) } + + fn screenshot<T: AsRef<str>>( + &mut self, + renderer: &mut Self::Renderer, + _surface: &mut Self::Surface, + viewport: &Viewport, + background_color: Color, + overlay: &[T], + ) -> Vec<u8> { + renderer.with_primitives(|backend, primitives| { + screenshot( + self, + backend, + primitives, + viewport, + background_color, + overlay, + ) + }) + } +} + +/// Renders the current surface to an offscreen buffer. +/// +/// Returns RGBA bytes of the texture data. +pub fn screenshot<Theme, T: AsRef<str>>( + compositor: &Compositor<Theme>, + backend: &mut Backend, + primitives: &[Primitive], + viewport: &Viewport, + background_color: Color, + overlay: &[T], +) -> Vec<u8> { + let mut encoder = compositor.device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { + label: Some("iced_wgpu.offscreen.encoder"), + }, + ); + + let dimensions = BufferDimensions::new(viewport.physical_size()); + + let texture_extent = wgpu::Extent3d { + width: dimensions.width, + height: dimensions.height, + depth_or_array_layers: 1, + }; + + let texture = compositor.device.create_texture(&wgpu::TextureDescriptor { + label: Some("iced_wgpu.offscreen.source_texture"), + size: texture_extent, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: compositor.format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::COPY_SRC + | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + + let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + + backend.present( + &compositor.device, + &compositor.queue, + &mut encoder, + Some(background_color), + &view, + primitives, + viewport, + overlay, + ); + + let texture = crate::color::convert( + &compositor.device, + &mut encoder, + texture, + if color::GAMMA_CORRECTION { + wgpu::TextureFormat::Rgba8UnormSrgb + } else { + wgpu::TextureFormat::Rgba8Unorm + }, + ); + + let output_buffer = + compositor.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("iced_wgpu.offscreen.output_texture_buffer"), + size: (dimensions.padded_bytes_per_row * dimensions.height as usize) + as u64, + usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + encoder.copy_texture_to_buffer( + texture.as_image_copy(), + wgpu::ImageCopyBuffer { + buffer: &output_buffer, + layout: wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: Some(dimensions.padded_bytes_per_row as u32), + rows_per_image: None, + }, + }, + texture_extent, + ); + + let index = compositor.queue.submit(Some(encoder.finish())); + + let slice = output_buffer.slice(..); + slice.map_async(wgpu::MapMode::Read, |_| {}); + + let _ = compositor + .device + .poll(wgpu::Maintain::WaitForSubmissionIndex(index)); + + let mapped_buffer = slice.get_mapped_range(); + + mapped_buffer.chunks(dimensions.padded_bytes_per_row).fold( + vec![], + |mut acc, row| { + acc.extend(&row[..dimensions.unpadded_bytes_per_row]); + acc + }, + ) +} + +#[derive(Clone, Copy, Debug)] +struct BufferDimensions { + width: u32, + height: u32, + unpadded_bytes_per_row: usize, + padded_bytes_per_row: usize, +} + +impl BufferDimensions { + fn new(size: Size<u32>) -> Self { + let unpadded_bytes_per_row = size.width as usize * 4; //slice of buffer per row; always RGBA + let alignment = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize; //256 + let padded_bytes_per_row_padding = + (alignment - unpadded_bytes_per_row % alignment) % alignment; + let padded_bytes_per_row = + unpadded_bytes_per_row + padded_bytes_per_row_padding; + + Self { + width: size.width, + height: size.height, + unpadded_bytes_per_row, + padded_bytes_per_row, + } + } } diff --git a/widget/src/button.rs b/widget/src/button.rs index 0f3cbde2..8ebc9657 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -102,8 +102,17 @@ where /// Sets the message that will be produced when the [`Button`] is pressed. /// /// Unless `on_press` is called, the [`Button`] will be disabled. - pub fn on_press(mut self, msg: Message) -> Self { - self.on_press = Some(msg); + pub fn on_press(mut self, on_press: Message) -> Self { + self.on_press = Some(on_press); + self + } + + /// Sets the message that will be produced when the [`Button`] is pressed, + /// if `Some`. + /// + /// If `None`, the [`Button`] will be disabled. + pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self { + self.on_press = on_press; self } diff --git a/widget/src/canvas/program.rs b/widget/src/canvas/program.rs index 929ee285..b3f6175e 100644 --- a/widget/src/canvas/program.rs +++ b/widget/src/canvas/program.rs @@ -1,7 +1,7 @@ use crate::canvas::event::{self, Event}; use crate::canvas::mouse; use crate::core::Rectangle; -use crate::graphics::geometry::{self, Geometry}; +use crate::graphics::geometry; /// The state and logic of a [`Canvas`]. /// @@ -51,7 +51,7 @@ where theme: &Renderer::Theme, bounds: Rectangle, cursor: mouse::Cursor, - ) -> Vec<Geometry>; + ) -> Vec<Renderer::Geometry>; /// Returns the current mouse interaction of the [`Program`]. /// @@ -93,7 +93,7 @@ where theme: &Renderer::Theme, bounds: Rectangle, cursor: mouse::Cursor, - ) -> Vec<Geometry> { + ) -> Vec<Renderer::Geometry> { T::draw(self, state, renderer, theme, bounds, cursor) } diff --git a/widget/src/qr_code.rs b/widget/src/qr_code.rs index 06be93c0..51a541fd 100644 --- a/widget/src/qr_code.rs +++ b/widget/src/qr_code.rs @@ -7,6 +7,7 @@ use crate::core::widget::Tree; use crate::core::{ Color, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget, }; +use crate::graphics::geometry::Renderer as _; use crate::Renderer; use thiserror::Error; @@ -121,7 +122,7 @@ impl<'a, Message, Theme> Widget<Message, Renderer<Theme>> for QRCode<'a> { let translation = Vector::new(bounds.x, bounds.y); renderer.with_translation(translation, |renderer| { - renderer.draw_primitive(geometry.0); + renderer.draw(vec![geometry]); }); } } diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 010befac..473124ca 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -29,8 +29,7 @@ where id: Option<Id>, width: Length, height: Length, - vertical: Properties, - horizontal: Option<Properties>, + direction: Direction, content: Element<'a, Message, Renderer>, on_scroll: Option<Box<dyn Fn(Viewport) -> Message + 'a>>, style: <Renderer::Theme as StyleSheet>::Style, @@ -47,8 +46,7 @@ where id: None, width: Length::Shrink, height: Length::Shrink, - vertical: Properties::default(), - horizontal: None, + direction: Default::default(), content: content.into(), on_scroll: None, style: Default::default(), @@ -73,15 +71,9 @@ where self } - /// Configures the vertical scrollbar of the [`Scrollable`] . - pub fn vertical_scroll(mut self, properties: Properties) -> Self { - self.vertical = properties; - self - } - - /// Configures the horizontal scrollbar of the [`Scrollable`] . - pub fn horizontal_scroll(mut self, properties: Properties) -> Self { - self.horizontal = Some(properties); + /// Sets the [`Direction`] of the [`Scrollable`] . + pub fn direction(mut self, direction: Direction) -> Self { + self.direction = direction; self } @@ -103,8 +95,50 @@ where } } +/// The direction of [`Scrollable`]. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Direction { + /// Vertical scrolling + Vertical(Properties), + /// Horizontal scrolling + Horizontal(Properties), + /// Both vertical and horizontal scrolling + Both { + /// The properties of the vertical scrollbar. + vertical: Properties, + /// The properties of the horizontal scrollbar. + horizontal: Properties, + }, +} + +impl Direction { + /// Returns the [`Properties`] of the horizontal scrollbar, if any. + pub fn horizontal(&self) -> Option<&Properties> { + match self { + Self::Horizontal(properties) => Some(properties), + Self::Both { horizontal, .. } => Some(horizontal), + _ => None, + } + } + + /// Returns the [`Properties`] of the vertical scrollbar, if any. + pub fn vertical(&self) -> Option<&Properties> { + match self { + Self::Vertical(properties) => Some(properties), + Self::Both { vertical, .. } => Some(vertical), + _ => None, + } + } +} + +impl Default for Direction { + fn default() -> Self { + Self::Vertical(Properties::default()) + } +} + /// Properties of a scrollbar within a [`Scrollable`]. -#[derive(Debug)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Properties { width: f32, margin: f32, @@ -186,7 +220,7 @@ where limits, self.width, self.height, - self.horizontal.is_some(), + &self.direction, |renderer, limits| { self.content.as_widget().layout(renderer, limits) }, @@ -234,8 +268,7 @@ where cursor, clipboard, shell, - &self.vertical, - self.horizontal.as_ref(), + &self.direction, &self.on_scroll, |event, layout, cursor, clipboard, shell| { self.content.as_widget_mut().on_event( @@ -267,8 +300,7 @@ where theme, layout, cursor, - &self.vertical, - self.horizontal.as_ref(), + &self.direction, &self.style, |renderer, layout, cursor, viewport| { self.content.as_widget().draw( @@ -296,8 +328,7 @@ where tree.state.downcast_ref::<State>(), layout, cursor, - &self.vertical, - self.horizontal.as_ref(), + &self.direction, |layout, cursor, viewport| { self.content.as_widget().mouse_interaction( &tree.children[0], @@ -327,10 +358,11 @@ where let bounds = layout.bounds(); let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); - let offset = tree - .state - .downcast_ref::<State>() - .offset(bounds, content_bounds); + let offset = tree.state.downcast_ref::<State>().offset( + &self.direction, + bounds, + content_bounds, + ); overlay.translate(Vector::new(-offset.x, -offset.y)) }) @@ -399,20 +431,24 @@ pub fn layout<Renderer>( limits: &layout::Limits, width: Length, height: Length, - horizontal_enabled: bool, + direction: &Direction, layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, ) -> layout::Node { let limits = limits.width(width).height(height); let child_limits = layout::Limits::new( - Size::new(limits.min().width, 0.0), + Size::new(limits.min().width, limits.min().height), Size::new( - if horizontal_enabled { + if direction.horizontal().is_some() { f32::INFINITY } else { limits.max().width }, - f32::MAX, + if direction.vertical().is_some() { + f32::MAX + } else { + limits.max().height + }, ), ); @@ -431,8 +467,7 @@ pub fn update<Message>( cursor: mouse::Cursor, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - vertical: &Properties, - horizontal: Option<&Properties>, + direction: &Direction, on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>, update_content: impl FnOnce( Event, @@ -448,8 +483,7 @@ pub fn update<Message>( let content = layout.children().next().unwrap(); let content_bounds = content.bounds(); - let scrollbars = - Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); + let scrollbars = Scrollbars::new(state, direction, bounds, content_bounds); let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = scrollbars.is_mouse_over(cursor); @@ -460,7 +494,8 @@ pub fn update<Message>( if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) => { mouse::Cursor::Available( - cursor_position + state.offset(bounds, content_bounds), + cursor_position + + state.offset(direction, bounds, content_bounds), ) } _ => mouse::Cursor::Unavailable, @@ -713,8 +748,7 @@ pub fn mouse_interaction( state: &State, layout: Layout<'_>, cursor: mouse::Cursor, - vertical: &Properties, - horizontal: Option<&Properties>, + direction: &Direction, content_interaction: impl FnOnce( Layout<'_>, mouse::Cursor, @@ -727,8 +761,7 @@ pub fn mouse_interaction( let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); - let scrollbars = - Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); + let scrollbars = Scrollbars::new(state, direction, bounds, content_bounds); let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = scrollbars.is_mouse_over(cursor); @@ -738,7 +771,7 @@ pub fn mouse_interaction( { mouse::Interaction::Idle } else { - let offset = state.offset(bounds, content_bounds); + let offset = state.offset(direction, bounds, content_bounds); let cursor = match cursor_over_scrollable { Some(cursor_position) @@ -768,8 +801,7 @@ pub fn draw<Renderer>( theme: &Renderer::Theme, layout: Layout<'_>, cursor: mouse::Cursor, - vertical: &Properties, - horizontal: Option<&Properties>, + direction: &Direction, style: &<Renderer::Theme as StyleSheet>::Style, draw_content: impl FnOnce(&mut Renderer, Layout<'_>, mouse::Cursor, &Rectangle), ) where @@ -780,14 +812,13 @@ pub fn draw<Renderer>( let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); - let scrollbars = - Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); + let scrollbars = Scrollbars::new(state, direction, bounds, content_bounds); let cursor_over_scrollable = cursor.position_over(bounds); let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = scrollbars.is_mouse_over(cursor); - let offset = state.offset(bounds, content_bounds); + let offset = state.offset(direction, bounds, content_bounds); let cursor = match cursor_over_scrollable { Some(cursor_position) @@ -1126,16 +1157,25 @@ impl State { ); } - /// Returns the scrolling offset of the [`State`], given the bounds of the - /// [`Scrollable`] and its contents. + /// Returns the scrolling offset of the [`State`], given a [`Direction`], + /// the bounds of the [`Scrollable`] and its contents. pub fn offset( &self, + direction: &Direction, bounds: Rectangle, content_bounds: Rectangle, ) -> Vector { Vector::new( - self.offset_x.absolute(bounds.width, content_bounds.width), - self.offset_y.absolute(bounds.height, content_bounds.height), + if direction.horizontal().is_some() { + self.offset_x.absolute(bounds.width, content_bounds.width) + } else { + 0.0 + }, + if direction.vertical().is_some() { + self.offset_y.absolute(bounds.height, content_bounds.height) + } else { + 0.0 + }, ) } @@ -1157,22 +1197,21 @@ impl Scrollbars { /// Create y and/or x scrollbar(s) if content is overflowing the [`Scrollable`] bounds. fn new( state: &State, - vertical: &Properties, - horizontal: Option<&Properties>, + direction: &Direction, bounds: Rectangle, content_bounds: Rectangle, ) -> Self { - let offset = state.offset(bounds, content_bounds); + let offset = state.offset(direction, bounds, content_bounds); - let show_scrollbar_x = horizontal.and_then(|h| { - if content_bounds.width > bounds.width { - Some(h) - } else { - None - } - }); + let show_scrollbar_x = direction + .horizontal() + .filter(|_| content_bounds.width > bounds.width); + + let show_scrollbar_y = direction + .vertical() + .filter(|_| content_bounds.height > bounds.height); - let y_scrollbar = if content_bounds.height > bounds.height { + let y_scrollbar = if let Some(vertical) = show_scrollbar_y { let Properties { width, margin, @@ -1240,9 +1279,8 @@ impl Scrollbars { // Need to adjust the width of the horizontal scrollbar if the vertical scrollbar // is present - let scrollbar_y_width = y_scrollbar.map_or(0.0, |_| { - vertical.width.max(vertical.scroller_width) + vertical.margin - }); + let scrollbar_y_width = show_scrollbar_y + .map_or(0.0, |v| v.width.max(v.scroller_width) + v.margin); let total_scrollbar_height = width.max(scroller_width) + 2.0 * margin; diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index 8b51f2d0..1b31765f 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -5,6 +5,7 @@ use crate::core::layout; use crate::core::mouse; use crate::core::renderer; use crate::core::text; +use crate::core::touch; use crate::core::widget::Tree; use crate::core::{ Alignment, Clipboard, Element, Event, Layout, Length, Pixels, Rectangle, @@ -208,7 +209,8 @@ where shell: &mut Shell<'_, Message>, ) -> event::Status { match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { let mouse_over = cursor.is_over(layout.bounds()); if mouse_over { diff --git a/winit/Cargo.toml b/winit/Cargo.toml index b75b1929..de7c1c62 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -31,7 +31,7 @@ raw-window-handle = "0.5" [dependencies.winit] version = "0.28" git = "https://github.com/iced-rs/winit.git" -rev = "ac1ddfe0bd870910b3aa64a18d386fdd55b30a1d" +rev = "c52db2045d0a2f1b8d9923870de1d4ab1994146e" default-features = false [dependencies.iced_runtime] diff --git a/winit/src/application.rs b/winit/src/application.rs index afc523b6..d1689452 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -308,6 +308,8 @@ async fn run_instance<A, E, C>( run_command( &application, + &mut compositor, + &mut surface, &mut cache, &state, &mut renderer, @@ -318,7 +320,6 @@ async fn run_instance<A, E, C>( &mut proxy, &mut debug, &window, - || compositor.fetch_information(), ); runtime.track(application.subscription().into_recipes()); @@ -382,6 +383,8 @@ async fn run_instance<A, E, C>( // Update application update( &mut application, + &mut compositor, + &mut surface, &mut cache, &state, &mut renderer, @@ -392,7 +395,6 @@ async fn run_instance<A, E, C>( &mut debug, &mut messages, &window, - || compositor.fetch_information(), ); // Update window @@ -645,8 +647,10 @@ where /// Updates an [`Application`] by feeding it the provided messages, spawning any /// resulting [`Command`], and tracking its [`Subscription`]. -pub fn update<A: Application, E: Executor>( +pub fn update<A: Application, C, E: Executor>( application: &mut A, + compositor: &mut C, + surface: &mut C::Surface, cache: &mut user_interface::Cache, state: &State<A>, renderer: &mut A::Renderer, @@ -657,8 +661,8 @@ pub fn update<A: Application, E: Executor>( debug: &mut Debug, messages: &mut Vec<A::Message>, window: &winit::window::Window, - graphics_info: impl FnOnce() -> compositor::Information + Copy, ) where + C: Compositor<Renderer = A::Renderer> + 'static, <A::Renderer as core::Renderer>::Theme: StyleSheet, { for message in messages.drain(..) { @@ -676,6 +680,8 @@ pub fn update<A: Application, E: Executor>( run_command( application, + compositor, + surface, cache, state, renderer, @@ -686,7 +692,6 @@ pub fn update<A: Application, E: Executor>( proxy, debug, window, - graphics_info, ); } @@ -695,8 +700,10 @@ pub fn update<A: Application, E: Executor>( } /// Runs the actions of a [`Command`]. -pub fn run_command<A, E>( +pub fn run_command<A, C, E>( application: &A, + compositor: &mut C, + surface: &mut C::Surface, cache: &mut user_interface::Cache, state: &State<A>, renderer: &mut A::Renderer, @@ -707,10 +714,10 @@ pub fn run_command<A, E>( proxy: &mut winit::event_loop::EventLoopProxy<A::Message>, debug: &mut Debug, window: &winit::window::Window, - _graphics_info: impl FnOnce() -> compositor::Information + Copy, ) where A: Application, E: Executor, + C: Compositor<Renderer = A::Renderer> + 'static, <A::Renderer as core::Renderer>::Theme: StyleSheet, { use crate::runtime::command; @@ -812,12 +819,28 @@ pub fn run_command<A, E>( .send_event(tag(window.id().into())) .expect("Send message to event loop"); } + window::Action::Screenshot(tag) => { + let bytes = compositor.screenshot( + renderer, + surface, + state.viewport(), + state.background_color(), + &debug.overlay(), + ); + + proxy + .send_event(tag(window::Screenshot::new( + bytes, + state.physical_size(), + ))) + .expect("Send message to event loop.") + } }, command::Action::System(action) => match action { system::Action::QueryInformation(_tag) => { #[cfg(feature = "system")] { - let graphics_info = _graphics_info(); + let graphics_info = compositor.fetch_information(); let proxy = proxy.clone(); let _ = std::thread::spawn(move || { |