From 50cc94d944ada88bf3d7fcd1d2741b7104b9b1d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 22 Apr 2025 19:41:00 +0200 Subject: [PATCH 01/13] Fix even height distribution logic in `grid` widget --- widget/src/grid.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/widget/src/grid.rs b/widget/src/grid.rs index 26e741db..62471cfe 100644 --- a/widget/src/grid.rs +++ b/widget/src/grid.rs @@ -202,9 +202,10 @@ where let cell_height = match self.height { Sizing::AspectRatio(ratio) => Some(cell_width / ratio), Sizing::EvenlyDistribute(Length::Shrink) => None, - Sizing::EvenlyDistribute(_) => { - Some(available.height / total_rows as f32) - } + Sizing::EvenlyDistribute(_) => Some( + (available.height - self.spacing * (total_rows - 1) as f32) + / total_rows as f32, + ), }; let cell_limits = layout::Limits::new( From 6034a0cf603fa7849f074c9dbe1b1399c385cef1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 24 Apr 2025 22:55:31 +0200 Subject: [PATCH 02/13] Fix incorrect default implementation of `Widget::diff` --- core/src/widget.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/src/widget.rs b/core/src/widget.rs index 3c9c50ab..04107482 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -96,7 +96,9 @@ where } /// Reconciles the [`Widget`] with the provided [`Tree`]. - fn diff(&self, _tree: &mut Tree) {} + fn diff(&self, tree: &mut Tree) { + tree.diff_children(&[] as &[&dyn Widget]); + } /// Applies an [`Operation`] to the [`Widget`]. fn operate( From f67785edb52f5398eae3a703e116871375198c2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 24 Apr 2025 22:58:17 +0200 Subject: [PATCH 03/13] Simplify `Widget::diff` default implementation --- core/src/widget.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/widget.rs b/core/src/widget.rs index 04107482..807b14ac 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -97,7 +97,7 @@ where /// Reconciles the [`Widget`] with the provided [`Tree`]. fn diff(&self, tree: &mut Tree) { - tree.diff_children(&[] as &[&dyn Widget]); + tree.children.clear(); } /// Applies an [`Operation`] to the [`Widget`]. From 0b6e0667ae3c9d5199b3f09570a2c353fd0b03da Mon Sep 17 00:00:00 2001 From: Juniper <67175453+Ultrasquid9@users.noreply.github.com> Date: Thu, 24 Apr 2025 17:20:19 -0400 Subject: [PATCH 04/13] Indent and Unindent Text Editor Actions --- core/src/text/editor.rs | 4 ++++ graphics/src/text/editor.rs | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index 6921c61c..bee5560d 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -106,6 +106,10 @@ pub enum Edit { Paste(Arc), /// Break the current line. Enter, + /// Indent the current line. + Indent, + /// Unindent the current line. + Unindent, /// Delete the previous character. Backspace, /// Delete the next character. diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index aaf46a6a..4afd7b12 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -377,6 +377,18 @@ impl editor::Editor for Editor { Edit::Paste(text) => { editor.insert_string(&text, None); } + Edit::Indent => { + editor.action( + font_system.raw(), + cosmic_text::Action::Indent, + ); + } + Edit::Unindent => { + editor.action( + font_system.raw(), + cosmic_text::Action::Unindent, + ); + } Edit::Enter => { editor.action( font_system.raw(), From 0231c152fcbbe11a82a897902d46673dcc057f99 Mon Sep 17 00:00:00 2001 From: Johann Tuffe Date: Fri, 25 Apr 2025 07:00:10 +0800 Subject: [PATCH 05/13] fix number of rows in grid when evenly distributed (#2896) * fix number of rows in grid when evenly distributed * use div_ceil --------- Co-authored-by: Johann Tuffe --- widget/src/grid.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/widget/src/grid.rs b/widget/src/grid.rs index 62471cfe..da827007 100644 --- a/widget/src/grid.rs +++ b/widget/src/grid.rs @@ -193,8 +193,6 @@ where Constraint::Amount(amount) => amount, }; - let total_rows = self.children.len() / cells_per_row; - let cell_width = (available.width - self.spacing * (cells_per_row - 1) as f32) / cells_per_row as f32; @@ -202,10 +200,13 @@ where let cell_height = match self.height { Sizing::AspectRatio(ratio) => Some(cell_width / ratio), Sizing::EvenlyDistribute(Length::Shrink) => None, - Sizing::EvenlyDistribute(_) => Some( - (available.height - self.spacing * (total_rows - 1) as f32) - / total_rows as f32, - ), + Sizing::EvenlyDistribute(_) => { + let total_rows = self.children.len().div_ceil(cells_per_row); + Some( + (available.height - self.spacing * (total_rows - 1) as f32) + / total_rows as f32, + ) + } }; let cell_limits = layout::Limits::new( @@ -213,7 +214,7 @@ where Size::new(cell_width, cell_height.unwrap_or(available.height)), ); - let mut nodes = Vec::new(); + let mut nodes = Vec::with_capacity(self.children.len()); let mut x = 0.0; let mut y = 0.0; let mut row_height = 0.0f32; From e64c58d03297d048b7bca9c65eec4e48e11c23e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 25 Apr 2025 11:20:16 +0200 Subject: [PATCH 06/13] Implement overlay ordering for `Image::float` --- core/src/overlay.rs | 10 + core/src/overlay/element.rs | 88 ++------- core/src/overlay/group.rs | 42 ++++- examples/gallery/src/main.rs | 17 +- runtime/src/overlay/nested.rs | 41 +++-- widget/src/helpers.rs | 7 +- widget/src/image.rs | 337 ++++++++++++++++++++++++++++++---- widget/src/themer.rs | 17 +- 8 files changed, 416 insertions(+), 143 deletions(-) diff --git a/core/src/overlay.rs b/core/src/overlay.rs index 94239152..49d4c39e 100644 --- a/core/src/overlay.rs +++ b/core/src/overlay.rs @@ -101,6 +101,16 @@ where ) -> Option> { None } + + /// The index of the overlay. + /// + /// Overlays with a higher index will be rendered on top of overlays with + /// a lower index. + /// + /// By default, it returns `1.0`. + fn index(&self) -> f32 { + 1.0 + } } /// Returns a [`Group`] of overlay [`Element`] children. diff --git a/core/src/overlay/element.rs b/core/src/overlay/element.rs index de6e73fd..f696d2d6 100644 --- a/core/src/overlay/element.rs +++ b/core/src/overlay/element.rs @@ -23,6 +23,18 @@ where Self { overlay } } + /// Returns a reference to the [`Overlay`] of the [`Element`], + pub fn as_overlay(&self) -> &dyn Overlay { + self.overlay.as_ref() + } + + /// Returns a mutable reference to the [`Overlay`] of the [`Element`], + pub fn as_overlay_mut( + &mut self, + ) -> &mut dyn Overlay { + self.overlay.as_mut() + } + /// Applies a transformation to the produced message of the [`Element`]. pub fn map( self, @@ -38,82 +50,6 @@ where overlay: Box::new(Map::new(self.overlay, f)), } } - - /// Computes the layout of the [`Element`] in the given bounds. - pub fn layout( - &mut self, - renderer: &Renderer, - bounds: Size, - ) -> layout::Node { - self.overlay.layout(renderer, bounds) - } - - /// Processes a runtime [`Event`]. - pub fn update( - &mut self, - event: &Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) { - self.overlay - .update(event, layout, cursor, renderer, clipboard, shell); - } - - /// Returns the current [`mouse::Interaction`] of the [`Element`]. - pub fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.overlay - .mouse_interaction(layout, cursor, viewport, renderer) - } - - /// Draws the [`Element`] and its children using the given [`Layout`]. - pub fn draw( - &self, - renderer: &mut Renderer, - theme: &Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - ) { - self.overlay.draw(renderer, theme, style, layout, cursor); - } - - /// Applies a [`widget::Operation`] to the [`Element`]. - pub fn operate( - &mut self, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn widget::Operation, - ) { - self.overlay.operate(layout, renderer, operation); - } - - /// Returns true if the cursor is over the [`Element`]. - pub fn is_over( - &self, - layout: Layout<'_>, - renderer: &Renderer, - cursor_position: Point, - ) -> bool { - self.overlay.is_over(layout, renderer, cursor_position) - } - - /// Returns the nested overlay of the [`Element`], if there is any. - pub fn overlay<'b>( - &'b mut self, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - self.overlay.overlay(layout, renderer) - } } struct Map<'a, A, B, Theme, Renderer> { diff --git a/core/src/overlay/group.rs b/core/src/overlay/group.rs index 970c1b0e..2e199441 100644 --- a/core/src/overlay/group.rs +++ b/core/src/overlay/group.rs @@ -25,8 +25,17 @@ where /// Creates a [`Group`] with the given elements. pub fn with_children( - children: Vec>, + mut children: Vec>, ) -> Self { + use std::cmp; + + children.sort_unstable_by(|a, b| { + a.as_overlay() + .index() + .partial_cmp(&b.as_overlay().index()) + .unwrap_or(cmp::Ordering::Equal) + }); + Group { children } } @@ -67,7 +76,7 @@ where bounds, self.children .iter_mut() - .map(|child| child.layout(renderer, bounds)) + .map(|child| child.as_overlay_mut().layout(renderer, bounds)) .collect(), ) } @@ -82,7 +91,9 @@ where shell: &mut Shell<'_, Message>, ) { for (child, layout) in self.children.iter_mut().zip(layout.children()) { - child.update(event, layout, cursor, renderer, clipboard, shell); + child + .as_overlay_mut() + .update(event, layout, cursor, renderer, clipboard, shell); } } @@ -95,7 +106,9 @@ where cursor: mouse::Cursor, ) { for (child, layout) in self.children.iter().zip(layout.children()) { - child.draw(renderer, theme, style, layout, cursor); + child + .as_overlay() + .draw(renderer, theme, style, layout, cursor); } } @@ -110,7 +123,9 @@ where .iter() .zip(layout.children()) .map(|(child, layout)| { - child.mouse_interaction(layout, cursor, viewport, renderer) + child + .as_overlay() + .mouse_interaction(layout, cursor, viewport, renderer) }) .max() .unwrap_or_default() @@ -125,7 +140,7 @@ where operation.container(None, layout.bounds(), &mut |operation| { self.children.iter_mut().zip(layout.children()).for_each( |(child, layout)| { - child.operate(layout, renderer, operation); + child.as_overlay_mut().operate(layout, renderer, operation); }, ); }); @@ -141,7 +156,9 @@ where .iter() .zip(layout.children()) .any(|(child, layout)| { - child.is_over(layout, renderer, cursor_position) + child + .as_overlay() + .is_over(layout, renderer, cursor_position) }) } @@ -154,11 +171,20 @@ where .children .iter_mut() .zip(layout.children()) - .filter_map(|(child, layout)| child.overlay(layout, renderer)) + .filter_map(|(child, layout)| { + child.as_overlay_mut().overlay(layout, renderer) + }) .collect::>(); (!children.is_empty()).then(|| Group::with_children(children).overlay()) } + + fn index(&self) -> f32 { + self.children + .first() + .map(|child| child.as_overlay().index()) + .unwrap_or(1.0) + } } impl<'a, Message, Theme, Renderer> From> diff --git a/examples/gallery/src/main.rs b/examples/gallery/src/main.rs index 0d52483b..e8cc95a3 100644 --- a/examples/gallery/src/main.rs +++ b/examples/gallery/src/main.rs @@ -14,8 +14,8 @@ use iced::widget::{ }; use iced::window; use iced::{ - Animation, ContentFit, Element, Fill, Function, Subscription, Task, Theme, - color, + Animation, Color, ContentFit, Element, Fill, Function, Shadow, + Subscription, Task, Theme, color, }; use std::collections::HashMap; @@ -210,6 +210,19 @@ fn card<'a>( .content_fit(ContentFit::Cover) .opacity(thumbnail.fade_in.interpolate(0.0, 1.0, now)) .scale(thumbnail.zoom.interpolate(1.0, 1.1, now)) + .float(true) + .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() } else { horizontal_space().into() diff --git a/runtime/src/overlay/nested.rs b/runtime/src/overlay/nested.rs index 38054d7b..a577b903 100644 --- a/runtime/src/overlay/nested.rs +++ b/runtime/src/overlay/nested.rs @@ -39,10 +39,11 @@ where where Renderer: renderer::Renderer, { - let node = element.layout(renderer, bounds); + let overlay = element.as_overlay_mut(); + let node = overlay.layout(renderer, bounds); if let Some(mut nested) = - element.overlay(Layout::new(&node), renderer) + overlay.overlay(Layout::new(&node), renderer) { layout::Node::with_children( node.size(), @@ -79,13 +80,14 @@ where if let Some(layout) = layouts.next() { let nested_layout = layouts.next(); + let overlay = element.as_overlay_mut(); let is_over = cursor .position() .zip(nested_layout) .and_then(|(cursor_position, nested_layout)| { - element.overlay(layout, renderer).map(|nested| { - nested.is_over( + overlay.overlay(layout, renderer).map(|nested| { + nested.as_overlay().is_over( nested_layout.children().next().unwrap(), renderer, cursor_position, @@ -95,7 +97,7 @@ where .unwrap_or_default(); renderer.with_layer(layout.bounds(), |renderer| { - element.draw( + overlay.draw( renderer, theme, style, @@ -109,7 +111,7 @@ where }); if let Some((mut nested, nested_layout)) = - element.overlay(layout, renderer).zip(nested_layout) + overlay.overlay(layout, renderer).zip(nested_layout) { recurse( &mut nested, @@ -144,10 +146,12 @@ where let mut layouts = layout.children(); if let Some(layout) = layouts.next() { - element.operate(layout, renderer, operation); + let overlay = element.as_overlay_mut(); + + overlay.operate(layout, renderer, operation); if let Some((mut nested, nested_layout)) = - element.overlay(layout, renderer).zip(layouts.next()) + overlay.overlay(layout, renderer).zip(layouts.next()) { recurse(&mut nested, nested_layout, renderer, operation); } @@ -182,8 +186,10 @@ where let mut layouts = layout.children(); if let Some(layout) = layouts.next() { + let overlay = element.as_overlay_mut(); + let nested_is_over = if let Some((mut nested, nested_layout)) = - element.overlay(layout, renderer).zip(layouts.next()) + overlay.overlay(layout, renderer).zip(layouts.next()) { recurse( &mut nested, @@ -203,7 +209,7 @@ where || cursor .position() .map(|cursor_position| { - element.is_over( + overlay.is_over( layout, renderer, cursor_position, @@ -211,7 +217,7 @@ where }) .unwrap_or_default(); - element.update( + overlay.update( event, layout, if nested_is_over { @@ -266,13 +272,14 @@ where let layout = layouts.next()?; let cursor_position = cursor.position()?; + let overlay = element.as_overlay_mut(); - if !element.is_over(layout, renderer, cursor_position) { + if !overlay.is_over(layout, renderer, cursor_position) { return None; } Some( - element + overlay .overlay(layout, renderer) .zip(layouts.next()) .and_then(|(mut overlay, layout)| { @@ -285,7 +292,7 @@ where ) }) .unwrap_or_else(|| { - element.mouse_interaction( + overlay.mouse_interaction( layout, cursor, viewport, renderer, ) }), @@ -315,12 +322,14 @@ where let mut layouts = layout.children(); if let Some(layout) = layouts.next() { - if element.is_over(layout, renderer, cursor_position) { + let overlay = element.as_overlay_mut(); + + if overlay.is_over(layout, renderer, cursor_position) { return true; } if let Some((mut nested, nested_layout)) = - element.overlay(layout, renderer).zip(layouts.next()) + overlay.overlay(layout, renderer).zip(layouts.next()) { recurse( &mut nested, diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 1bd9f8ee..6dfa660b 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -1844,7 +1844,12 @@ where /// ``` /// #[cfg(feature = "image")] -pub fn image(handle: impl Into) -> crate::Image { +pub fn image<'a, Handle, Theme>( + handle: impl Into, +) -> crate::Image<'a, Handle, Theme> +where + Theme: crate::image::Catalog, +{ crate::Image::new(handle.into()) } diff --git a/widget/src/image.rs b/widget/src/image.rs index 6c84ec92..d06fe759 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -19,14 +19,17 @@ 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, Size, - Vector, Widget, + ContentFit, Element, Layout, Length, Point, Rectangle, Rotation, Shadow, + Size, Vector, Widget, }; pub use image::{FilterMethod, Handle}; @@ -55,7 +58,10 @@ pub fn viewer(handle: Handle) -> Viewer { /// ``` /// #[derive(Debug)] -pub struct Image { +pub struct Image<'a, Handle = image::Handle, Theme = crate::Theme> +where + Theme: Catalog, +{ handle: Handle, width: Length, height: Length, @@ -64,9 +70,14 @@ pub struct Image { rotation: Rotation, opacity: f32, scale: f32, + float: bool, + class: Theme::Class<'a>, } -impl Image { +impl<'a, Handle, Theme> Image<'a, Handle, Theme> +where + Theme: Catalog, +{ /// Creates a new [`Image`] with the given path. pub fn new(handle: impl Into) -> Self { Image { @@ -78,6 +89,8 @@ impl Image { rotation: Rotation::default(), opacity: 1.0, scale: 1.0, + float: false, + class: Theme::default(), } } @@ -130,6 +143,35 @@ impl Image { self.scale = scale.into(); self } + + /// Sets whether an [`Image`] should float above other content when scaled up. + /// + /// By default, an [`Image`] has this flag set to `false`; meaning it + /// will be clipped or "framed" inside its bounds when scaled. + /// + /// Enabling this flag is useful to create cool hover effects! + pub fn float(mut self, float: bool) -> Self { + self.float = float; + 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`]. @@ -174,26 +216,20 @@ where layout::Node::new(final_size) } -/// Draws an [`Image`] -pub fn draw( - renderer: &mut Renderer, - layout: Layout<'_>, - viewport: &Rectangle, +fn drawing_bounds( + renderer: &Renderer, + bounds: Rectangle, handle: &Handle, content_fit: ContentFit, - filter_method: FilterMethod, rotation: Rotation, - opacity: f32, scale: f32, -) where +) -> Rectangle +where Renderer: image::Renderer, - Handle: Clone, { let Size { width, height } = renderer.measure_image(handle); let image_size = Size::new(width as f32, height as f32); let rotated_size = rotation.apply(image_size); - - let bounds = layout.bounds(); let adjusted_fit = content_fit.fit(rotated_size, bounds.size()); let fit_scale = Vector::new( @@ -214,36 +250,103 @@ pub fn draw( ), }; - let drawing_bounds = Rectangle::new(position, final_size); + Rectangle::new(position, final_size) +} - let render = |renderer: &mut Renderer| { - renderer.draw_image( - image::Image { - handle: handle.clone(), - filter_method, - rotation: rotation.radians(), - opacity, - snap: true, - }, - drawing_bounds, - ); - }; +fn must_clip(bounds: Rectangle, drawing_bounds: Rectangle) -> bool { + drawing_bounds.width > bounds.width || drawing_bounds.height > bounds.height +} + +/// Draws an [`Image`] +pub fn draw( + renderer: &mut Renderer, + layout: Layout<'_>, + viewport: &Rectangle, + handle: &Handle, + content_fit: ContentFit, + filter_method: FilterMethod, + rotation: Rotation, + opacity: f32, + scale: f32, + float: bool, + style: Style, +) where + Renderer: image::Renderer, + Handle: Clone, +{ + let bounds = layout.bounds(); + let drawing_bounds = + drawing_bounds(renderer, bounds, handle, content_fit, rotation, scale); + + if must_clip(bounds, drawing_bounds) { + if scale > 1.0 && float { + return; + } - if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height - { if let Some(bounds) = bounds.intersection(viewport) { - renderer.with_layer(bounds, render); + renderer.with_layer(bounds, |renderer| { + render( + renderer, + handle, + filter_method, + rotation, + opacity, + drawing_bounds, + ); + }); } } else { - render(renderer); + render( + renderer, + handle, + filter_method, + rotation, + opacity, + 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( + renderer: &mut Renderer, + handle: &Handle, + filter_method: FilterMethod, + rotation: Rotation, + opacity: f32, + drawing_bounds: Rectangle, +) where + Renderer: image::Renderer, + Handle: Clone, +{ + renderer.draw_image( + image::Image { + handle: handle.clone(), + filter_method, + rotation: rotation.radians(), + opacity, + snap: true, + }, + drawing_bounds, + ); +} + impl Widget - for Image + for Image<'_, Handle, Theme> where Renderer: image::Renderer, Handle: Clone, + Theme: Catalog, { fn size(&self) -> Size { Size { @@ -273,7 +376,7 @@ where &self, _state: &Tree, renderer: &mut Renderer, - _theme: &Theme, + theme: &Theme, _style: &renderer::Style, layout: Layout<'_>, _cursor: mouse::Cursor, @@ -289,17 +392,179 @@ where self.rotation, self.opacity, self.scale, + self.float, + theme.style(&self.class), ); } + + fn overlay<'a>( + &'a mut self, + _state: &'a mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + translation: Vector, + ) -> Option> { + if !self.float || self.scale <= 1.0 { + return None; + } + + 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) { + Some(overlay::Element::new(Box::new(Overlay { + image: self, + clip_bounds: bounds, + drawing_bounds, + }))) + } 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) -> Element<'a, Message, Theme, Renderer> { + fn from( + image: Image<'a, Handle, Theme>, + ) -> 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 a [`Button`]. +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>, + 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 clip_bounds = Rectangle { + x: self.clip_bounds.x + - (self.clip_bounds.width * self.image.scale + - self.clip_bounds.width) + / 2.0, + y: self.clip_bounds.y + - (self.clip_bounds.height * self.image.scale + - self.clip_bounds.height) + / 2.0, + width: self.clip_bounds.width * self.image.scale, + height: self.clip_bounds.height * self.image.scale, + }; + + 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: clip_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/themer.rs b/widget/src/themer.rs index cf0845be..eb7f861c 100644 --- a/widget/src/themer.rs +++ b/widget/src/themer.rs @@ -198,7 +198,7 @@ where renderer: &Renderer, bounds: Size, ) -> layout::Node { - self.content.layout(renderer, bounds) + self.content.as_overlay_mut().layout(renderer, bounds) } fn draw( @@ -209,7 +209,7 @@ where layout: Layout<'_>, cursor: mouse::Cursor, ) { - self.content.draw( + self.content.as_overlay().draw( renderer, &(self.to_theme)(theme), style, @@ -228,6 +228,7 @@ where shell: &mut Shell<'_, Message>, ) { self.content + .as_overlay_mut() .update(event, layout, cursor, renderer, clipboard, shell); } @@ -237,7 +238,9 @@ where renderer: &Renderer, operation: &mut dyn Operation, ) { - self.content.operate(layout, renderer, operation); + self.content + .as_overlay_mut() + .operate(layout, renderer, operation); } fn mouse_interaction( @@ -248,6 +251,7 @@ where renderer: &Renderer, ) -> mouse::Interaction { self.content + .as_overlay() .mouse_interaction(layout, cursor, viewport, renderer) } @@ -257,7 +261,11 @@ where renderer: &Renderer, cursor_position: Point, ) -> bool { - self.content.is_over(layout, renderer, cursor_position) + self.content.as_overlay().is_over( + layout, + renderer, + cursor_position, + ) } fn overlay<'b>( @@ -267,6 +275,7 @@ where ) -> Option> { self.content + .as_overlay_mut() .overlay(layout, renderer) .map(|content| Overlay { to_theme: &self.to_theme, From a2a7c18c9d0a0b7cc469bd4000a7239618167477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 25 Apr 2025 13:31:49 +0200 Subject: [PATCH 07/13] Implement `Image::translate` for positioning floating images --- core/src/rectangle.rs | 35 ++++++++++++++++++++ examples/gallery/src/main.rs | 6 ++++ widget/src/image.rs | 62 +++++++++++++++++++++++------------- 3 files changed, 81 insertions(+), 22 deletions(-) diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index 14d2a2e8..5ffdafa1 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -157,6 +157,30 @@ impl Rectangle { distance_x.hypot(distance_y) } + /// Computes the offset that must be applied to the [`Rectangle`] to be placed + /// inside the given `container`. + pub fn offset(&self, container: &Rectangle) -> Vector { + if let Some(intersection) = self.intersection(container) { + let left = intersection.x - self.x; + let top = intersection.y - self.y; + + Vector::new( + if left > 0.0 { + left + } else { + intersection.x + intersection.width - self.x - self.width + }, + if top > 0.0 { + top + } else { + intersection.y + intersection.height - self.y - self.height + }, + ) + } else { + Vector::ZERO + } + } + /// Returns true if the current [`Rectangle`] is completely within the given /// `container`. pub fn is_within(&self, container: &Rectangle) -> bool { @@ -268,6 +292,17 @@ impl Rectangle { Self::new(position, size) } + + /// Scales the [`Rectangle`] without changing its position, effectively + /// "zooming" it. + pub fn zoom(self, zoom: f32) -> Self { + Self { + x: self.x - (self.width * (zoom - 1.0)) / 2.0, + y: self.y - (self.height * (zoom - 1.0)) / 2.0, + width: self.width * zoom, + height: self.height * zoom, + } + } } impl std::ops::Mul for Rectangle { diff --git a/examples/gallery/src/main.rs b/examples/gallery/src/main.rs index e8cc95a3..47653686 100644 --- a/examples/gallery/src/main.rs +++ b/examples/gallery/src/main.rs @@ -210,6 +210,12 @@ fn card<'a>( .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| { + let final_bounds = bounds.zoom(1.1); + + final_bounds.offset(&viewport.shrink(10)) + * thumbnail.zoom.interpolate(0.0, 1.0, now) + }) .float(true) .style(move |_theme| image::Style { shadow: Shadow { diff --git a/widget/src/image.rs b/widget/src/image.rs index d06fe759..5aaf98c7 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -57,7 +57,7 @@ pub fn viewer(handle: Handle) -> Viewer { /// } /// ``` /// -#[derive(Debug)] +#[allow(missing_debug_implementations)] pub struct Image<'a, Handle = image::Handle, Theme = crate::Theme> where Theme: Catalog, @@ -70,6 +70,7 @@ where rotation: Rotation, opacity: f32, scale: f32, + translate: Option Vector + 'a>>, float: bool, class: Theme::Class<'a>, } @@ -89,6 +90,7 @@ where rotation: Rotation::default(), opacity: 1.0, scale: 1.0, + translate: None, float: false, class: Theme::default(), } @@ -144,10 +146,27 @@ where self } - /// Sets whether an [`Image`] should float above other content when scaled up. + /// Sets the translation to apply to the [`Image`] when floating. + /// + /// 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 floating 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 whether an [`Image`] should float above other content when + /// scaled up. /// /// By default, an [`Image`] has this flag set to `false`; meaning it - /// will be clipped or "framed" inside its bounds when scaled. + /// will be clipped or "framed" inside its bounds. /// /// Enabling this flag is useful to create cool hover effects! pub fn float(mut self, float: bool) -> Self { @@ -459,7 +478,7 @@ pub trait Catalog { fn style(&self, class: &Self::Class<'_>) -> Style; } -/// A styling function for a [`Button`]. +/// A styling function for an [`Image`]. pub type StyleFn<'a, Theme> = Box Style + 'a>; impl Catalog for crate::Theme { @@ -499,9 +518,18 @@ where 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 layout(&mut self, _renderer: &Renderer, bounds: Size) -> layout::Node { + let bounds = if let Some(translate) = &self.image.translate { + self.clip_bounds + + translate( + self.clip_bounds, + Rectangle::new(Point::ORIGIN, bounds), + ) + } else { + self.clip_bounds + }; + + layout::Node::new(bounds.size()).move_to(bounds.position()) } fn is_over( @@ -518,22 +546,12 @@ where renderer: &mut Renderer, theme: &Theme, _style: &renderer::Style, - _layout: Layout<'_>, + layout: Layout<'_>, _cursor: mouse::Cursor, ) { - let clip_bounds = Rectangle { - x: self.clip_bounds.x - - (self.clip_bounds.width * self.image.scale - - self.clip_bounds.width) - / 2.0, - y: self.clip_bounds.y - - (self.clip_bounds.height * self.image.scale - - self.clip_bounds.height) - / 2.0, - width: self.clip_bounds.width * self.image.scale, - height: self.clip_bounds.height * self.image.scale, - }; - + let bounds = layout.bounds(); + let translation = bounds.position() - self.clip_bounds.position(); + let clip_bounds = bounds.zoom(self.image.scale); let style = theme.style(&self.image.class); if style.shadow.color.a > 0.0 { @@ -559,7 +577,7 @@ where self.image.filter_method, self.image.rotation, self.image.opacity, - self.drawing_bounds, + self.drawing_bounds + translation, ); }); } From 6c51a9579ddb039790981602a1cb57b6eb42dca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 26 Apr 2025 02:51:32 +0200 Subject: [PATCH 08/13] Make `Overlay` aware of `viewport` ... and unify `Image::float` and `Image::translate` --- core/src/element.rs | 14 ++++-- core/src/overlay.rs | 11 +++-- core/src/widget.rs | 1 + examples/gallery/src/main.rs | 2 - examples/toast/src/main.rs | 2 + runtime/src/user_interface.rs | 8 ++++ widget/src/button.rs | 2 + widget/src/column.rs | 2 + widget/src/combo_box.rs | 1 + widget/src/container.rs | 2 + widget/src/grid.rs | 2 + widget/src/helpers.rs | 4 ++ widget/src/image.rs | 74 ++++++++++++++----------------- widget/src/keyed/column.rs | 2 + widget/src/lazy.rs | 3 +- widget/src/lazy/component.rs | 2 + widget/src/lazy/responsive.rs | 9 +++- widget/src/mouse_area.rs | 2 + widget/src/pane_grid.rs | 3 +- widget/src/pane_grid/content.rs | 4 ++ widget/src/pane_grid/title_bar.rs | 6 ++- widget/src/pick_list.rs | 1 + widget/src/pin.rs | 2 + widget/src/pop.rs | 2 + widget/src/row.rs | 6 ++- widget/src/scrollable.rs | 3 ++ widget/src/stack.rs | 2 + widget/src/themer.rs | 3 +- widget/src/tooltip.rs | 2 + 29 files changed, 121 insertions(+), 56 deletions(-) diff --git a/core/src/element.rs b/core/src/element.rs index b7d51aeb..6f8751ab 100644 --- a/core/src/element.rs +++ b/core/src/element.rs @@ -368,12 +368,13 @@ where tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { let mapper = &self.mapper; self.widget - .overlay(tree, layout, renderer, translation) + .overlay(tree, layout, renderer, viewport, translation) .map(move |overlay| overlay.map(mapper)) } } @@ -519,10 +520,15 @@ where state: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { - self.element - .widget - .overlay(state, layout, renderer, translation) + self.element.widget.overlay( + state, + layout, + renderer, + viewport, + translation, + ) } } diff --git a/core/src/overlay.rs b/core/src/overlay.rs index 49d4c39e..92118567 100644 --- a/core/src/overlay.rs +++ b/core/src/overlay.rs @@ -122,6 +122,7 @@ pub fn from_children<'a, Message, Theme, Renderer>( tree: &'a mut Tree, layout: Layout<'_>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> where @@ -132,9 +133,13 @@ where .zip(&mut tree.children) .zip(layout.children()) .filter_map(|((child, state), layout)| { - child - .as_widget_mut() - .overlay(state, layout, renderer, translation) + child.as_widget_mut().overlay( + state, + layout, + renderer, + viewport, + translation, + ) }) .collect::>(); diff --git a/core/src/widget.rs b/core/src/widget.rs index 807b14ac..72985e3e 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -146,6 +146,7 @@ where _state: &'a mut Tree, _layout: Layout<'_>, _renderer: &Renderer, + _viewport: &Rectangle, _translation: Vector, ) -> Option> { None diff --git a/examples/gallery/src/main.rs b/examples/gallery/src/main.rs index 47653686..4b8af044 100644 --- a/examples/gallery/src/main.rs +++ b/examples/gallery/src/main.rs @@ -212,11 +212,9 @@ fn card<'a>( .scale(thumbnail.zoom.interpolate(1.0, 1.1, now)) .translate(move |bounds, viewport| { let final_bounds = bounds.zoom(1.1); - final_bounds.offset(&viewport.shrink(10)) * thumbnail.zoom.interpolate(0.0, 1.0, now) }) - .float(true) .style(move |_theme| image::Style { shadow: Shadow { color: Color::BLACK.scale_alpha( diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index 87d4f107..ef9418d8 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -425,6 +425,7 @@ mod toast { state: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { let instants = state.state.downcast_mut::>>(); @@ -435,6 +436,7 @@ mod toast { &mut content_state[0], layout, renderer, + viewport, translation, ); diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index 9b396c69..88cab5e4 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -190,6 +190,7 @@ where let mut outdated = false; let mut redraw_request = window::RedrawRequest::Wait; let mut input_method = InputMethod::Disabled; + let viewport = Rectangle::with_size(self.bounds); let mut manual_overlay = ManuallyDrop::new( self.root @@ -198,6 +199,7 @@ where &mut self.state, Layout::new(&self.base), renderer, + &viewport, Vector::ZERO, ) .map(overlay::Nested::new), @@ -242,6 +244,7 @@ where &mut self.state, Layout::new(&self.base), renderer, + &viewport, Vector::ZERO, ) .map(overlay::Nested::new), @@ -443,6 +446,7 @@ where &mut self.state, Layout::new(&self.base), renderer, + &viewport, Vector::ZERO, ) .map(overlay::Nested::new) @@ -513,6 +517,7 @@ where &mut self.state, Layout::new(base), renderer, + &viewport, Vector::ZERO, ) .map(overlay::Nested::new) @@ -558,6 +563,8 @@ where renderer: &Renderer, operation: &mut dyn widget::Operation, ) { + let viewport = Rectangle::with_size(self.bounds); + self.root.as_widget().operate( &mut self.state, Layout::new(&self.base), @@ -572,6 +579,7 @@ where &mut self.state, Layout::new(&self.base), renderer, + &viewport, Vector::ZERO, ) .map(overlay::Nested::new) diff --git a/widget/src/button.rs b/widget/src/button.rs index d4500888..63987597 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -432,12 +432,14 @@ where tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { self.content.as_widget_mut().overlay( &mut tree.children[0], layout.children().next().unwrap(), renderer, + viewport, translation, ) } diff --git a/widget/src/column.rs b/widget/src/column.rs index 7200690b..777eb328 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -338,6 +338,7 @@ where tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { overlay::from_children( @@ -345,6 +346,7 @@ where tree, layout, renderer, + viewport, translation, ) } diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index f71e4a6e..195fe0cb 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -832,6 +832,7 @@ where tree: &'b mut widget::Tree, layout: Layout<'_>, _renderer: &Renderer, + _viewport: &Rectangle, translation: Vector, ) -> Option> { let is_focused = { diff --git a/widget/src/container.rs b/widget/src/container.rs index 82774186..1c774ced 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -379,12 +379,14 @@ where tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { self.content.as_widget_mut().overlay( tree, layout.children().next().unwrap(), renderer, + viewport, translation, ) } diff --git a/widget/src/grid.rs b/widget/src/grid.rs index da827007..76de415d 100644 --- a/widget/src/grid.rs +++ b/widget/src/grid.rs @@ -345,6 +345,7 @@ where tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { overlay::from_children( @@ -352,6 +353,7 @@ where tree, layout, renderer, + viewport, translation, ) } diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 6dfa660b..82908f88 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -694,6 +694,7 @@ where state: &'b mut core::widget::Tree, layout: core::Layout<'_>, renderer: &Renderer, + viewport: &Rectangle, translation: core::Vector, ) -> Option> { @@ -701,6 +702,7 @@ where state, layout, renderer, + viewport, translation, ) } @@ -948,6 +950,7 @@ where tree: &'b mut core::widget::Tree, layout: core::Layout<'_>, renderer: &Renderer, + viewport: &Rectangle, translation: core::Vector, ) -> Option> { @@ -959,6 +962,7 @@ where tree, layout, renderer, + viewport, translation, ) }); diff --git a/widget/src/image.rs b/widget/src/image.rs index 5aaf98c7..5a7bfb01 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -71,7 +71,6 @@ where opacity: f32, scale: f32, translate: Option Vector + 'a>>, - float: bool, class: Theme::Class<'a>, } @@ -91,7 +90,6 @@ where opacity: 1.0, scale: 1.0, translate: None, - float: false, class: Theme::default(), } } @@ -146,14 +144,14 @@ where self } - /// Sets the translation to apply to the [`Image`] when floating. + /// 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 floating images stay visible inside the - /// viewport. + /// Translating can be useful to ensure images stay visible inside the viewport. pub fn translate( mut self, translate: impl Fn(Rectangle, Rectangle) -> Vector + 'a, @@ -162,18 +160,6 @@ where self } - /// Sets whether an [`Image`] should float above other content when - /// scaled up. - /// - /// By default, an [`Image`] has this flag set to `false`; meaning it - /// will be clipped or "framed" inside its bounds. - /// - /// Enabling this flag is useful to create cool hover effects! - pub fn float(mut self, float: bool) -> Self { - self.float = float; - self - } - /// Sets the style of the [`Image`]. #[must_use] pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self @@ -287,7 +273,7 @@ pub fn draw( rotation: Rotation, opacity: f32, scale: f32, - float: bool, + translate: Option<&dyn Fn(Rectangle, Rectangle) -> Vector>, style: Style, ) where Renderer: image::Renderer, @@ -298,7 +284,9 @@ pub fn draw( drawing_bounds(renderer, bounds, handle, content_fit, rotation, scale); if must_clip(bounds, drawing_bounds) { - if scale > 1.0 && float { + if translate.is_some_and(|translate| { + scale > 1.0 || translate(bounds, *viewport) != Vector::ZERO + }) { return; } @@ -411,7 +399,7 @@ where self.rotation, self.opacity, self.scale, - self.float, + self.translate.as_deref(), theme.style(&self.class), ); } @@ -421,12 +409,10 @@ where _state: &'a mut Tree, layout: Layout<'_>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { - if !self.float || self.scale <= 1.0 { - return None; - } - + let translate = self.translate.as_ref()?; let bounds = layout.bounds() + translation; let drawing_bounds = drawing_bounds( renderer, @@ -438,10 +424,17 @@ where ); 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, - clip_bounds: bounds, - drawing_bounds, + viewport: *viewport, + clip_bounds: bounds + translate, + drawing_bounds: drawing_bounds + translate, }))) } else { None @@ -507,6 +500,7 @@ where Theme: Catalog, { image: &'a Image<'b, Handle, Theme>, + viewport: Rectangle, clip_bounds: Rectangle, drawing_bounds: Rectangle, } @@ -518,18 +512,9 @@ where Handle: Clone, Theme: Catalog, { - fn layout(&mut self, _renderer: &Renderer, bounds: Size) -> layout::Node { - let bounds = if let Some(translate) = &self.image.translate { - self.clip_bounds - + translate( - self.clip_bounds, - Rectangle::new(Point::ORIGIN, bounds), - ) - } else { - self.clip_bounds - }; - - layout::Node::new(bounds.size()).move_to(bounds.position()) + 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( @@ -550,7 +535,6 @@ where _cursor: mouse::Cursor, ) { let bounds = layout.bounds(); - let translation = bounds.position() - self.clip_bounds.position(); let clip_bounds = bounds.zoom(self.image.scale); let style = theme.style(&self.image.class); @@ -560,7 +544,11 @@ where |renderer| { renderer.fill_quad( renderer::Quad { - bounds: clip_bounds.shrink(1.0), + 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), }, @@ -570,6 +558,10 @@ where ); } + let clip_bounds = clip_bounds + .intersection(&self.viewport) + .unwrap_or(self.viewport); + renderer.with_layer(clip_bounds, |renderer| { render( renderer, @@ -577,7 +569,7 @@ where self.image.filter_method, self.image.rotation, self.image.opacity, - self.drawing_bounds + translation, + self.drawing_bounds, ); }); } diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs index 3064a8c4..ed9b5dbf 100644 --- a/widget/src/keyed/column.rs +++ b/widget/src/keyed/column.rs @@ -369,6 +369,7 @@ where tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { overlay::from_children( @@ -376,6 +377,7 @@ where tree, layout, renderer, + viewport, translation, ) } diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index 8b7b38ce..82fab287 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -267,6 +267,7 @@ where tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { let overlay = InnerBuilder { @@ -283,7 +284,7 @@ where overlay_builder: |element, tree| { element .as_widget_mut() - .overlay(tree, layout, renderer, translation) + .overlay(tree, layout, renderer, viewport, translation) .map(|overlay| RefCell::new(Nested::new(overlay))) }, } diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index 0cfcc953..d6984109 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -447,6 +447,7 @@ where tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { self.rebuild_element_if_necessary(); @@ -469,6 +470,7 @@ where &mut tree.children[0], layout, renderer, + viewport, translation, ) .map(|overlay| RefCell::new(Nested::new(overlay))) diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index e7c937af..ca5825e3 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -283,6 +283,7 @@ where tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { use std::ops::DerefMut; @@ -315,7 +316,13 @@ where ( element .as_widget_mut() - .overlay(tree, content_layout, renderer, translation) + .overlay( + tree, + content_layout, + renderer, + viewport, + translation, + ) .map(|overlay| RefCell::new(Nested::new(overlay))), content_layout_node, ) diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index 2ea0b059..54387e2d 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -296,12 +296,14 @@ where tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { self.content.as_widget_mut().overlay( &mut tree.children[0], layout, renderer, + viewport, translation, ) } diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index db93c724..743eec0a 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -979,6 +979,7 @@ where tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { let children = self @@ -997,7 +998,7 @@ where return None; } - content.overlay(state, layout, renderer, translation) + content.overlay(state, layout, renderer, viewport, translation) }) .collect::>(); diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs index 4d63dd18..8a235be7 100644 --- a/widget/src/pane_grid/content.rs +++ b/widget/src/pane_grid/content.rs @@ -366,6 +366,7 @@ where tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { if let Some(title_bar) = self.title_bar.as_mut() { @@ -380,6 +381,7 @@ where title_bar_state, title_bar_layout, renderer, + viewport, translation, ) { Some(overlay) => Some(overlay), @@ -387,6 +389,7 @@ where body_state, children.next()?, renderer, + viewport, translation, ), } @@ -395,6 +398,7 @@ where &mut tree.children[0], layout, renderer, + viewport, translation, ) } diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs index 611c3d67..714f7f47 100644 --- a/widget/src/pane_grid/title_bar.rs +++ b/widget/src/pane_grid/title_bar.rs @@ -570,6 +570,7 @@ where tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { let mut children = layout.children(); @@ -588,7 +589,7 @@ where content .as_widget_mut() - .overlay(title_state, title_layout, renderer, translation) + .overlay(title_state, title_layout, renderer, viewport, translation) .or_else(move || { controls.as_mut().and_then(|controls| { let controls_layout = children.next()?; @@ -605,6 +606,7 @@ where compact_state, compact_layout, renderer, + viewport, translation, ) } else { @@ -612,6 +614,7 @@ where controls_state, controls_layout, renderer, + viewport, translation, ) } @@ -620,6 +623,7 @@ where controls_state, controls_layout, renderer, + viewport, translation, ) } diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index 653c2f04..4a9d1e2f 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -690,6 +690,7 @@ where tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, + _viewport: &Rectangle, translation: Vector, ) -> Option> { let state = tree.state.downcast_mut::>(); diff --git a/widget/src/pin.rs b/widget/src/pin.rs index afa29398..2b663d1e 100644 --- a/widget/src/pin.rs +++ b/widget/src/pin.rs @@ -244,12 +244,14 @@ where tree: &'b mut widget::Tree, layout: Layout<'_>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { self.content.as_widget_mut().overlay( tree, layout.children().next().unwrap(), renderer, + viewport, translation, ) } diff --git a/widget/src/pop.rs b/widget/src/pop.rs index 5add1525..75973e3c 100644 --- a/widget/src/pop.rs +++ b/widget/src/pop.rs @@ -295,12 +295,14 @@ where tree: &'b mut Tree, layout: core::Layout<'_>, renderer: &Renderer, + viewport: &Rectangle, translation: core::Vector, ) -> Option> { self.content.as_widget_mut().overlay( &mut tree.children[0], layout, renderer, + viewport, translation, ) } diff --git a/widget/src/row.rs b/widget/src/row.rs index b9fd2569..1de29e17 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -337,6 +337,7 @@ where tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { overlay::from_children( @@ -344,6 +345,7 @@ where tree, layout, renderer, + viewport, translation, ) } @@ -546,9 +548,11 @@ where tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { - self.row.overlay(tree, layout, renderer, translation) + self.row + .overlay(tree, layout, renderer, viewport, translation) } } diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index d50591a1..9d704d40 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -1180,11 +1180,13 @@ where tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { let bounds = layout.bounds(); let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); + let visible_bounds = bounds.intersection(viewport).unwrap_or(*viewport); let offset = tree.state.downcast_ref::().translation( self.direction, @@ -1196,6 +1198,7 @@ where &mut tree.children[0], layout.children().next().unwrap(), renderer, + &visible_bounds, translation - offset, ) } diff --git a/widget/src/stack.rs b/widget/src/stack.rs index df9f6162..dda9c357 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -354,6 +354,7 @@ where tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { overlay::from_children( @@ -361,6 +362,7 @@ where tree, layout, renderer, + viewport, translation, ) } diff --git a/widget/src/themer.rs b/widget/src/themer.rs index eb7f861c..3d58db61 100644 --- a/widget/src/themer.rs +++ b/widget/src/themer.rs @@ -180,6 +180,7 @@ where tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { struct Overlay<'a, Message, Theme, NewTheme, Renderer> { @@ -287,7 +288,7 @@ where self.content .as_widget_mut() - .overlay(tree, layout, renderer, translation) + .overlay(tree, layout, renderer, viewport, translation) .map(|content| Overlay { to_theme: &self.to_theme, content, diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs index ce34f2a5..4223c8b6 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -273,6 +273,7 @@ where tree: &'b mut widget::Tree, layout: Layout<'_>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { let state = tree.state.downcast_ref::(); @@ -283,6 +284,7 @@ where children.next().unwrap(), layout, renderer, + viewport, translation, ); From 385cfcddb22df49edc07a322cab9628899dea3ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 26 Apr 2025 02:55:42 +0200 Subject: [PATCH 09/13] Remove `push` method for `overlay::Group` --- core/src/overlay/group.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/core/src/overlay/group.rs b/core/src/overlay/group.rs index 2e199441..a0afe3be 100644 --- a/core/src/overlay/group.rs +++ b/core/src/overlay/group.rs @@ -39,15 +39,6 @@ where Group { children } } - /// Adds an [`overlay::Element`] to the [`Group`]. - pub fn push( - mut self, - child: impl Into>, - ) -> Self { - self.children.push(child.into()); - self - } - /// Turns the [`Group`] into an overlay [`overlay::Element`]. pub fn overlay(self) -> overlay::Element<'a, Message, Theme, Renderer> { overlay::Element::new(Box::new(self)) From c4e6e42759a293e2e4d6a18db1b92316273efd20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 26 Apr 2025 03:16:45 +0200 Subject: [PATCH 10/13] Clip shadow bounds of floating images to the `viewport` --- widget/src/image.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/widget/src/image.rs b/widget/src/image.rs index 5a7bfb01..751e6eb9 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -538,6 +538,10 @@ where let clip_bounds = bounds.zoom(self.image.scale); let style = theme.style(&self.image.class); + let clip_bounds = clip_bounds + .intersection(&self.viewport) + .unwrap_or(self.viewport); + if style.shadow.color.a > 0.0 { renderer.with_layer( clip_bounds.expand(style.shadow.blur_radius), @@ -558,10 +562,6 @@ where ); } - let clip_bounds = clip_bounds - .intersection(&self.viewport) - .unwrap_or(self.viewport); - renderer.with_layer(clip_bounds, |renderer| { render( renderer, From 00e4de88bf8c3ce753ed0e277c167a813c97b299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 26 Apr 2025 03:19:30 +0200 Subject: [PATCH 11/13] Return early when drawing `image::Overlay` if not visible --- widget/src/image.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/widget/src/image.rs b/widget/src/image.rs index 751e6eb9..fa40f441 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -536,11 +536,12 @@ where ) { let bounds = layout.bounds(); let clip_bounds = bounds.zoom(self.image.scale); - let style = theme.style(&self.image.class); - let clip_bounds = clip_bounds - .intersection(&self.viewport) - .unwrap_or(self.viewport); + 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( From 61b96caf219009e8328572aeb8826fa2b7552d42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 26 Apr 2025 04:50:59 +0200 Subject: [PATCH 12/13] Reduce nesting in `Rectangle::offset` --- core/src/rectangle.rs | 36 ++++++++++++++++++------------------ examples/gallery/src/main.rs | 3 +-- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index 5ffdafa1..1984394c 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -160,25 +160,25 @@ impl Rectangle { /// Computes the offset that must be applied to the [`Rectangle`] to be placed /// inside the given `container`. pub fn offset(&self, container: &Rectangle) -> Vector { - if let Some(intersection) = self.intersection(container) { - let left = intersection.x - self.x; - let top = intersection.y - self.y; + let Some(intersection) = self.intersection(container) else { + return Vector::ZERO; + }; - Vector::new( - if left > 0.0 { - left - } else { - intersection.x + intersection.width - self.x - self.width - }, - if top > 0.0 { - top - } else { - intersection.y + intersection.height - self.y - self.height - }, - ) - } else { - Vector::ZERO - } + let left = intersection.x - self.x; + let top = intersection.y - self.y; + + Vector::new( + if left > 0.0 { + left + } else { + intersection.x + intersection.width - self.x - self.width + }, + if top > 0.0 { + top + } else { + intersection.y + intersection.height - self.y - self.height + }, + ) } /// Returns true if the current [`Rectangle`] is completely within the given diff --git a/examples/gallery/src/main.rs b/examples/gallery/src/main.rs index 4b8af044..709771c1 100644 --- a/examples/gallery/src/main.rs +++ b/examples/gallery/src/main.rs @@ -211,8 +211,7 @@ fn card<'a>( .opacity(thumbnail.fade_in.interpolate(0.0, 1.0, now)) .scale(thumbnail.zoom.interpolate(1.0, 1.1, now)) .translate(move |bounds, viewport| { - let final_bounds = bounds.zoom(1.1); - final_bounds.offset(&viewport.shrink(10)) + bounds.zoom(1.1).offset(&viewport.shrink(10)) * thumbnail.zoom.interpolate(0.0, 1.0, now) }) .style(move |_theme| image::Style { From 40ed7d8762d89605724176c337c988088fcd1972 Mon Sep 17 00:00:00 2001 From: edwloef Date: Sat, 26 Apr 2025 17:47:11 +0200 Subject: [PATCH 13/13] fix combo-box panic after 6034a0c --- widget/src/combo_box.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index 195fe0cb..9ca92d03 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -509,6 +509,10 @@ where vec![widget::Tree::new(&self.text_input as &dyn Widget<_, _, _>)] } + fn diff(&self, _tree: &mut widget::Tree) { + // do nothing so the children don't get cleared + } + fn update( &mut self, tree: &mut widget::Tree,