Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pop widget #2751

Merged
merged 1 commit into from
Jan 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions core/src/rectangle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
16 changes: 15 additions & 1 deletion widget/src/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions widget/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
232 changes: 232 additions & 0 deletions widget/src/pop.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
//! 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(
tree, 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)
}
}
Loading