From fae95d60953d59c520ee2f5214174c5b3a6f0087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 29 Apr 2025 22:31:58 +0200 Subject: [PATCH 1/9] Draft `float` widget and simplify `image` again :tada: --- core/src/element.rs | 4 +- core/src/overlay.rs | 2 +- core/src/transformation.rs | 5 + core/src/widget.rs | 2 +- examples/gallery/src/main.rs | 48 ++-- examples/toast/src/main.rs | 2 +- widget/src/button.rs | 2 +- widget/src/column.rs | 2 +- widget/src/container.rs | 2 +- widget/src/float.rs | 370 ++++++++++++++++++++++++++++++ widget/src/grid.rs | 2 +- widget/src/helpers.rs | 23 +- widget/src/image.rs | 249 +------------------- widget/src/keyed/column.rs | 2 +- widget/src/lazy.rs | 10 +- widget/src/lazy/component.rs | 2 +- widget/src/lazy/responsive.rs | 14 +- widget/src/lib.rs | 3 + widget/src/mouse_area.rs | 2 +- widget/src/pane_grid.rs | 2 +- widget/src/pane_grid/content.rs | 2 +- widget/src/pane_grid/title_bar.rs | 2 +- widget/src/pin.rs | 2 +- widget/src/pop.rs | 2 +- widget/src/row.rs | 4 +- widget/src/scrollable.rs | 2 +- widget/src/stack.rs | 2 +- widget/src/themer.rs | 2 +- widget/src/tooltip.rs | 2 +- 29 files changed, 463 insertions(+), 305 deletions(-) create mode 100644 widget/src/float.rs diff --git a/core/src/element.rs b/core/src/element.rs index 6f8751ab..9d083d79 100644 --- a/core/src/element.rs +++ b/core/src/element.rs @@ -366,7 +366,7 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, viewport: &Rectangle, translation: Vector, @@ -518,7 +518,7 @@ where fn overlay<'b>( &'b mut self, state: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, viewport: &Rectangle, translation: Vector, diff --git a/core/src/overlay.rs b/core/src/overlay.rs index 92118567..90d5731a 100644 --- a/core/src/overlay.rs +++ b/core/src/overlay.rs @@ -120,7 +120,7 @@ where pub fn from_children<'a, Message, Theme, Renderer>( children: &'a mut [crate::Element<'_, Message, Theme, Renderer>], tree: &'a mut Tree, - layout: Layout<'_>, + layout: Layout<'a>, renderer: &Renderer, viewport: &Rectangle, translation: Vector, diff --git a/core/src/transformation.rs b/core/src/transformation.rs index 74183147..d9b805ae 100644 --- a/core/src/transformation.rs +++ b/core/src/transformation.rs @@ -40,6 +40,11 @@ impl Transformation { pub fn translation(&self) -> Vector { Vector::new(self.0.w_axis.x, self.0.w_axis.y) } + + /// Returns the inverse of the [`Transformation`]. + pub fn inverse(self) -> Self { + Transformation(self.0.inverse()) + } } impl Default for Transformation { diff --git a/core/src/widget.rs b/core/src/widget.rs index 72985e3e..655c010b 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -144,7 +144,7 @@ where fn overlay<'a>( &'a mut self, _state: &'a mut Tree, - _layout: Layout<'_>, + _layout: Layout<'a>, _renderer: &Renderer, _viewport: &Rectangle, _translation: Vector, diff --git a/examples/gallery/src/main.rs b/examples/gallery/src/main.rs index bbf4ac7d..a40df370 100644 --- a/examples/gallery/src/main.rs +++ b/examples/gallery/src/main.rs @@ -9,8 +9,8 @@ use crate::civitai::{Error, Id, Image, Rgba, Size}; use iced::animation; use iced::time::{Instant, milliseconds}; use iced::widget::{ - button, container, grid, horizontal_space, image, mouse_area, opaque, pop, - scrollable, stack, + button, container, float, grid, horizontal_space, image, mouse_area, + opaque, pop, scrollable, stack, }; use iced::window; use iced::{ @@ -204,28 +204,28 @@ fn card<'a>( let image = if let Some(preview) = preview { let thumbnail: Element<'_, _> = if let Preview::Ready { thumbnail, .. } = &preview { - image(&thumbnail.handle) - .width(Fill) - .content_fit(ContentFit::Cover) - .opacity(thumbnail.fade_in.interpolate(0.0, 1.0, now)) - .scale(thumbnail.zoom.interpolate(1.0, 1.1, now)) - .translate(move |bounds, viewport| { - bounds.zoom(1.1).offset(&viewport.shrink(10)) - * thumbnail.zoom.interpolate(0.0, 1.0, now) - }) - .style(move |_theme| image::Style { - shadow: Shadow { - color: Color::BLACK.scale_alpha( - thumbnail.zoom.interpolate(0.0, 1.0, now), - ), - blur_radius: thumbnail - .zoom - .interpolate(0.0, 20.0, now), - ..Shadow::default() - }, - ..image::Style::default() - }) - .into() + float( + image(&thumbnail.handle) + .width(Fill) + .content_fit(ContentFit::Cover) + .opacity(thumbnail.fade_in.interpolate(0.0, 1.0, now)), + ) + .scale(thumbnail.zoom.interpolate(1.0, 1.1, now)) + .translate(move |bounds, viewport| { + bounds.zoom(1.1).offset(&viewport.shrink(10)) + * thumbnail.zoom.interpolate(0.0, 1.0, now) + }) + .style(move |_theme| float::Style { + shadow: Shadow { + color: Color::BLACK.scale_alpha( + thumbnail.zoom.interpolate(0.0, 1.0, now), + ), + blur_radius: thumbnail.zoom.interpolate(0.0, 20.0, now), + ..Shadow::default() + }, + ..float::Style::default() + }) + .into() } else { horizontal_space().into() }; diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index ef9418d8..a58fb03a 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -423,7 +423,7 @@ mod toast { fn overlay<'b>( &'b mut self, state: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, viewport: &Rectangle, translation: Vector, diff --git a/widget/src/button.rs b/widget/src/button.rs index 63987597..d084cd61 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -430,7 +430,7 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, viewport: &Rectangle, translation: Vector, diff --git a/widget/src/column.rs b/widget/src/column.rs index 777eb328..4ab56c89 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -336,7 +336,7 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, viewport: &Rectangle, translation: Vector, diff --git a/widget/src/container.rs b/widget/src/container.rs index 1c774ced..7b9c8bf7 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -377,7 +377,7 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, viewport: &Rectangle, translation: Vector, diff --git a/widget/src/float.rs b/widget/src/float.rs new file mode 100644 index 00000000..a5f893d9 --- /dev/null +++ b/widget/src/float.rs @@ -0,0 +1,370 @@ +#![allow(missing_docs)] +use crate::core; +use crate::core::border; +use crate::core::layout; +use crate::core::mouse; +use crate::core::overlay; +use crate::core::renderer; +use crate::core::widget; +use crate::core::widget::tree; +use crate::core::{ + Clipboard, Element, Event, Layout, Length, Point, Rectangle, Shadow, Shell, + Size, Transformation, Vector, Widget, +}; + +#[allow(missing_debug_implementations)] +pub struct Float<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> +where + Theme: Catalog, +{ + content: Element<'a, Message, Theme, Renderer>, + scale: f32, + translate: Option Vector + 'a>>, + opaque: bool, + class: Theme::Class<'a>, +} + +impl<'a, Message, Theme, Renderer> Float<'a, Message, Theme, Renderer> +where + Theme: Catalog, +{ + pub fn new( + content: impl Into>, + ) -> Self { + Self { + content: content.into(), + scale: 1.0, + translate: None, + opaque: false, + class: Theme::default(), + } + } + + pub fn scale(mut self, scale: f32) -> Self { + self.scale = scale; + self + } + + pub fn translate( + mut self, + translate: impl Fn(Rectangle, Rectangle) -> Vector + 'a, + ) -> Self { + self.translate = Some(Box::new(translate)); + self + } + + pub fn opaque(mut self, opaque: bool) -> Self { + self.opaque = opaque; + self + } + + /// Sets the style of the [`Float`]. + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self + where + Theme::Class<'a>: From>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`Float`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into>) -> Self { + self.class = class.into(); + self + } + + fn is_floating(&self, bounds: Rectangle, viewport: Rectangle) -> bool { + self.scale > 1.0 + || self.translate.as_ref().is_some_and(|translate| { + translate(bounds, viewport) != Vector::ZERO + }) + } +} + +impl Widget + for Float<'_, Message, Theme, Renderer> +where + Theme: Catalog, + Renderer: core::Renderer, +{ + fn tag(&self) -> tree::Tag { + self.content.as_widget().tag() + } + + fn state(&self) -> tree::State { + self.content.as_widget().state() + } + + fn children(&self) -> Vec { + self.content.as_widget().children() + } + + fn diff(&self, tree: &mut widget::Tree) { + self.content.as_widget().diff(tree); + } + + fn size(&self) -> Size { + self.content.as_widget().size() + } + + fn size_hint(&self) -> Size { + self.content.as_widget().size_hint() + } + + fn layout( + &self, + tree: &mut widget::Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.content.as_widget().layout(tree, renderer, limits) + } + + fn update( + &mut self, + state: &mut widget::Tree, + event: &Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) { + self.content.as_widget_mut().update( + state, event, layout, cursor, renderer, clipboard, shell, viewport, + ); + } + + fn draw( + &self, + tree: &widget::Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + if self.is_floating(layout.bounds(), *viewport) { + return; + } + + { + let style = theme.style(&self.class); + + if style.shadow.color.a > 0.0 { + renderer.fill_quad( + renderer::Quad { + bounds: layout.bounds().shrink(1.0), + shadow: style.shadow, + border: border::rounded(style.shadow_border_radius), + }, + style.shadow.color, + ); + } + } + + self.content + .as_widget() + .draw(tree, renderer, theme, style, layout, cursor, viewport); + } + + fn mouse_interaction( + &self, + state: &widget::Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.content + .as_widget() + .mouse_interaction(state, layout, cursor, viewport, renderer) + } + + fn operate( + &self, + state: &mut widget::Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation, + ) { + self.content + .as_widget() + .operate(state, layout, renderer, operation); + } + + fn overlay<'a>( + &'a mut self, + state: &'a mut widget::Tree, + layout: Layout<'a>, + renderer: &Renderer, + viewport: &Rectangle, + offset: Vector, + ) -> Option> { + let bounds = layout.bounds(); + + let translation = self + .translate + .as_ref() + .map(|translate| translate(bounds + offset, *viewport)) + .unwrap_or(Vector::ZERO); + + if self.scale > 1.0 || translation != Vector::ZERO { + Some(overlay::Element::new(Box::new(Overlay { + float: self, + state, + layout, + viewport: *viewport, + bounds, + translation: translation + offset, + }))) + } else { + self.content + .as_widget_mut() + .overlay(state, layout, renderer, viewport, offset) + } + } +} + +impl<'a, Message, Theme, Renderer> From> + for Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: Catalog + 'a, + Renderer: core::Renderer + 'a, +{ + fn from(float: Float<'a, Message, Theme, Renderer>) -> Self { + Element::new(float) + } +} + +struct Overlay<'a, 'b, Message, Theme, Renderer> +where + Theme: Catalog, +{ + float: &'a Float<'b, Message, Theme, Renderer>, + state: &'a mut widget::Tree, + layout: Layout<'a>, + viewport: Rectangle, + bounds: Rectangle, + translation: Vector, +} + +impl core::Overlay + for Overlay<'_, '_, Message, Theme, Renderer> +where + Theme: Catalog, + Renderer: core::Renderer, +{ + fn layout(&mut self, _renderer: &Renderer, _bounds: Size) -> layout::Node { + layout::Node::new(self.bounds.size()).move_to(self.bounds.position()) + } + + fn is_over( + &self, + layout: Layout<'_>, + _renderer: &Renderer, + cursor_position: Point, + ) -> bool { + self.float.opaque && layout.bounds().contains(cursor_position) + } + + fn draw( + &self, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + ) { + let bounds = layout.bounds(); + + let transformation = Transformation::translate( + bounds.x + bounds.width / 2.0 + self.translation.x, + bounds.y + bounds.height / 2.0 + self.translation.y, + ) * Transformation::scale(self.float.scale) + * Transformation::translate( + -bounds.x - bounds.width / 2.0, + -bounds.y - bounds.height / 2.0, + ); + + renderer.with_layer(self.viewport, |renderer| { + renderer.with_transformation(transformation, |renderer| { + { + let style = theme.style(&self.float.class); + + if style.shadow.color.a > 0.0 { + renderer.fill_quad( + renderer::Quad { + bounds: bounds.shrink(1.0), + shadow: style.shadow, + border: border::rounded( + style.shadow_border_radius, + ), + }, + style.shadow.color, + ); + } + } + + self.float.content.as_widget().draw( + self.state, + renderer, + theme, + style, + self.layout, + cursor, + &(self.viewport * transformation.inverse()), + ); + }); + }); + } + + fn index(&self) -> f32 { + self.float.scale * 0.5 + } +} + +/// The theme catalog of a [`Float`]. +/// +/// All themes that can be used with [`Float`] +/// must implement this trait. +pub trait Catalog { + /// The item class of the [`Catalog`]. + type Class<'a>; + + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; + + /// The [`Style`] of a class with the given status. + fn style(&self, class: &Self::Class<'_>) -> Style; +} + +/// A styling function for a [`Float`]. +pub type StyleFn<'a, Theme> = Box Style + 'a>; + +impl Catalog for crate::Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(|_| Style::default()) + } + + fn style(&self, class: &Self::Class<'_>) -> Style { + class(self) + } +} + +/// The style of a [`Float`]. +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub struct Style { + /// The [`Shadow`] of the [`Float`]. + pub shadow: Shadow, + /// The border radius of the shadow. + pub shadow_border_radius: border::Radius, +} diff --git a/widget/src/grid.rs b/widget/src/grid.rs index 76de415d..4a08dc55 100644 --- a/widget/src/grid.rs +++ b/widget/src/grid.rs @@ -343,7 +343,7 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, viewport: &Rectangle, translation: Vector, diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 82908f88..9e8d5f6c 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -7,6 +7,7 @@ use crate::core; use crate::core::widget::operation::{self, Operation}; use crate::core::window; use crate::core::{Element, Length, Pixels, Widget}; +use crate::float::{self, Float}; use crate::keyed; use crate::overlay; use crate::pane_grid::{self, PaneGrid}; @@ -692,7 +693,7 @@ where fn overlay<'b>( &'b mut self, state: &'b mut core::widget::Tree, - layout: core::Layout<'_>, + layout: core::Layout<'b>, renderer: &Renderer, viewport: &Rectangle, translation: core::Vector, @@ -948,7 +949,7 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut core::widget::Tree, - layout: core::Layout<'_>, + layout: core::Layout<'b>, renderer: &Renderer, viewport: &Rectangle, translation: core::Vector, @@ -1848,12 +1849,7 @@ where /// ``` /// #[cfg(feature = "image")] -pub fn image<'a, Handle, Theme>( - handle: impl Into, -) -> crate::Image<'a, Handle, Theme> -where - Theme: crate::image::Catalog, -{ +pub fn image(handle: impl Into) -> crate::Image { crate::Image::new(handle.into()) } @@ -2127,3 +2123,14 @@ where { PaneGrid::new(state, view) } + +/// Creates a new [`Float`] widget with the given content. +pub fn float<'a, Message, Theme, Renderer>( + content: impl Into>, +) -> Float<'a, Message, Theme, Renderer> +where + Theme: float::Catalog, + Renderer: core::Renderer, +{ + Float::new(content) +} diff --git a/widget/src/image.rs b/widget/src/image.rs index fa40f441..e94213a0 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -19,17 +19,14 @@ pub mod viewer; pub use viewer::Viewer; -use crate::core; -use crate::core::border; use crate::core::image; use crate::core::layout; use crate::core::mouse; -use crate::core::overlay; use crate::core::renderer; use crate::core::widget::Tree; use crate::core::{ - ContentFit, Element, Layout, Length, Point, Rectangle, Rotation, Shadow, - Size, Vector, Widget, + ContentFit, Element, Layout, Length, Point, Rectangle, Rotation, Size, + Vector, Widget, }; pub use image::{FilterMethod, Handle}; @@ -58,10 +55,7 @@ pub fn viewer(handle: Handle) -> Viewer { /// ``` /// #[allow(missing_debug_implementations)] -pub struct Image<'a, Handle = image::Handle, Theme = crate::Theme> -where - Theme: Catalog, -{ +pub struct Image { handle: Handle, width: Length, height: Length, @@ -70,14 +64,9 @@ where rotation: Rotation, opacity: f32, scale: f32, - translate: Option Vector + 'a>>, - class: Theme::Class<'a>, } -impl<'a, Handle, Theme> Image<'a, Handle, Theme> -where - Theme: Catalog, -{ +impl Image { /// Creates a new [`Image`] with the given path. pub fn new(handle: impl Into) -> Self { Image { @@ -89,8 +78,6 @@ where rotation: Rotation::default(), opacity: 1.0, scale: 1.0, - translate: None, - class: Theme::default(), } } @@ -143,40 +130,6 @@ where self.scale = scale.into(); self } - - /// Sets the translation that should be applied to an [`Image`], potentially making it - /// float above other content. - /// - /// This method takes a closure that will receive the non-scaled bounds of the [`Image`] - /// and the bounds of the viewport. The closure must produce a [`Vector`] representing - /// the translation to be applied. - /// - /// Translating can be useful to ensure images stay visible inside the viewport. - pub fn translate( - mut self, - translate: impl Fn(Rectangle, Rectangle) -> Vector + 'a, - ) -> Self { - self.translate = Some(Box::new(translate)); - self - } - - /// Sets the style of the [`Image`]. - #[must_use] - pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self - where - Theme::Class<'a>: From>, - { - self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); - self - } - - /// Sets the style class of the [`Image`]. - #[cfg(feature = "advanced")] - #[must_use] - pub fn class(mut self, class: impl Into>) -> Self { - self.class = class.into(); - self - } } /// Computes the layout of an [`Image`]. @@ -273,8 +226,6 @@ pub fn draw( rotation: Rotation, opacity: f32, scale: f32, - translate: Option<&dyn Fn(Rectangle, Rectangle) -> Vector>, - style: Style, ) where Renderer: image::Renderer, Handle: Clone, @@ -284,12 +235,6 @@ pub fn draw( drawing_bounds(renderer, bounds, handle, content_fit, rotation, scale); if must_clip(bounds, drawing_bounds) { - if translate.is_some_and(|translate| { - scale > 1.0 || translate(bounds, *viewport) != Vector::ZERO - }) { - return; - } - if let Some(bounds) = bounds.intersection(viewport) { renderer.with_layer(bounds, |renderer| { render( @@ -312,17 +257,6 @@ pub fn draw( drawing_bounds, ); } - - if style.shadow.color.a > 0.0 { - renderer.fill_quad( - renderer::Quad { - bounds: bounds.shrink(1.0), - shadow: style.shadow, - border: border::rounded(style.shadow_border_radius), - }, - style.shadow.color, - ); - } } fn render( @@ -349,11 +283,10 @@ fn render( } impl Widget - for Image<'_, Handle, Theme> + for Image where Renderer: image::Renderer, Handle: Clone, - Theme: Catalog, { fn size(&self) -> Size { Size { @@ -383,7 +316,7 @@ where &self, _state: &Tree, renderer: &mut Renderer, - theme: &Theme, + _theme: &Theme, _style: &renderer::Style, layout: Layout<'_>, _cursor: mouse::Cursor, @@ -399,183 +332,17 @@ where self.rotation, self.opacity, self.scale, - self.translate.as_deref(), - theme.style(&self.class), ); } - - fn overlay<'a>( - &'a mut self, - _state: &'a mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - viewport: &Rectangle, - translation: Vector, - ) -> Option> { - let translate = self.translate.as_ref()?; - let bounds = layout.bounds() + translation; - let drawing_bounds = drawing_bounds( - renderer, - bounds, - &self.handle, - self.content_fit, - self.rotation, - self.scale, - ); - - if must_clip(bounds, drawing_bounds) { - let translate = translate(bounds, *viewport); - - if self.scale <= 1.0 && translate == Vector::ZERO { - return None; - } - - Some(overlay::Element::new(Box::new(Overlay { - image: self, - viewport: *viewport, - clip_bounds: bounds + translate, - drawing_bounds: drawing_bounds + translate, - }))) - } else { - None - } - } } -impl<'a, Message, Theme, Renderer, Handle> From> +impl<'a, Message, Theme, Renderer, Handle> From> for Element<'a, Message, Theme, Renderer> where Renderer: image::Renderer, Handle: Clone + 'a, - Theme: Catalog + 'a, { - fn from( - image: Image<'a, Handle, Theme>, - ) -> Element<'a, Message, Theme, Renderer> { + fn from(image: Image) -> Element<'a, Message, Theme, Renderer> { Element::new(image) } } - -/// The theme catalog of an [`Image`]. -/// -/// All themes that can be used with [`Image`] -/// must implement this trait. -pub trait Catalog { - /// The item class of the [`Catalog`]. - type Class<'a>; - - /// The default class produced by the [`Catalog`]. - fn default<'a>() -> Self::Class<'a>; - - /// The [`Style`] of a class with the given status. - fn style(&self, class: &Self::Class<'_>) -> Style; -} - -/// A styling function for an [`Image`]. -pub type StyleFn<'a, Theme> = Box Style + 'a>; - -impl Catalog for crate::Theme { - type Class<'a> = StyleFn<'a, Self>; - - fn default<'a>() -> Self::Class<'a> { - Box::new(|_| Style::default()) - } - - fn style(&self, class: &Self::Class<'_>) -> Style { - class(self) - } -} - -/// The style of an [`Image`]. -#[derive(Debug, Clone, Copy, PartialEq, Default)] -pub struct Style { - /// The [`Shadow`] of the [`Image`]. - pub shadow: Shadow, - /// The border radius of the shadow. - pub shadow_border_radius: border::Radius, -} - -struct Overlay<'a, 'b, Handle, Theme> -where - Theme: Catalog, -{ - image: &'a Image<'b, Handle, Theme>, - viewport: Rectangle, - clip_bounds: Rectangle, - drawing_bounds: Rectangle, -} - -impl core::Overlay - for Overlay<'_, '_, Handle, Theme> -where - Renderer: image::Renderer, - Handle: Clone, - Theme: Catalog, -{ - fn layout(&mut self, _renderer: &Renderer, _bounds: Size) -> layout::Node { - layout::Node::new(self.clip_bounds.size()) - .move_to(self.clip_bounds.position()) - } - - fn is_over( - &self, - _layout: Layout<'_>, - _renderer: &Renderer, - _cursor_position: Point, - ) -> bool { - false - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &Theme, - _style: &renderer::Style, - layout: Layout<'_>, - _cursor: mouse::Cursor, - ) { - let bounds = layout.bounds(); - let clip_bounds = bounds.zoom(self.image.scale); - - let Some(clip_bounds) = clip_bounds.intersection(&self.viewport) else { - return; - }; - - let style = theme.style(&self.image.class); - - if style.shadow.color.a > 0.0 { - renderer.with_layer( - clip_bounds.expand(style.shadow.blur_radius), - |renderer| { - renderer.fill_quad( - renderer::Quad { - bounds: self - .drawing_bounds - .intersection(&clip_bounds) - .unwrap_or(self.drawing_bounds) - .shrink(1.0), - shadow: style.shadow, - border: border::rounded(style.shadow_border_radius), - }, - style.shadow.color, - ); - }, - ); - } - - renderer.with_layer(clip_bounds, |renderer| { - render( - renderer, - &self.image.handle, - self.image.filter_method, - self.image.rotation, - self.image.opacity, - self.drawing_bounds, - ); - }); - } - - fn index(&self) -> f32 { - self.image.scale * 0.5 - } -} diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs index ed9b5dbf..a774c239 100644 --- a/widget/src/keyed/column.rs +++ b/widget/src/keyed/column.rs @@ -367,7 +367,7 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, viewport: &Rectangle, translation: Vector, diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index 82fab287..eb8ccb12 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -265,7 +265,7 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, viewport: &Rectangle, translation: Vector, @@ -281,10 +281,11 @@ where .take() .unwrap(), tree: &mut tree.children[0], - overlay_builder: |element, tree| { + layout, + overlay_builder: |element, tree, layout| { element .as_widget_mut() - .overlay(tree, layout, renderer, viewport, translation) + .overlay(tree, *layout, renderer, viewport, translation) .map(|overlay| RefCell::new(Nested::new(overlay))) }, } @@ -312,8 +313,9 @@ struct Inner<'a, Message: 'a, Theme: 'a, Renderer: 'a> { cell: Rc>>>, element: Element<'static, Message, Theme, Renderer>, tree: &'a mut Tree, + layout: Layout<'a>, - #[borrows(mut element, mut tree)] + #[borrows(mut element, mut tree, layout)] #[not_covariant] overlay: Option>>, } diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index d6984109..4b9833c3 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -445,7 +445,7 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, viewport: &Rectangle, translation: Vector, diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index ca5825e3..f02c119e 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -50,6 +50,7 @@ where content: RefCell::new(Content { size: Size::ZERO, layout: None, + is_layout_invalid: true, element: Element::new(horizontal_space().width(0)), }), } @@ -59,6 +60,7 @@ where struct Content<'a, Message, Theme, Renderer> { size: Size, layout: Option, + is_layout_invalid: bool, element: Element<'a, Message, Theme, Renderer>, } @@ -67,12 +69,13 @@ where Renderer: core::Renderer, { fn layout(&mut self, tree: &mut Tree, renderer: &Renderer) { - if self.layout.is_none() { + if self.layout.is_none() || self.is_layout_invalid { self.layout = Some(self.element.as_widget().layout( tree, renderer, &layout::Limits::new(Size::ZERO, self.size), )); + self.is_layout_invalid = false; } } @@ -281,7 +284,7 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, viewport: &Rectangle, translation: Vector, @@ -305,6 +308,7 @@ where let Content { element, layout: content_layout_node, + is_layout_invalid, .. } = content.deref_mut(); @@ -324,7 +328,7 @@ where translation, ) .map(|overlay| RefCell::new(Nested::new(overlay))), - content_layout_node, + is_layout_invalid, ) }, } @@ -361,7 +365,7 @@ struct Overlay<'a, 'b, Message, Theme, Renderer> { #[not_covariant] overlay: ( Option>>, - &'this mut Option, + &'this mut bool, ), } @@ -440,7 +444,7 @@ where if is_layout_invalid { self.with_overlay_mut(|(_overlay, layout)| { - **layout = None; + **layout = true; }); } } diff --git a/widget/src/lib.rs b/widget/src/lib.rs index dcaea007..f39f081a 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -20,6 +20,7 @@ pub mod button; pub mod checkbox; pub mod combo_box; pub mod container; +pub mod float; pub mod grid; pub mod keyed; pub mod overlay; @@ -60,6 +61,8 @@ pub use combo_box::ComboBox; #[doc(no_inline)] pub use container::Container; #[doc(no_inline)] +pub use float::Float; +#[doc(no_inline)] pub use grid::Grid; #[doc(no_inline)] pub use mouse_area::MouseArea; diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index 54387e2d..6367698a 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -294,7 +294,7 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, viewport: &Rectangle, translation: Vector, diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 743eec0a..8ab296e8 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -977,7 +977,7 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, viewport: &Rectangle, translation: Vector, diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs index 8a235be7..8800d13b 100644 --- a/widget/src/pane_grid/content.rs +++ b/widget/src/pane_grid/content.rs @@ -364,7 +364,7 @@ where pub(crate) fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, viewport: &Rectangle, translation: Vector, diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs index 714f7f47..5cd53b50 100644 --- a/widget/src/pane_grid/title_bar.rs +++ b/widget/src/pane_grid/title_bar.rs @@ -568,7 +568,7 @@ where pub(crate) fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, viewport: &Rectangle, translation: Vector, diff --git a/widget/src/pin.rs b/widget/src/pin.rs index 2b663d1e..539bf2c1 100644 --- a/widget/src/pin.rs +++ b/widget/src/pin.rs @@ -242,7 +242,7 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut widget::Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, viewport: &Rectangle, translation: Vector, diff --git a/widget/src/pop.rs b/widget/src/pop.rs index 75973e3c..a736b434 100644 --- a/widget/src/pop.rs +++ b/widget/src/pop.rs @@ -293,7 +293,7 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: core::Layout<'_>, + layout: core::Layout<'b>, renderer: &Renderer, viewport: &Rectangle, translation: core::Vector, diff --git a/widget/src/row.rs b/widget/src/row.rs index 1de29e17..a51106d1 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -335,7 +335,7 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, viewport: &Rectangle, translation: Vector, @@ -546,7 +546,7 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, viewport: &Rectangle, translation: Vector, diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 9d704d40..72019137 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -1178,7 +1178,7 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, viewport: &Rectangle, translation: Vector, diff --git a/widget/src/stack.rs b/widget/src/stack.rs index dda9c357..5e10ff2c 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -352,7 +352,7 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, viewport: &Rectangle, translation: Vector, diff --git a/widget/src/themer.rs b/widget/src/themer.rs index 3d58db61..82b65486 100644 --- a/widget/src/themer.rs +++ b/widget/src/themer.rs @@ -178,7 +178,7 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, viewport: &Rectangle, translation: Vector, diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs index 4223c8b6..ab6b8f14 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -271,7 +271,7 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut widget::Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, viewport: &Rectangle, translation: Vector, From 613c706a39553c78a496bcb81a221c6b3a1f20bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 29 Apr 2025 23:01:45 +0200 Subject: [PATCH 2/9] Report layers rendered to `iced_beacon` --- beacon/src/client.rs | 1 + beacon/src/lib.rs | 9 ++++++++- beacon/src/span.rs | 3 ++- debug/src/lib.rs | 16 ++++++++++++---- wgpu/src/layer.rs | 10 ++++++++++ wgpu/src/lib.rs | 12 ++++++++++++ 6 files changed, 45 insertions(+), 6 deletions(-) diff --git a/beacon/src/client.rs b/beacon/src/client.rs index 85f44eb8..3850ca63 100644 --- a/beacon/src/client.rs +++ b/beacon/src/client.rs @@ -48,6 +48,7 @@ pub enum Event { MessageLogged { number: usize, message: String }, CommandsSpawned(usize), SubscriptionsTracked(usize), + LayersRendered(usize), } impl Client { diff --git a/beacon/src/lib.rs b/beacon/src/lib.rs index a5291e32..50af8b04 100644 --- a/beacon/src/lib.rs +++ b/beacon/src/lib.rs @@ -127,6 +127,7 @@ pub fn run() -> impl Stream { let mut last_tasks = 0; let mut last_subscriptions = 0; let mut last_present_window = None; + let mut last_present_layers = 0; drop(task::spawn(async move { let mut last_message_number = None; @@ -199,6 +200,9 @@ pub fn run() -> impl Stream { ) => { last_tasks = commands; } + client::Event::LayersRendered(layers) => { + last_present_layers = layers; + } client::Event::SpanStarted( span::Stage::Update, ) => { @@ -264,7 +268,10 @@ pub fn run() -> impl Stream { } } span::Stage::Present(window) => { - Span::Present { window } + Span::Present { + window, + layers: last_present_layers, + } } span::Stage::Custom(name) => { Span::Custom { name } diff --git a/beacon/src/span.rs b/beacon/src/span.rs index ff869e1e..8a266acc 100644 --- a/beacon/src/span.rs +++ b/beacon/src/span.rs @@ -33,6 +33,7 @@ pub enum Span { }, Present { window: window::Id, + layers: usize, }, Custom { name: String, @@ -70,7 +71,7 @@ impl Span { Span::Draw { window } => Stage::Draw(*window), Span::Prepare { primitive, .. } => Stage::Prepare(*primitive), Span::Render { primitive, .. } => Stage::Render(*primitive), - Span::Present { window } => Stage::Present(*window), + Span::Present { window, .. } => Stage::Present(*window), Span::Custom { name, .. } => Stage::Custom(name.clone()), } } diff --git a/debug/src/lib.rs b/debug/src/lib.rs index 83a424d2..6090cfbf 100644 --- a/debug/src/lib.rs +++ b/debug/src/lib.rs @@ -43,11 +43,15 @@ pub fn theme_changed(f: impl FnOnce() -> Option) { } pub fn tasks_spawned(amount: usize) { - internal::tasks_spawned(amount) + internal::tasks_spawned(amount); } pub fn subscriptions_tracked(amount: usize) { - internal::subscriptions_tracked(amount) + internal::subscriptions_tracked(amount); +} + +pub fn layers_rendered(amount: impl FnOnce() -> usize) { + internal::layers_rendered(amount); } pub fn boot() -> Span { @@ -157,6 +161,10 @@ mod internal { log(client::Event::SubscriptionsTracked(amount)); } + pub fn layers_rendered(amount: impl FnOnce() -> usize) { + log(client::Event::LayersRendered(amount())); + } + pub fn boot() -> Span { span(span::Stage::Boot) } @@ -300,8 +308,6 @@ mod internal { use crate::futures::Subscription; use crate::{Command, Primitive}; - use std::io; - pub fn enable() {} pub fn disable() {} @@ -317,6 +323,8 @@ mod internal { pub fn subscriptions_tracked(_amount: usize) {} + pub fn layers_rendered(_amount: impl FnOnce() -> usize) {} + pub fn boot() -> Span { Span } diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index 5e307ca7..8003d8f6 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -27,6 +27,16 @@ pub struct Layer { } impl Layer { + pub fn is_empty(&self) -> bool { + self.quads.is_empty() + && self.triangles.is_empty() + && self.primitives.is_empty() + && self.images.is_empty() + && self.text.is_empty() + && self.pending_meshes.is_empty() + && self.pending_text.is_empty() + } + pub fn draw_quad( &mut self, quad: renderer::Quad, diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 94ab81c4..7f74905b 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -604,6 +604,18 @@ impl Renderer { } let _ = ManuallyDrop::into_inner(render_pass); + + debug::layers_rendered(|| { + self.layers + .iter() + .filter(|layer| { + !layer.is_empty() + && physical_bounds + .intersection(&(layer.bounds * scale_factor)) + .is_some_and(|viewport| viewport.snap().is_some()) + }) + .count() + }); } } From 5fd2ae070e6d858c974396a27612ecf7dc15d5c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 29 Apr 2025 23:03:14 +0200 Subject: [PATCH 3/9] Update `comet` to display layers rendered --- devtools/src/comet.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devtools/src/comet.rs b/devtools/src/comet.rs index 2372899f..c534d6ba 100644 --- a/devtools/src/comet.rs +++ b/devtools/src/comet.rs @@ -4,7 +4,7 @@ use crate::runtime::Task; use std::process; pub const COMPATIBLE_REVISION: &str = - "69dd2283886dccdaa1ee6e1c274af62f7250bc38"; + "63f30c779a72315598255703f35af44f8ec3e583"; pub fn launch() -> Task { executor::try_spawn_blocking(|mut sender| { From 6ebf38624937c6cff843ada185063b077e1e19df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 30 Apr 2025 01:30:53 +0200 Subject: [PATCH 4/9] Fix missing `is_empty` in `image::Batch` --- wgpu/src/image/null.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/wgpu/src/image/null.rs b/wgpu/src/image/null.rs index c06d56be..cfcd53fc 100644 --- a/wgpu/src/image/null.rs +++ b/wgpu/src/image/null.rs @@ -7,4 +7,8 @@ impl Batch { pub fn push(&mut self, _image: Image) {} pub fn clear(&mut self) {} + + pub fn is_empty(&self) -> bool { + true + } } From 97b4ed0d84f23dd66c6f259320a0a6db81b89a08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 30 Apr 2025 04:19:15 +0200 Subject: [PATCH 5/9] Support `max_width` and `text::Alignment` for `canvas::Text` --- benches/wgpu.rs | 3 +- examples/clock/src/main.rs | 8 ++-- examples/color_palette/src/main.rs | 2 +- examples/game_of_life/src/main.rs | 3 +- examples/vectorial_text/src/main.rs | 2 +- graphics/src/geometry/text.rs | 74 +++++++++++++---------------- graphics/src/text.rs | 41 ++++++++++++++++ graphics/src/text/cache.rs | 13 ++--- graphics/src/text/paragraph.rs | 52 ++++---------------- tiny_skia/src/geometry.rs | 28 ++++++----- tiny_skia/src/text.rs | 9 ++-- wgpu/src/geometry.rs | 29 ++++++----- wgpu/src/text.rs | 2 + 13 files changed, 139 insertions(+), 127 deletions(-) diff --git a/benches/wgpu.rs b/benches/wgpu.rs index 24d8212c..a39ebbcd 100644 --- a/benches/wgpu.rs +++ b/benches/wgpu.rs @@ -179,9 +179,10 @@ fn scene<'a, Message: 'a>(n: usize) -> Element<'a, Message, Theme, Renderer> { size: Pixels::from(16), line_height: text::LineHeight::default(), font: Font::DEFAULT, - align_x: alignment::Horizontal::Left, + align_x: text::Alignment::Left, align_y: alignment::Vertical::Top, shaping: text::Shaping::Basic, + max_width: f32::INFINITY, }); } })] diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index b7811653..89855bc2 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -2,7 +2,7 @@ use iced::alignment; use iced::mouse; use iced::time::{self, milliseconds}; use iced::widget::canvas::{Cache, Geometry, LineCap, Path, Stroke, stroke}; -use iced::widget::{canvas, container}; +use iced::widget::{canvas, container, text}; use iced::{ Degrees, Element, Fill, Font, Point, Radians, Rectangle, Renderer, Size, Subscription, Theme, Vector, @@ -148,9 +148,9 @@ impl canvas::Program for Clock { ), color: palette.secondary.strong.text, align_x: if rotate_factor > 0.0 { - alignment::Horizontal::Right + text::Alignment::Right } else { - alignment::Horizontal::Left + text::Alignment::Left }, align_y: alignment::Vertical::Bottom, font: Font::MONOSPACE, @@ -170,7 +170,7 @@ impl canvas::Program for Clock { size: (radius / 5.0).into(), position: Point::new(x * 0.82, y * 0.82), color: palette.secondary.strong.text, - align_x: alignment::Horizontal::Center, + align_x: text::Alignment::Center, align_y: alignment::Vertical::Center, font: Font::MONOSPACE, ..canvas::Text::default() diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs index e705c40d..10e72d3b 100644 --- a/examples/color_palette/src/main.rs +++ b/examples/color_palette/src/main.rs @@ -169,7 +169,7 @@ impl Theme { }); let mut text = canvas::Text { - align_x: alignment::Horizontal::Center, + align_x: text::Alignment::Center, align_y: alignment::Vertical::Top, size: Pixels(15.0), color: text_color, diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 8e0ecc45..8860759e 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -187,6 +187,7 @@ mod grid { use iced::widget::canvas::{ Cache, Canvas, Event, Frame, Geometry, Path, Text, }; + use iced::widget::text; use iced::{ Color, Element, Fill, Point, Rectangle, Renderer, Size, Theme, Vector, }; @@ -575,7 +576,7 @@ mod grid { color: Color::WHITE, size: 14.0.into(), position: Point::new(frame.width(), frame.height()), - align_x: alignment::Horizontal::Right, + align_x: text::Alignment::Right, align_y: alignment::Vertical::Bottom, ..Text::default() }; diff --git a/examples/vectorial_text/src/main.rs b/examples/vectorial_text/src/main.rs index 92eacca8..72525d55 100644 --- a/examples/vectorial_text/src/main.rs +++ b/examples/vectorial_text/src/main.rs @@ -141,7 +141,7 @@ impl canvas::Program for State { } else { "Vectorial Text! 🎉" }), - align_x: alignment::Horizontal::Center, + align_x: text::Alignment::Center, align_y: alignment::Vertical::Center, shaping: text::Shaping::Advanced, ..canvas::Text::default() diff --git a/graphics/src/geometry/text.rs b/graphics/src/geometry/text.rs index baab7e0f..a1c3d7fa 100644 --- a/graphics/src/geometry/text.rs +++ b/graphics/src/geometry/text.rs @@ -1,5 +1,6 @@ +use crate::core; use crate::core::alignment; -use crate::core::text::{LineHeight, Shaping}; +use crate::core::text::{Alignment, LineHeight, Paragraph, Shaping, Wrapping}; use crate::core::{Color, Font, Pixels, Point, Size, Vector}; use crate::geometry::Path; use crate::text; @@ -21,6 +22,10 @@ pub struct Text { /// For example, when the horizontal_alignment and vertical_alignment are set to Center, the /// center of the text will be placed at the given position NOT the top-left coordinate. pub position: Point, + /// The maximum horizontal space available for this [`Text`]. + /// + /// Text will break into new lines when the width is reached. + pub max_width: f32, /// The color of the text pub color: Color, /// The size of the text @@ -30,7 +35,7 @@ pub struct Text { /// The font of the text pub font: Font, /// The horizontal alignment of the text - pub align_x: alignment::Horizontal, + pub align_x: Alignment, /// The vertical alignment of the text pub align_y: alignment::Vertical, /// The shaping strategy of the text. @@ -41,62 +46,50 @@ impl Text { /// Computes the [`Path`]s of the [`Text`] and draws them using /// the given closure. pub fn draw_with(&self, mut f: impl FnMut(Path, Color)) { - let mut font_system = - text::font_system().write().expect("Write font system"); - - let mut buffer = cosmic_text::BufferLine::new( - &self.content, - cosmic_text::LineEnding::default(), - cosmic_text::AttrsList::new(&text::to_attributes(self.font)), - text::to_shaping(self.shaping), - ); - - let layout = buffer.layout( - font_system.raw(), - self.size.0, - None, - cosmic_text::Wrap::None, - None, - 4, - ); + let paragraph = text::Paragraph::with_text(core::text::Text { + content: &self.content, + bounds: Size::new(self.max_width, f32::INFINITY), + size: self.size, + line_height: self.line_height, + font: self.font, + align_x: self.align_x, + align_y: self.align_y, + shaping: self.shaping, + wrapping: Wrapping::default(), + }); let translation_x = match self.align_x { - alignment::Horizontal::Left => self.position.x, - alignment::Horizontal::Center | alignment::Horizontal::Right => { - let mut line_width = 0.0f32; - - for line in layout.iter() { - line_width = line_width.max(line.w); - } - - if self.align_x == alignment::Horizontal::Center { - self.position.x - line_width / 2.0 - } else { - self.position.x - line_width - } + Alignment::Default | Alignment::Left | Alignment::Justified => { + self.position.x } + Alignment::Center => self.position.x - paragraph.min_width() / 2.0, + Alignment::Right => self.position.x - paragraph.min_width(), }; let translation_y = { - let line_height = self.line_height.to_absolute(self.size); - match self.align_y { alignment::Vertical::Top => self.position.y, alignment::Vertical::Center => { - self.position.y - line_height.0 / 2.0 + self.position.y - paragraph.min_height() / 2.0 + } + alignment::Vertical::Bottom => { + self.position.y - paragraph.min_height() } - alignment::Vertical::Bottom => self.position.y - line_height.0, } }; + let buffer = paragraph.buffer(); let mut swash_cache = cosmic_text::SwashCache::new(); - for run in layout.iter() { + let mut font_system = + text::font_system().write().expect("Write font system"); + + for run in buffer.layout_runs() { for glyph in run.glyphs.iter() { let physical_glyph = glyph.physical((0.0, 0.0), 1.0); let start_x = translation_x + glyph.x + glyph.x_offset; - let start_y = translation_y + glyph.y_offset + self.size.0; + let start_y = translation_y + glyph.y_offset + run.line_y; let offset = Vector::new(start_x, start_y); if let Some(commands) = swash_cache.get_outline_commands( @@ -176,11 +169,12 @@ impl Default for Text { Text { content: String::new(), position: Point::ORIGIN, + max_width: f32::INFINITY, color: Color::BLACK, size: Pixels(16.0), line_height: LineHeight::Relative(1.2), font: Font::default(), - align_x: alignment::Horizontal::Left, + align_x: Alignment::Default, align_y: alignment::Vertical::Top, shaping: Shaping::Basic, } diff --git a/graphics/src/text.rs b/graphics/src/text.rs index 507ef178..14262076 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -257,6 +257,47 @@ pub fn measure(buffer: &cosmic_text::Buffer) -> (Size, bool) { (Size::new(width, height), has_rtl) } +/// Aligns the given [`cosmic_text::Buffer`] with the given [`Alignment`] +/// and returns its minimum [`Size`]. +pub fn align( + buffer: &mut cosmic_text::Buffer, + font_system: &mut cosmic_text::FontSystem, + alignment: Alignment, +) -> Size { + let (min_bounds, has_rtl) = measure(buffer); + let mut needs_relayout = has_rtl; + + if let Some(align) = to_align(alignment) { + let has_multiple_lines = buffer.lines.len() > 1 + || buffer.lines.first().is_some_and(|line| { + line.layout_opt().is_some_and(|layout| layout.len() > 1) + }); + + if has_multiple_lines { + for line in &mut buffer.lines { + let _ = line.set_align(Some(align)); + } + + needs_relayout = true; + } else if let Some(line) = buffer.lines.first_mut() { + needs_relayout = line.set_align(None); + } + } + + // TODO: Avoid relayout with some changes to `cosmic-text` (?) + if needs_relayout { + log::trace!("Relayouting paragraph..."); + + buffer.set_size( + font_system, + Some(min_bounds.width), + Some(min_bounds.height), + ); + } + + min_bounds +} + /// Returns the attributes of the given [`Font`]. pub fn to_attributes(font: Font) -> cosmic_text::Attrs<'static> { cosmic_text::Attrs::new() diff --git a/graphics/src/text/cache.rs b/graphics/src/text/cache.rs index 5de2a2fb..9bb66362 100644 --- a/graphics/src/text/cache.rs +++ b/graphics/src/text/cache.rs @@ -58,15 +58,7 @@ impl Cache { text::to_shaping(key.shaping), ); - let (bounds, has_rtl) = text::measure(&buffer); - - if has_rtl { - buffer.set_size( - font_system, - Some(bounds.width), - Some(bounds.height), - ); - } + let bounds = text::align(&mut buffer, font_system, key.align_x); let _ = entry.insert(Entry { buffer, @@ -123,6 +115,8 @@ pub struct Key<'a> { pub bounds: Size, /// The shaping strategy of the text. pub shaping: text::Shaping, + /// The alignment of the text. + pub align_x: text::Alignment, } impl Key<'_> { @@ -134,6 +128,7 @@ impl Key<'_> { self.bounds.width.to_bits().hash(&mut hasher); self.bounds.height.to_bits().hash(&mut hasher); self.shaping.hash(&mut hasher); + self.align_x.hash(&mut hasher); hasher.finish() } diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs index 9cda9cb0..6b8e7167 100644 --- a/graphics/src/text/paragraph.rs +++ b/graphics/src/text/paragraph.rs @@ -89,7 +89,8 @@ impl core::text::Paragraph for Paragraph { text::to_shaping(text.shaping), ); - let min_bounds = align(&mut buffer, &mut font_system, text.align_x); + let min_bounds = + text::align(&mut buffer, font_system.raw(), text.align_x); Self(Arc::new(Internal { buffer, @@ -159,7 +160,8 @@ impl core::text::Paragraph for Paragraph { None, ); - let min_bounds = align(&mut buffer, &mut font_system, text.align_x); + let min_bounds = + text::align(&mut buffer, font_system.raw(), text.align_x); Self(Arc::new(Internal { buffer, @@ -186,8 +188,11 @@ impl core::text::Paragraph for Paragraph { Some(new_bounds.height), ); - let min_bounds = - align(&mut paragraph.buffer, &mut font_system, paragraph.align_x); + let min_bounds = text::align( + &mut paragraph.buffer, + font_system.raw(), + paragraph.align_x, + ); paragraph.bounds = new_bounds; paragraph.min_bounds = min_bounds; @@ -357,45 +362,6 @@ impl core::text::Paragraph for Paragraph { } } -fn align( - buffer: &mut cosmic_text::Buffer, - font_system: &mut text::FontSystem, - alignment: Alignment, -) -> Size { - let (min_bounds, has_rtl) = text::measure(buffer); - let mut needs_relayout = has_rtl; - - if let Some(align) = text::to_align(alignment) { - let has_multiple_lines = buffer.lines.len() > 1 - || buffer.lines.first().is_some_and(|line| { - line.layout_opt().is_some_and(|layout| layout.len() > 1) - }); - - if has_multiple_lines { - for line in &mut buffer.lines { - let _ = line.set_align(Some(align)); - } - - needs_relayout = true; - } else if let Some(line) = buffer.lines.first_mut() { - needs_relayout = line.set_align(None); - } - } - - // TODO: Avoid relayout with some changes to `cosmic-text` (?) - if needs_relayout { - log::trace!("Relayouting paragraph..."); - - buffer.set_size( - font_system.raw(), - Some(min_bounds.width), - Some(min_bounds.height), - ); - } - - min_bounds -} - impl Default for Paragraph { fn default() -> Self { Self(Arc::new(Internal::default())) diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs index e089bbb1..11367b7c 100644 --- a/tiny_skia/src/geometry.rs +++ b/tiny_skia/src/geometry.rs @@ -187,9 +187,15 @@ impl geometry::frame::Backend for Frame { && scale_x > 0.0 && scale_y > 0.0 { - let (position, size, line_height) = if self.transform.is_identity() - { - (text.position, text.size, text.line_height) + let (bounds, size, line_height) = if self.transform.is_identity() { + ( + Rectangle::new( + text.position, + Size::new(text.max_width, f32::INFINITY), + ), + text.size, + text.line_height, + ) } else { let mut position = [tiny_skia::Point { x: text.position.x, @@ -210,19 +216,17 @@ impl geometry::frame::Backend for Frame { }; ( - Point::new(position[0].x, position[0].y), + Rectangle { + x: position[0].x, + y: position[0].y, + width: text.max_width * scale_x, + height: f32::INFINITY, + }, size.into(), line_height, ) }; - let bounds = Rectangle { - x: position.x, - y: position.y, - width: f32::INFINITY, - height: f32::INFINITY, - }; - // TODO: Honor layering! self.text.push(Text::Cached { content: text.content, @@ -231,7 +235,7 @@ impl geometry::frame::Backend for Frame { size, line_height: line_height.to_absolute(size), font: text.font, - align_x: text.align_x.into(), + align_x: text.align_x, align_y: text.align_y, shaping: text.shaping, clip_bounds: Rectangle::with_size(Size::INFINITY), diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index 9f55458d..1ef825ae 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -108,8 +108,8 @@ impl Pipeline { size: Pixels, line_height: Pixels, font: Font, - horizontal_alignment: Alignment, - vertical_alignment: alignment::Vertical, + align_x: Alignment, + align_y: alignment::Vertical, shaping: Shaping, pixels: &mut tiny_skia::PixmapMut<'_>, clip_mask: Option<&tiny_skia::Mask>, @@ -127,6 +127,7 @@ impl Pipeline { size: size.into(), line_height, shaping, + align_x, }; let (_, entry) = self.cache.get_mut().allocate(font_system, key); @@ -144,8 +145,8 @@ impl Pipeline { ..bounds }, color, - horizontal_alignment, - vertical_alignment, + align_x, + align_y, pixels, clip_mask, transformation, diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index 881eb6be..da98e479 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -301,9 +301,16 @@ impl geometry::frame::Backend for Frame { && scale_x > 0.0 && scale_y > 0.0 { - let (position, size, line_height) = + let (bounds, size, line_height) = if self.transforms.current.is_identity() { - (text.position, text.size, text.line_height) + ( + Rectangle::new( + text.position, + Size::new(text.max_width, f32::INFINITY), + ), + text.size, + text.line_height, + ) } else { let position = self.transforms.current.transform_point(text.position); @@ -319,16 +326,16 @@ impl geometry::frame::Backend for Frame { } }; - (position, size, line_height) + ( + Rectangle::new( + position, + Size::new(text.max_width, f32::INFINITY), + ), + size, + line_height, + ) }; - let bounds = Rectangle { - x: position.x, - y: position.y, - width: f32::INFINITY, - height: f32::INFINITY, - }; - self.text.push(Text::Cached { content: text.content, bounds, @@ -336,7 +343,7 @@ impl geometry::frame::Backend for Frame { size, line_height: line_height.to_absolute(size), font: text.font, - align_x: text.align_x.into(), + align_x: text.align_x, align_y: text.align_y, shaping: text.shaping, clip_bounds: self.clip_bounds, diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 2a4c6161..b3b2ee68 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -479,6 +479,7 @@ fn prepare( line_height, font, shaping, + align_x, .. } => { let (key, _) = buffer_cache.allocate( @@ -488,6 +489,7 @@ fn prepare( size: f32::from(*size), line_height: f32::from(*line_height), font: *font, + align_x: *align_x, bounds: Size { width: bounds.width, height: bounds.height, From f8f2e55f193cd32c11966709fea2876202893e47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 30 Apr 2025 23:35:31 +0200 Subject: [PATCH 6/9] Implement `float` widget interactivity --- examples/gallery/src/main.rs | 1 + widget/src/float.rs | 112 +++++++++++++++++++++++++++++------ 2 files changed, 94 insertions(+), 19 deletions(-) diff --git a/examples/gallery/src/main.rs b/examples/gallery/src/main.rs index a40df370..a1a0edf1 100644 --- a/examples/gallery/src/main.rs +++ b/examples/gallery/src/main.rs @@ -210,6 +210,7 @@ fn card<'a>( .content_fit(ContentFit::Cover) .opacity(thumbnail.fade_in.interpolate(0.0, 1.0, now)), ) + .opaque(false) .scale(thumbnail.zoom.interpolate(1.0, 1.1, now)) .translate(move |bounds, viewport| { bounds.zoom(1.1).offset(&viewport.shrink(10)) diff --git a/widget/src/float.rs b/widget/src/float.rs index a5f893d9..aa601d28 100644 --- a/widget/src/float.rs +++ b/widget/src/float.rs @@ -35,7 +35,7 @@ where content: content.into(), scale: 1.0, translate: None, - opaque: false, + opaque: true, class: Theme::default(), } } @@ -134,6 +134,10 @@ where shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { + if self.is_floating(layout.bounds(), *viewport) { + return; + } + self.content.as_widget_mut().update( state, event, layout, cursor, renderer, clipboard, shell, viewport, ); @@ -181,6 +185,10 @@ where viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { + if self.is_floating(layout.bounds(), *viewport) { + return mouse::Interaction::None; + } + self.content .as_widget() .mouse_interaction(state, layout, cursor, viewport, renderer) @@ -215,13 +223,23 @@ where .unwrap_or(Vector::ZERO); if self.scale > 1.0 || translation != Vector::ZERO { + let translation = translation + offset; + + let transformation = Transformation::translate( + bounds.x + bounds.width / 2.0 + translation.x, + bounds.y + bounds.height / 2.0 + translation.y, + ) * Transformation::scale(self.scale) + * Transformation::translate( + -bounds.x - bounds.width / 2.0, + -bounds.y - bounds.height / 2.0, + ); + Some(overlay::Element::new(Box::new(Overlay { float: self, state, layout, viewport: *viewport, - bounds, - translation: translation + offset, + transformation, }))) } else { self.content @@ -247,12 +265,11 @@ struct Overlay<'a, 'b, Message, Theme, Renderer> where Theme: Catalog, { - float: &'a Float<'b, Message, Theme, Renderer>, + float: &'a mut Float<'b, Message, Theme, Renderer>, state: &'a mut widget::Tree, layout: Layout<'a>, viewport: Rectangle, - bounds: Rectangle, - translation: Vector, + transformation: Transformation, } impl core::Overlay @@ -262,7 +279,9 @@ where Renderer: core::Renderer, { fn layout(&mut self, _renderer: &Renderer, _bounds: Size) -> layout::Node { - layout::Node::new(self.bounds.size()).move_to(self.bounds.position()) + let bounds = self.layout.bounds(); + + layout::Node::new(bounds.size()).move_to(bounds.position()) } fn is_over( @@ -271,7 +290,33 @@ where _renderer: &Renderer, cursor_position: Point, ) -> bool { - self.float.opaque && layout.bounds().contains(cursor_position) + self.float.opaque + && layout + .bounds() + .contains(cursor_position * self.transformation.inverse()) + } + + fn update( + &mut self, + event: &Event, + _layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) { + let cursor = cursor * self.transformation.inverse(); + + self.float.content.as_widget_mut().update( + self.state, + event, + self.layout, + cursor, + renderer, + clipboard, + shell, + &self.viewport, + ); } fn draw( @@ -284,17 +329,8 @@ where ) { let bounds = layout.bounds(); - let transformation = Transformation::translate( - bounds.x + bounds.width / 2.0 + self.translation.x, - bounds.y + bounds.height / 2.0 + self.translation.y, - ) * Transformation::scale(self.float.scale) - * Transformation::translate( - -bounds.x - bounds.width / 2.0, - -bounds.y - bounds.height / 2.0, - ); - renderer.with_layer(self.viewport, |renderer| { - renderer.with_transformation(transformation, |renderer| { + renderer.with_transformation(self.transformation, |renderer| { { let style = theme.style(&self.float.class); @@ -319,15 +355,53 @@ where style, self.layout, cursor, - &(self.viewport * transformation.inverse()), + &(self.viewport * self.transformation.inverse()), ); }); }); } + fn mouse_interaction( + &self, + _layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.float.content.as_widget().mouse_interaction( + self.state, + self.layout, + cursor * self.transformation.inverse(), + &self.viewport, + renderer, + ) + } + fn index(&self) -> f32 { self.float.scale * 0.5 } + + fn operate( + &mut self, + _layout: Layout<'_>, + _renderer: &Renderer, + _operation: &mut dyn widget::Operation, + ) { + } + + fn overlay<'a>( + &'a mut self, + _layout: Layout<'_>, + renderer: &Renderer, + ) -> Option> { + self.float.content.as_widget_mut().overlay( + self.state, + self.layout, + renderer, + &self.viewport, + self.transformation.translation(), + ) + } } /// The theme catalog of a [`Float`]. From aa27ab50e5b9cdd702981a2b0154e3bca1a09628 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 1 May 2025 01:13:10 +0200 Subject: [PATCH 7/9] Use `Self` in `core::transformation` --- core/src/transformation.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/core/src/transformation.rs b/core/src/transformation.rs index d9b805ae..09287e22 100644 --- a/core/src/transformation.rs +++ b/core/src/transformation.rs @@ -13,8 +13,8 @@ impl Transformation { /// Creates an orthographic projection. #[rustfmt::skip] - pub fn orthographic(width: u32, height: u32) -> Transformation { - Transformation(Mat4::orthographic_rh_gl( + pub fn orthographic(width: u32, height: u32) -> Self{ + Self(Mat4::orthographic_rh_gl( 0.0, width as f32, height as f32, 0.0, -1.0, 1.0 @@ -22,13 +22,18 @@ impl Transformation { } /// Creates a translate transformation. - pub fn translate(x: f32, y: f32) -> Transformation { - Transformation(Mat4::from_translation(Vec3::new(x, y, 0.0))) + pub fn translate(x: f32, y: f32) -> Self { + Self(Mat4::from_translation(Vec3::new(x, y, 0.0))) } /// Creates a uniform scaling transformation. - pub fn scale(scaling: f32) -> Transformation { - Transformation(Mat4::from_scale(Vec3::new(scaling, scaling, 1.0))) + pub fn scale(scaling: f32) -> Self { + Self(Mat4::from_scale(Vec3::new(scaling, scaling, 1.0))) + } + + /// Returns the inverse of the [`Transformation`]. + pub fn inverse(self) -> Self { + Self(self.0.inverse()) } /// Returns the scale factor of the [`Transformation`]. @@ -40,11 +45,6 @@ impl Transformation { pub fn translation(&self) -> Vector { Vector::new(self.0.w_axis.x, self.0.w_axis.y) } - - /// Returns the inverse of the [`Transformation`]. - pub fn inverse(self) -> Self { - Transformation(self.0.inverse()) - } } impl Default for Transformation { @@ -57,7 +57,7 @@ impl Mul for Transformation { type Output = Self; fn mul(self, rhs: Self) -> Self { - Transformation(self.0 * rhs.0) + Self(self.0 * rhs.0) } } From 8f3bca299be59afac3921a4f8fb76fe66184cad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 1 May 2025 01:33:43 +0200 Subject: [PATCH 8/9] Write some docs for the new `float` widget --- widget/src/float.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/widget/src/float.rs b/widget/src/float.rs index aa601d28..aa6ae5f7 100644 --- a/widget/src/float.rs +++ b/widget/src/float.rs @@ -1,4 +1,4 @@ -#![allow(missing_docs)] +//! Make elements float! use crate::core; use crate::core::border; use crate::core::layout; @@ -12,6 +12,7 @@ use crate::core::{ Size, Transformation, Vector, Widget, }; +/// A widget that can make its contents float over other widgets. #[allow(missing_debug_implementations)] pub struct Float<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> where @@ -28,6 +29,7 @@ impl<'a, Message, Theme, Renderer> Float<'a, Message, Theme, Renderer> where Theme: Catalog, { + /// Creates a new [`Float`] widget with the given content. pub fn new( content: impl Into>, ) -> Self { @@ -40,11 +42,17 @@ where } } + /// Sets the scale to be applied to the contents of the [`Float`]. pub fn scale(mut self, scale: f32) -> Self { self.scale = scale; self } + /// Sets the translation logic to be applied to the contents of the [`Float`]. + /// + /// The logic takes the original (non-scaled) bounds of the contents and the + /// viewport bounds. These bounds can be useful to ensure the floating elements + /// always stay on screen. pub fn translate( mut self, translate: impl Fn(Rectangle, Rectangle) -> Vector + 'a, @@ -53,6 +61,12 @@ where self } + /// Sets whether the [`Float`] contents should be opaque when floating. + /// + /// Disabling opacity will make the mouse pass through the floating content, allowing + /// interaction with whatever is under it. + /// + /// By default, a [`Float`] widget is opaque. pub fn opaque(mut self, opaque: bool) -> Self { self.opaque = opaque; self From acde7ea735034d67f626ebbb93e5cbd2dcdf396e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 1 May 2025 02:59:59 +0200 Subject: [PATCH 9/9] Implement `Frame::stroke_text` in `canvas` API --- graphics/src/geometry/frame.rs | 11 +++++++++++ renderer/src/fallback.rs | 8 ++++++++ tiny_skia/src/geometry.rs | 11 +++++++++++ wgpu/src/geometry.rs | 11 +++++++++++ 4 files changed, 41 insertions(+) diff --git a/graphics/src/geometry/frame.rs b/graphics/src/geometry/frame.rs index 3dee7e75..29b534f8 100644 --- a/graphics/src/geometry/frame.rs +++ b/graphics/src/geometry/frame.rs @@ -217,6 +217,11 @@ pub trait Backend: Sized { size: Size, stroke: impl Into>, ); + fn stroke_text<'a>( + &mut self, + text: impl Into, + stroke: impl Into>, + ); fn fill(&mut self, path: &Path, fill: impl Into); fn fill_text(&mut self, text: impl Into); @@ -272,6 +277,12 @@ impl Backend for () { _stroke: impl Into>, ) { } + fn stroke_text<'a>( + &mut self, + _text: impl Into, + _stroke: impl Into>, + ) { + } fn fill(&mut self, _path: &Path, _fill: impl Into) {} fn fill_text(&mut self, _text: impl Into) {} diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index 7223f06f..79e22c77 100644 --- a/renderer/src/fallback.rs +++ b/renderer/src/fallback.rs @@ -537,6 +537,14 @@ mod geometry { ); } + fn stroke_text<'a>( + &mut self, + text: impl Into, + stroke: impl Into>, + ) { + delegate!(self, frame, frame.stroke_text(text, stroke)); + } + fn fill_text(&mut self, text: impl Into) { delegate!(self, frame, frame.fill_text(text)); } diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs index 11367b7c..af55f2a5 100644 --- a/tiny_skia/src/geometry.rs +++ b/tiny_skia/src/geometry.rs @@ -245,6 +245,17 @@ impl geometry::frame::Backend for Frame { } } + fn stroke_text<'a>( + &mut self, + text: impl Into, + stroke: impl Into>, + ) { + let text = text.into(); + let stroke = stroke.into(); + + text.draw_with(|path, _color| self.stroke(&path, stroke)); + } + fn push_transform(&mut self) { self.stack.push(self.transform); } diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index da98e479..c3c66464 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -291,6 +291,17 @@ impl geometry::frame::Backend for Frame { .expect("Stroke rectangle"); } + fn stroke_text<'a>( + &mut self, + text: impl Into, + stroke: impl Into>, + ) { + let text = text.into(); + let stroke = stroke.into(); + + text.draw_with(|glyph, _color| self.stroke(&glyph, stroke)); + } + fn fill_text(&mut self, text: impl Into) { let text = text.into();