diff options
-rw-r--r-- | core/src/mouse/click.rs | 2 | ||||
-rw-r--r-- | core/src/rectangle.rs | 14 | ||||
-rw-r--r-- | wgpu/src/lib.rs | 21 | ||||
-rw-r--r-- | widget/src/helpers.rs | 16 | ||||
-rw-r--r-- | widget/src/image.rs | 25 | ||||
-rw-r--r-- | widget/src/lib.rs | 3 | ||||
-rw-r--r-- | widget/src/mouse_area.rs | 2 | ||||
-rw-r--r-- | widget/src/pop.rs | 239 |
8 files changed, 309 insertions, 13 deletions
diff --git a/core/src/mouse/click.rs b/core/src/mouse/click.rs index 0a373878..dd1c84cd 100644 --- a/core/src/mouse/click.rs +++ b/core/src/mouse/click.rs @@ -13,7 +13,7 @@ pub struct Click { } /// The kind of mouse click. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Kind { /// A single click Single, diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index cff33991..14d2a2e8 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -143,6 +143,20 @@ impl Rectangle<f32> { && point.y < self.y + self.height } + /// Returns the minimum distance from the given [`Point`] to any of the edges + /// of the [`Rectangle`]. + pub fn distance(&self, point: Point) -> f32 { + let center = self.center(); + + let distance_x = + ((point.x - center.x).abs() - self.width / 2.0).max(0.0); + + let distance_y = + ((point.y - center.y).abs() - self.height / 2.0).max(0.0); + + distance_x.hypot(distance_y) + } + /// Returns true if the current [`Rectangle`] is completely within the given /// `container`. pub fn is_within(&self, container: &Rectangle) -> bool { diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 63667ada..2283cf71 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -145,7 +145,19 @@ impl Renderer { self.text_viewport.update(queue, viewport.physical_size()); + let physical_bounds = Rectangle::<f32>::from(Rectangle::with_size( + viewport.physical_size(), + )); + for layer in self.layers.iter_mut() { + if physical_bounds + .intersection(&(layer.bounds * scale_factor)) + .and_then(Rectangle::snap) + .is_none() + { + continue; + } + if !layer.quads.is_empty() { engine.quad_pipeline.prepare( device, @@ -268,16 +280,13 @@ impl Renderer { let scale = Transformation::scale(scale_factor); for layer in self.layers.iter() { - let Some(physical_bounds) = - physical_bounds.intersection(&(layer.bounds * scale)) + let Some(scissor_rect) = physical_bounds + .intersection(&(layer.bounds * scale_factor)) + .and_then(Rectangle::snap) else { continue; }; - let Some(scissor_rect) = physical_bounds.snap() else { - continue; - }; - if !layer.quads.is_empty() { engine.quad_pipeline.render( quad_layer, diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index b1e02943..17cf94cc 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -24,7 +24,7 @@ use crate::text_input::{self, TextInput}; use crate::toggler::{self, Toggler}; use crate::tooltip::{self, Tooltip}; use crate::vertical_slider::{self, VerticalSlider}; -use crate::{Column, MouseArea, Pin, Row, Space, Stack, Themer}; +use crate::{Column, MouseArea, Pin, Pop, Row, Space, Stack, Themer}; use std::borrow::Borrow; use std::ops::RangeInclusive; @@ -970,6 +970,20 @@ where }) } +/// Creates a new [`Pop`] widget. +/// +/// A [`Pop`] widget can generate messages when it pops in and out of view. +/// It can even notify you with anticipation at a given distance! +pub fn pop<'a, Message, Theme, Renderer>( + content: impl Into<Element<'a, Message, Theme, Renderer>>, +) -> Pop<'a, Message, Theme, Renderer> +where + Renderer: core::Renderer, + Message: Clone, +{ + Pop::new(content) +} + /// Creates a new [`Scrollable`] with the provided content. /// /// Scrollables let users navigate an endless amount of content with a scrollbar. diff --git a/widget/src/image.rs b/widget/src/image.rs index c8f2a620..6c84ec92 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -63,6 +63,7 @@ pub struct Image<Handle = image::Handle> { filter_method: FilterMethod, rotation: Rotation, opacity: f32, + scale: f32, } impl<Handle> Image<Handle> { @@ -76,6 +77,7 @@ impl<Handle> Image<Handle> { filter_method: FilterMethod::default(), rotation: Rotation::default(), opacity: 1.0, + scale: 1.0, } } @@ -119,6 +121,15 @@ impl<Handle> Image<Handle> { self.opacity = opacity.into(); self } + + /// Sets the scale of the [`Image`]. + /// + /// The region of the [`Image`] drawn will be scaled from the center by the given scale factor. + /// This can be useful to create certain effects and animations, like smooth zoom in / out. + pub fn scale(mut self, scale: impl Into<f32>) -> Self { + self.scale = scale.into(); + self + } } /// Computes the layout of an [`Image`]. @@ -167,11 +178,13 @@ where pub fn draw<Renderer, Handle>( renderer: &mut Renderer, layout: Layout<'_>, + viewport: &Rectangle, handle: &Handle, content_fit: ContentFit, filter_method: FilterMethod, rotation: Rotation, opacity: f32, + scale: f32, ) where Renderer: image::Renderer<Handle = Handle>, Handle: Clone, @@ -183,12 +196,12 @@ pub fn draw<Renderer, Handle>( let bounds = layout.bounds(); let adjusted_fit = content_fit.fit(rotated_size, bounds.size()); - let scale = Vector::new( + let fit_scale = Vector::new( adjusted_fit.width / rotated_size.width, adjusted_fit.height / rotated_size.height, ); - let final_size = image_size * scale; + let final_size = image_size * fit_scale * scale; let position = match content_fit { ContentFit::None => Point::new( @@ -218,7 +231,9 @@ pub fn draw<Renderer, Handle>( if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height { - renderer.with_layer(bounds, render); + if let Some(bounds) = bounds.intersection(viewport) { + renderer.with_layer(bounds, render); + } } else { render(renderer); } @@ -262,16 +277,18 @@ where _style: &renderer::Style, layout: Layout<'_>, _cursor: mouse::Cursor, - _viewport: &Rectangle, + viewport: &Rectangle, ) { draw( renderer, layout, + viewport, &self.handle, self.content_fit, self.filter_method, self.rotation, self.opacity, + self.scale, ); } } diff --git a/widget/src/lib.rs b/widget/src/lib.rs index 38c9929a..b8cfa98f 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -25,6 +25,7 @@ pub mod keyed; pub mod overlay; pub mod pane_grid; pub mod pick_list; +pub mod pop; pub mod progress_bar; pub mod radio; pub mod rule; @@ -66,6 +67,8 @@ pub use pick_list::PickList; #[doc(no_inline)] pub use pin::Pin; #[doc(no_inline)] +pub use pop::Pop; +#[doc(no_inline)] pub use progress_bar::ProgressBar; #[doc(no_inline)] pub use radio::Radio; diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index bdc81bdf..9ba3cff5 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -383,7 +383,7 @@ fn update<Message: Clone, Theme, Renderer>( state.previous_click, ); - if matches!(new_click.kind(), mouse::click::Kind::Double) { + if new_click.kind() == mouse::click::Kind::Double { shell.publish(message.clone()); } diff --git a/widget/src/pop.rs b/widget/src/pop.rs new file mode 100644 index 00000000..a8738aa2 --- /dev/null +++ b/widget/src/pop.rs @@ -0,0 +1,239 @@ +//! Generate messages when content pops in and out of view. +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::{self, Tree}; +use crate::core::window; +use crate::core::{ + self, Clipboard, Element, Event, Layout, Length, Pixels, Rectangle, Shell, + Size, Vector, Widget, +}; + +/// A widget that can generate messages when its content pops in and out of view. +/// +/// It can even notify you with anticipation at a given distance! +#[allow(missing_debug_implementations)] +pub struct Pop<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> { + content: Element<'a, Message, Theme, Renderer>, + on_show: Option<Message>, + on_hide: Option<Message>, + anticipate: Pixels, +} + +impl<'a, Message, Theme, Renderer> Pop<'a, Message, Theme, Renderer> +where + Renderer: core::Renderer, + Message: Clone, +{ + /// Creates a new [`Pop`] widget with the given content. + pub fn new( + content: impl Into<Element<'a, Message, Theme, Renderer>>, + ) -> Self { + Self { + content: content.into(), + on_show: None, + on_hide: None, + anticipate: Pixels::ZERO, + } + } + + /// Sets the message to be produced when the content pops into view. + pub fn on_show(mut self, on_show: Message) -> Self { + self.on_show = Some(on_show); + self + } + + /// Sets the message to be produced when the content pops out of view. + pub fn on_hide(mut self, on_hide: Message) -> Self { + self.on_hide = Some(on_hide); + self + } + + /// Sets the distance in [`Pixels`] to use in anticipation of the + /// content popping into view. + /// + /// This can be quite useful to lazily load items in a long scrollable + /// behind the scenes before the user can notice it! + pub fn anticipate(mut self, distance: impl Into<Pixels>) -> Self { + self.anticipate = distance.into(); + self + } +} + +#[derive(Debug, Clone, Copy, Default)] +struct State { + has_popped_in: bool, +} + +impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for Pop<'_, Message, Theme, Renderer> +where + Message: Clone, + Renderer: core::Renderer, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State>() + } + + fn state(&self) -> tree::State { + tree::State::new(State::default()) + } + + fn children(&self) -> Vec<Tree> { + vec![Tree::new(&self.content)] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(&[&self.content]); + } + + fn update( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) { + if let Event::Window(window::Event::RedrawRequested(_)) = &event { + let state = tree.state.downcast_mut::<State>(); + let bounds = layout.bounds(); + + let top_left_distance = viewport.distance(bounds.position()); + + let bottom_right_distance = viewport + .distance(bounds.position() + Vector::from(bounds.size())); + + let distance = top_left_distance.min(bottom_right_distance); + + if state.has_popped_in { + if let Some(on_hide) = &self.on_hide { + if distance > self.anticipate.0 { + state.has_popped_in = false; + shell.publish(on_hide.clone()); + } + } + } else if let Some(on_show) = &self.on_show { + if distance <= self.anticipate.0 { + state.has_popped_in = true; + shell.publish(on_show.clone()); + } + } + } + + self.content.as_widget_mut().update( + &mut tree.children[0], + event, + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } + + fn size(&self) -> Size<Length> { + self.content.as_widget().size() + } + + fn size_hint(&self) -> Size<Length> { + self.content.as_widget().size_hint() + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.content + .as_widget() + .layout(&mut tree.children[0], renderer, limits) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: layout::Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + layout, + cursor, + viewport, + ); + } + + fn operate( + &self, + tree: &mut Tree, + layout: core::Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation, + ) { + self.content.as_widget().operate( + &mut tree.children[0], + layout, + renderer, + operation, + ); + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: core::Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor, + viewport, + renderer, + ) + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: core::Layout<'_>, + renderer: &Renderer, + translation: core::Vector, + ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> { + self.content.as_widget_mut().overlay( + &mut tree.children[0], + layout, + renderer, + translation, + ) + } +} + +impl<'a, Message, Theme, Renderer> From<Pop<'a, Message, Theme, Renderer>> + for Element<'a, Message, Theme, Renderer> +where + Renderer: core::Renderer + 'a, + Theme: 'a, + Message: Clone + 'a, +{ + fn from(pop: Pop<'a, Message, Theme, Renderer>) -> Self { + Element::new(pop) + } +} |