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,