Skip to main content

iced_widget/
sticky.rs

1//! Stick elements to the viewport!
2//!
3//! [`Sticky`] keeps its contents visible when they would otherwise go out of
4//! view, for example when they are scrolled out of a
5//! [`Scrollable`](crate::Scrollable). As soon as any of the contents' edges
6//! goes out of the visible bounds, they are moved into an [`overlay`] inside
7//! the visible bounds, so that they stay at the edge of the viewport.
8//!
9//! While they are stuck, the contents keep their original layout: they
10//! simply float in a new position, inside the visible bounds.
11//!
12//! When the edges of the contents reach the edges of their parent, they stay
13//! attached to them: they keep their original bounds, and are clipped to the
14//! visible bounds instead.
15//!
16//! Once the parent of the contents has gone out of the visible bounds, they
17//! stop floating and scroll with it.
18//!
19//! # Example
20//! ```no_run
21//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; pub use iced_widget::core::Length::Fill; }
22//! # use iced::widget::Widget;
23//! # pub type State = ();
24//! use iced::widget::{column, container, scrollable, sticky, space};
25//! use iced::Fill;
26//!
27//! enum Message {
28//!     // ...
29//! }
30//!
31//! fn view(state: &State) -> impl Widget<Message> {
32//!     scrollable(column![
33//!         sticky(container("I always stay in view!").width(Fill).padding(10)),
34//!         space().height(3000),
35//!     ])
36//! }
37//! ```
38use crate::core;
39use crate::core::layout;
40use crate::core::mouse;
41use crate::core::overlay;
42use crate::core::renderer;
43use crate::core::widget;
44use crate::core::window;
45use crate::core::{Event, Layout, Length, Point, Rectangle, Shell, Size, Vector, Widget};
46
47/// A widget that keeps its contents in view.
48///
49/// When the contents of a [`Sticky`] widget would go out of view (for
50/// example, when they are scrolled out of a [`Scrollable`](crate::Scrollable)),
51/// they are moved into an [`overlay`] inside the intersection of the parent's
52/// [`layout.bounds`](Layout::bounds) and the viewport, so that they stay
53/// visible at the edge of the viewport.
54///
55/// The contents start floating as soon as any of their edges goes out of the
56/// visible bounds, and they are always clamped to the visible bounds, so
57/// that they stay inside them.
58///
59/// While they are stuck, the contents keep their original layout: they
60/// simply float in a new position.
61///
62/// When the edges of the contents reach the edges of their parent, they stay
63/// attached to them: they keep their original bounds, and are clipped to the
64/// visible bounds instead.
65///
66/// Once the parent of the contents has gone out of the visible bounds, they
67/// stop floating and scroll with it.
68///
69/// # Example
70/// ```no_run
71/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; pub use iced_widget::core::Length::Fill; }
72/// # use iced::widget::Widget;
73/// # pub type State = ();
74/// use iced::widget::{column, container, scrollable, sticky, space};
75/// use iced::Fill;
76///
77/// enum Message {
78///     // ...
79/// }
80///
81/// fn view(state: &State) -> impl Widget<Message> {
82///     scrollable(column![
83///         sticky(container("I always stay in view!").width(Fill).padding(10)),
84///         space().height(3000),
85///     ])
86/// }
87/// ```
88pub struct Sticky<W> {
89    content: W,
90}
91
92impl<W> Sticky<W> {
93    /// Creates a new [`Sticky`] with the given content.
94    pub fn new(content: W) -> Self {
95        Self { content }
96    }
97}
98
99struct State {
100    is_stuck: bool,
101}
102
103impl<W> widget::Meta for Sticky<W> {}
104
105impl<W, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Sticky<W>
106where
107    Renderer: core::Renderer,
108    W: Widget<Message, Theme, Renderer>,
109{
110    fn tag(&self) -> widget::tree::Tag {
111        widget::tree::Tag::of::<State>()
112    }
113
114    fn state(&self) -> widget::tree::State {
115        widget::tree::State::new(State { is_stuck: false })
116    }
117
118    fn diff(&mut self, tree: &mut widget::Tree) {
119        tree.diff_children(&mut [&mut self.content]);
120    }
121
122    fn size(&self) -> Size<Length> {
123        self.content.size()
124    }
125
126    fn layout(&mut self, tree: &mut widget::Tree, renderer: &Renderer, limits: &layout::Limits) {
127        self.content.layout(&mut tree.children[0], renderer, limits);
128
129        tree.size = tree.children[0].size;
130    }
131
132    fn operate(
133        &mut self,
134        tree: &mut widget::Tree,
135        layout: Layout,
136        viewport: &Rectangle,
137        renderer: &Renderer,
138        operation: &mut dyn widget::Operation,
139    ) {
140        let state = tree.state.downcast_ref::<State>();
141
142        if state.is_stuck {
143            return;
144        }
145
146        self.content
147            .operate(&mut tree.children[0], layout, viewport, renderer, operation);
148    }
149
150    fn update(
151        &mut self,
152        tree: &mut widget::Tree,
153        event: &Event,
154        layout: Layout,
155        cursor: mouse::Cursor,
156        renderer: &Renderer,
157        shell: &mut Shell<'_, Message>,
158        viewport: &Rectangle,
159    ) {
160        let state = tree.state.downcast_mut::<State>();
161
162        if let Event::Window(window::Event::RedrawRequested(_)) = event {
163            let is_stuck = if let Some(parent) = layout.parent()
164                && parent.intersects(viewport)
165                && !layout.bounds().is_within(viewport)
166            {
167                true
168            } else {
169                false
170            };
171
172            if is_stuck != state.is_stuck {
173                state.is_stuck = is_stuck;
174                shell.invalidate_overlay();
175                return; // Avoid propagating a stale redraw
176            }
177        }
178
179        if state.is_stuck {
180            return;
181        }
182
183        self.content.update(
184            &mut tree.children[0],
185            event,
186            layout,
187            cursor,
188            renderer,
189            shell,
190            viewport,
191        );
192    }
193
194    fn mouse_interaction(
195        &self,
196        tree: &widget::Tree,
197        layout: Layout,
198        cursor: mouse::Cursor,
199        viewport: &Rectangle,
200        renderer: &Renderer,
201    ) -> mouse::Interaction {
202        let state = tree.state.downcast_ref::<State>();
203
204        if state.is_stuck {
205            return mouse::Interaction::None;
206        }
207
208        self.content
209            .mouse_interaction(&tree.children[0], layout, cursor, viewport, renderer)
210    }
211
212    fn draw(
213        &self,
214        tree: &widget::Tree,
215        renderer: &mut Renderer,
216        theme: &Theme,
217        style: &renderer::Style,
218        layout: Layout,
219        cursor: mouse::Cursor,
220        viewport: &Rectangle,
221    ) {
222        let state = tree.state.downcast_ref::<State>();
223
224        if state.is_stuck {
225            return;
226        }
227
228        self.content.draw(
229            &tree.children[0],
230            renderer,
231            theme,
232            style,
233            layout,
234            cursor,
235            viewport,
236        );
237    }
238
239    fn overlay<'b>(
240        &'b mut self,
241        tree: &'b mut widget::Tree,
242        layout: Layout,
243        renderer: &Renderer,
244        viewport: &Rectangle,
245        translation: Vector,
246        window: Size,
247    ) -> Vec<overlay::Element<'b, Message, Theme, Renderer>> {
248        let bounds = layout.bounds();
249        let parent = layout.parent();
250        let state = tree.state.downcast_ref::<State>();
251
252        if let Some(parent) = parent
253            && state.is_stuck
254        {
255            let viewport = *viewport + translation;
256            let bounds = bounds + translation;
257            let parent = parent + translation;
258
259            let position = Point::new(
260                stuck_axis(
261                    bounds.x,
262                    bounds.width,
263                    viewport.x,
264                    viewport.width,
265                    (parent.x, parent.x + parent.width),
266                ),
267                stuck_axis(
268                    bounds.y,
269                    bounds.height,
270                    viewport.y,
271                    viewport.height,
272                    (parent.y, parent.y + parent.height),
273                ),
274            );
275
276            vec![overlay::Element::new(Box::new(Overlay {
277                content: &mut self.content,
278                tree: &mut tree.children[0],
279                layout: layout.move_to(position),
280                viewport,
281                window,
282            }))]
283        } else {
284            self.content.overlay(
285                &mut tree.children[0],
286                layout,
287                renderer,
288                viewport,
289                translation,
290                window,
291            )
292        }
293    }
294}
295
296/// Computes the position of the contents along one axis, stuck inside the
297/// visible bounds and the bounds of their parent.
298///
299/// `position` and `size` are the natural position and size of the contents
300/// along the axis, `viewport` and `viewport_size` describe the visible
301/// bounds, and `parent` describes the bounds of the parent.
302///
303/// The returned position keeps the contents inside both the visible bounds
304/// and the bounds of the parent. When that is not possible, because the
305/// contents are larger than their intersection, the contents are kept
306/// attached to the edge of the parent instead, so that they keep their
307/// original bounds.
308fn stuck_axis(
309    position: f32,
310    size: f32,
311    viewport: f32,
312    viewport_size: f32,
313    parent: (f32, f32),
314) -> f32 {
315    let viewport_max = viewport + viewport_size - size;
316    let (parent_min, parent_max) = parent;
317    let parent_max = parent_max - size;
318
319    let min = viewport.max(parent_min);
320    let max = viewport_max.min(parent_max);
321
322    if min <= max {
323        position.clamp(min, max)
324    } else if parent_max < viewport {
325        // The contents are attached to the upper edge of the parent
326        parent_max
327    } else {
328        // The contents are attached to the lower edge of the parent
329        parent_min
330    }
331}
332
333struct Overlay<'b, W> {
334    content: &'b mut W,
335    tree: &'b mut widget::Tree,
336    layout: Layout,
337    viewport: Rectangle,
338    window: Size,
339}
340
341impl<W> Overlay<'_, W> {
342    fn bounds(&self) -> Rectangle {
343        self.viewport
344            .intersection(&self.layout.bounds())
345            .unwrap_or(self.layout.bounds())
346    }
347}
348
349impl<W, Message, Theme, Renderer> core::Overlay<Message, Theme, Renderer> for Overlay<'_, W>
350where
351    Renderer: core::Renderer,
352    W: Widget<Message, Theme, Renderer>,
353{
354    fn operate(&mut self, renderer: &Renderer, operation: &mut dyn widget::Operation) {
355        let bounds = self.bounds();
356
357        self.content
358            .operate(self.tree, self.layout, &bounds, renderer, operation);
359    }
360
361    fn update(
362        &mut self,
363        event: &Event,
364        cursor: mouse::Cursor,
365        renderer: &Renderer,
366        shell: &mut Shell<'_, Message>,
367    ) {
368        let bounds = self.bounds();
369
370        self.content.update(
371            self.tree,
372            event,
373            self.layout,
374            cursor,
375            renderer,
376            shell,
377            &bounds,
378        );
379    }
380
381    fn mouse_interaction(&self, cursor: mouse::Cursor, renderer: &Renderer) -> mouse::Interaction {
382        let bounds = self.bounds();
383
384        let interaction =
385            self.content
386                .mouse_interaction(self.tree, self.layout, cursor, &bounds, renderer);
387
388        if interaction == mouse::Interaction::None && cursor.is_over(bounds) {
389            mouse::Interaction::Idle
390        } else {
391            interaction
392        }
393    }
394
395    fn draw(
396        &self,
397        renderer: &mut Renderer,
398        theme: &Theme,
399        style: &renderer::Style,
400        cursor: mouse::Cursor,
401    ) {
402        let bounds = self.bounds();
403
404        renderer.with_layer(bounds, |renderer| {
405            self.content.draw(
406                self.tree,
407                renderer,
408                theme,
409                style,
410                self.layout,
411                cursor,
412                &bounds,
413            );
414        });
415    }
416
417    fn overlay<'c>(
418        &'c mut self,
419        renderer: &Renderer,
420    ) -> Vec<overlay::Element<'c, Message, Theme, Renderer>> {
421        self.content.overlay(
422            self.tree,
423            self.layout,
424            renderer,
425            &self.viewport,
426            Vector::ZERO,
427            self.window,
428        )
429    }
430}