iced_widget/
pop.rs

1//! Generate messages when content pops in and out of view.
2use crate::core::layout;
3use crate::core::mouse;
4use crate::core::overlay;
5use crate::core::renderer;
6use crate::core::text;
7use crate::core::time::{Duration, Instant};
8use crate::core::widget;
9use crate::core::widget::tree::{self, Tree};
10use crate::core::window;
11use crate::core::{
12    self, Clipboard, Element, Event, Layout, Length, Pixels, Rectangle, Shell,
13    Size, Vector, Widget,
14};
15
16/// A widget that can generate messages when its content pops in and out of view.
17///
18/// It can even notify you with anticipation at a given distance!
19#[allow(missing_debug_implementations)]
20pub struct Pop<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> {
21    content: Element<'a, Message, Theme, Renderer>,
22    key: Option<text::Fragment<'a>>,
23    on_show: Option<Box<dyn Fn(Size) -> Message + 'a>>,
24    on_resize: Option<Box<dyn Fn(Size) -> Message + 'a>>,
25    on_hide: Option<Message>,
26    anticipate: Pixels,
27    delay: Duration,
28}
29
30impl<'a, Message, Theme, Renderer> Pop<'a, Message, Theme, Renderer>
31where
32    Renderer: core::Renderer,
33    Message: Clone,
34{
35    /// Creates a new [`Pop`] widget with the given content.
36    pub fn new(
37        content: impl Into<Element<'a, Message, Theme, Renderer>>,
38    ) -> Self {
39        Self {
40            content: content.into(),
41            key: None,
42            on_show: None,
43            on_resize: None,
44            on_hide: None,
45            anticipate: Pixels::ZERO,
46            delay: Duration::ZERO,
47        }
48    }
49
50    /// Sets the message to be produced when the content pops into view.
51    ///
52    /// The closure will receive the [`Size`] of the content in that moment.
53    pub fn on_show(mut self, on_show: impl Fn(Size) -> Message + 'a) -> Self {
54        self.on_show = Some(Box::new(on_show));
55        self
56    }
57
58    /// Sets the message to be produced when the content changes [`Size`] once its in view.
59    ///
60    /// The closure will receive the new [`Size`] of the content.
61    pub fn on_resize(
62        mut self,
63        on_resize: impl Fn(Size) -> Message + 'a,
64    ) -> Self {
65        self.on_resize = Some(Box::new(on_resize));
66        self
67    }
68
69    /// Sets the message to be produced when the content pops out of view.
70    pub fn on_hide(mut self, on_hide: Message) -> Self {
71        self.on_hide = Some(on_hide);
72        self
73    }
74
75    /// Sets the key of the [`Pop`] widget, for continuity.
76    ///
77    /// If the key changes, the [`Pop`] widget will trigger again.
78    pub fn key(mut self, key: impl text::IntoFragment<'a>) -> Self {
79        self.key = Some(key.into_fragment());
80        self
81    }
82
83    /// Sets the distance in [`Pixels`] to use in anticipation of the
84    /// content popping into view.
85    ///
86    /// This can be quite useful to lazily load items in a long scrollable
87    /// behind the scenes before the user can notice it!
88    pub fn anticipate(mut self, distance: impl Into<Pixels>) -> Self {
89        self.anticipate = distance.into();
90        self
91    }
92
93    /// Sets the amount of time to wait before firing an [`on_show`] or
94    /// [`on_hide`] event; after the content is shown or hidden.
95    ///
96    /// When combined with [`key`], this can be useful to debounce key changes.
97    ///
98    /// [`on_show`]: Self::on_show
99    /// [`on_hide`]: Self::on_hide
100    /// [`key`]: Self::key
101    pub fn delay(mut self, delay: impl Into<Duration>) -> Self {
102        self.delay = delay.into();
103        self
104    }
105}
106
107#[derive(Debug, Clone, Default)]
108struct State {
109    has_popped_in: bool,
110    should_notify_at: Option<(bool, Instant)>,
111    last_size: Option<Size>,
112    last_key: Option<String>,
113}
114
115impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
116    for Pop<'_, Message, Theme, Renderer>
117where
118    Message: Clone,
119    Renderer: core::Renderer,
120{
121    fn tag(&self) -> tree::Tag {
122        tree::Tag::of::<State>()
123    }
124
125    fn state(&self) -> tree::State {
126        tree::State::new(State::default())
127    }
128
129    fn children(&self) -> Vec<Tree> {
130        vec![Tree::new(&self.content)]
131    }
132
133    fn diff(&self, tree: &mut Tree) {
134        tree.diff_children(&[&self.content]);
135    }
136
137    fn update(
138        &mut self,
139        tree: &mut Tree,
140        event: &Event,
141        layout: Layout<'_>,
142        cursor: mouse::Cursor,
143        renderer: &Renderer,
144        clipboard: &mut dyn Clipboard,
145        shell: &mut Shell<'_, Message>,
146        viewport: &Rectangle,
147    ) {
148        if let Event::Window(window::Event::RedrawRequested(now)) = &event {
149            let state = tree.state.downcast_mut::<State>();
150
151            if state.has_popped_in
152                && state.last_key.as_deref() != self.key.as_deref()
153            {
154                state.has_popped_in = false;
155                state.should_notify_at = None;
156                state.last_key =
157                    self.key.as_ref().cloned().map(text::Fragment::into_owned);
158            }
159
160            let bounds = layout.bounds();
161            let top_left_distance = viewport.distance(bounds.position());
162
163            let bottom_right_distance = viewport
164                .distance(bounds.position() + Vector::from(bounds.size()));
165
166            let distance = top_left_distance.min(bottom_right_distance);
167
168            if state.has_popped_in {
169                if distance <= self.anticipate.0 {
170                    if let Some(on_resize) = &self.on_resize {
171                        let size = bounds.size();
172
173                        if Some(size) != state.last_size {
174                            state.last_size = Some(size);
175                            shell.publish(on_resize(size));
176                        }
177                    }
178                } else if self.on_hide.is_some() {
179                    state.has_popped_in = false;
180                    state.should_notify_at = Some((false, *now + self.delay));
181                }
182            } else if self.on_show.is_some() && distance <= self.anticipate.0 {
183                let size = bounds.size();
184
185                state.has_popped_in = true;
186                state.should_notify_at = Some((true, *now + self.delay));
187                state.last_size = Some(size);
188            }
189
190            match &state.should_notify_at {
191                Some((has_popped_in, at)) if at <= now => {
192                    if *has_popped_in {
193                        if let Some(on_show) = &self.on_show {
194                            shell.publish(on_show(layout.bounds().size()));
195                        }
196                    } else if let Some(on_hide) = &self.on_hide {
197                        shell.publish(on_hide.clone());
198                    }
199
200                    state.should_notify_at = None;
201                }
202                Some((_, at)) => {
203                    shell.request_redraw_at(*at);
204                }
205                None => {}
206            }
207        }
208
209        self.content.as_widget_mut().update(
210            &mut tree.children[0],
211            event,
212            layout,
213            cursor,
214            renderer,
215            clipboard,
216            shell,
217            viewport,
218        );
219    }
220
221    fn size(&self) -> Size<Length> {
222        self.content.as_widget().size()
223    }
224
225    fn size_hint(&self) -> Size<Length> {
226        self.content.as_widget().size_hint()
227    }
228
229    fn layout(
230        &self,
231        tree: &mut Tree,
232        renderer: &Renderer,
233        limits: &layout::Limits,
234    ) -> layout::Node {
235        self.content
236            .as_widget()
237            .layout(&mut tree.children[0], renderer, limits)
238    }
239
240    fn draw(
241        &self,
242        tree: &Tree,
243        renderer: &mut Renderer,
244        theme: &Theme,
245        style: &renderer::Style,
246        layout: layout::Layout<'_>,
247        cursor: mouse::Cursor,
248        viewport: &Rectangle,
249    ) {
250        self.content.as_widget().draw(
251            &tree.children[0],
252            renderer,
253            theme,
254            style,
255            layout,
256            cursor,
257            viewport,
258        );
259    }
260
261    fn operate(
262        &self,
263        tree: &mut Tree,
264        layout: core::Layout<'_>,
265        renderer: &Renderer,
266        operation: &mut dyn widget::Operation,
267    ) {
268        self.content.as_widget().operate(
269            &mut tree.children[0],
270            layout,
271            renderer,
272            operation,
273        );
274    }
275
276    fn mouse_interaction(
277        &self,
278        tree: &Tree,
279        layout: core::Layout<'_>,
280        cursor: mouse::Cursor,
281        viewport: &Rectangle,
282        renderer: &Renderer,
283    ) -> mouse::Interaction {
284        self.content.as_widget().mouse_interaction(
285            &tree.children[0],
286            layout,
287            cursor,
288            viewport,
289            renderer,
290        )
291    }
292
293    fn overlay<'b>(
294        &'b mut self,
295        tree: &'b mut Tree,
296        layout: core::Layout<'_>,
297        renderer: &Renderer,
298        translation: core::Vector,
299    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
300        self.content.as_widget_mut().overlay(
301            &mut tree.children[0],
302            layout,
303            renderer,
304            translation,
305        )
306    }
307}
308
309impl<'a, Message, Theme, Renderer> From<Pop<'a, Message, Theme, Renderer>>
310    for Element<'a, Message, Theme, Renderer>
311where
312    Renderer: core::Renderer + 'a,
313    Theme: 'a,
314    Message: Clone + 'a,
315{
316    fn from(pop: Pop<'a, Message, Theme, Renderer>) -> Self {
317        Element::new(pop)
318    }
319}