Skip to main content

iced_widget/
popover.rs

1//! A popover is a floating piece of content that appears over some element.
2//!
3//! Unlike a [`tooltip`], a popover is not shown on hover, and it does not
4//! control its own visibility. Instead, the `popover` argument is an
5//! `Option`: `Some` displays the overlay (open), and `None` hides it (closed).
6//! The base is always present.
7//!
8//! When the user clicks outside of the popover's bounds, the popover notifies
9//! the application through its `on_close` handler, which is typically how the
10//! application decides to set the `popover` argument to `None` (i.e. to close
11//! it).
12//!
13//! The [`popover()`] helper wraps the popup in [`opaque()`], so mouse button
14//! presses inside the popup's bounds are captured and do not pass through to
15//! the elements below. Use [`Popover::passthrough`] to let them fall through
16//! to the base layer, and [`Popover::capture_scroll`] to also capture scroll
17//! wheel events over the popup.
18//!
19//! [`tooltip`]: crate::tooltip::Tooltip
20//! [`popover()`]: crate::popover()
21//! [`opaque()`]: crate::opaque()
22//!
23//! # Example
24//! ```no_run
25//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
26//! # use iced::widget::Widget;
27//! # pub struct State {
28//! #     is_open: bool,
29//! # }
30//! use iced::widget::{button, container, popover, text};
31//!
32//! #[derive(Clone)]
33//! enum Message {
34//!     Close,
35//! }
36//!
37//! fn view(state: &State) -> impl Widget<Message> {
38//!     // The base is always present. The `popover` argument is `Some` when the
39//!     // popover is open and `None` when it is closed.
40//!     popover(
41//!         button(text("Click me!")).on_press(Message::Close),
42//!         state.is_open.then(|| {
43//!             container(text("This is the popover contents!")).padding(10)
44//!         }),
45//!     )
46//!     .position(popover::Position::Bottom)
47//!     .on_close(Message::Close)
48//! }
49//! ```
50use crate::Opaque;
51use crate::core::layout::{self, Layout};
52use crate::core::mouse;
53use crate::core::overlay;
54use crate::core::renderer;
55use crate::core::touch;
56use crate::core::widget::{self, Widget};
57use crate::core::{Event, Length, Pixels, Point, Rectangle, Shell, Size, Vector};
58
59/// A floating piece of content that appears over another element.
60///
61/// The popover does not control its own visibility. Instead, the `popover`
62/// argument is an `Option`: `Some` displays the overlay (open), and `None`
63/// hides it (closed). The base is always present.
64///
65/// When the user clicks outside of the popover's bounds, the popover notifies
66/// the application through its `on_close` handler.
67///
68/// The [`popover()`] helper wraps the popup in [`opaque()`], so mouse button
69/// presses inside the popup's bounds are captured and do not pass through to
70/// the elements below. Use [`Self::passthrough`] to let them fall through to
71/// the base layer, and [`Self::capture_scroll`] to also capture scroll wheel
72/// events over the popup.
73///
74/// [`popover()`]: crate::popover()
75/// [`opaque()`]: crate::opaque()
76///
77/// # Example
78/// ```no_run
79/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
80/// # use iced::widget::Widget;
81/// # pub struct State {
82/// #     is_open: bool,
83/// # }
84/// use iced::widget::{button, container, popover, text};
85///
86/// #[derive(Clone)]
87/// enum Message {
88///     Close,
89/// }
90///
91/// fn view(state: &State) -> impl Widget<Message> {
92///     // The base is always present. The `popover` argument is `Some` when the
93///     // popover is open and `None` when it is closed.
94///     popover(
95///         button(text("Click me!")).on_press(Message::Close),
96///         state.is_open.then(|| {
97///             container(text("This is the popover contents!")).padding(10)
98///         }),
99///     )
100///     .position(popover::Position::Bottom)
101///     .on_close(Message::Close)
102/// }
103/// ```
104pub struct Popover<W, V, Message> {
105    content: W,
106    popup: Option<V>,
107    position: Position,
108    gap: f32,
109    snap_within_viewport: bool,
110    on_close: Option<Message>,
111}
112
113impl<W, V, Message> Popover<W, V, Message> {
114    /// Creates a new [`Popover`].
115    ///
116    /// It expects:
117    ///   * the `content` widget that the popover is anchored to (the base), and
118    ///   * the optional `popup` widget to display: `Some` when the popover is
119    ///     open and `None` when it is closed.
120    ///
121    /// The base is always present; it is the `popup` argument that controls
122    /// whether the overlay is displayed. By default, the [`Popover`] is
123    /// positioned [`Position::Auto`]; use the [`Self::position`] method to set
124    /// a specific position.
125    pub fn new(content: W, popup: Option<V>) -> Self {
126        Popover {
127            content,
128            popup,
129            position: Position::default(),
130            gap: 0.0,
131            snap_within_viewport: true,
132            on_close: None,
133        }
134    }
135
136    /// Sets the message that will be produced when the user clicks outside of
137    /// the [`Popover`]'s bounds.
138    ///
139    /// This is typically how the application decides to close the popover, by
140    /// setting the `popover` argument to `None`.
141    pub fn on_close(mut self, message: Message) -> Self {
142        self.on_close = Some(message);
143        self
144    }
145
146    /// Sets the [`Position`] of the [`Popover`].
147    ///
148    /// By default, the [`Popover`] is positioned [`Position::Auto`], which
149    /// places it on the side of the base with the most available space.
150    pub fn position(mut self, position: Position) -> Self {
151        self.position = position;
152        self
153    }
154
155    /// Sets the gap between the content and its [`Popover`].
156    pub fn gap(mut self, gap: impl Into<Pixels>) -> Self {
157        self.gap = gap.into().0;
158        self
159    }
160
161    /// Sets whether the [`Popover`] is snapped within the viewport.
162    pub fn snap_within_viewport(mut self, snap: bool) -> Self {
163        self.snap_within_viewport = snap;
164        self
165    }
166}
167
168impl<W, V, Message> Popover<W, Opaque<V>, Message> {
169    /// Makes interactions on the popup of the [`Popover`] fall through
170    /// to the base layer.
171    pub fn passthrough(self) -> Popover<W, V, Message> {
172        Popover {
173            content: self.content,
174            popup: self.popup.map(Opaque::into_inner),
175            position: self.position,
176            gap: self.gap,
177            snap_within_viewport: self.snap_within_viewport,
178            on_close: self.on_close,
179        }
180    }
181
182    /// Sets whether the scroll wheel should be captured when the cursor is
183    /// over the bounds of the popup.
184    ///
185    /// By default, only mouse button presses are captured. Enabling this flag
186    /// also captures scroll wheel events, preventing them from passing through
187    /// to the elements below.
188    pub fn capture_scroll(mut self, capture: bool) -> Self {
189        self.popup = self
190            .popup
191            .take()
192            .map(|popup| Opaque::new(popup.into_inner()).capture_scroll(capture));
193        self
194    }
195}
196
197impl<W, V, Message> widget::Meta for Popover<W, V, Message> {}
198
199impl<W, V, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Popover<W, V, Message>
200where
201    W: Widget<Message, Theme, Renderer>,
202    V: Widget<Message, Theme, Renderer>,
203    Message: Clone,
204    Renderer: crate::core::Renderer,
205{
206    fn tag(&self) -> widget::tree::Tag {
207        widget::tree::Tag::of::<State>()
208    }
209
210    fn state(&self) -> widget::tree::State {
211        widget::tree::State::new(State {
212            needs_relayout: true,
213        })
214    }
215
216    fn diff(&mut self, tree: &mut widget::Tree) {
217        // The popup's contents may have changed, so the cached layout
218        // (if any) is no longer valid
219        tree.state.downcast_mut::<State>().needs_relayout = true;
220
221        if tree.children.len() != 2 {
222            tree.children = vec![
223                widget::Tree::new(&self.content),
224                widget::Tree::new(&self.popup),
225            ];
226        }
227
228        tree.children[0].diff(&mut self.content);
229        tree.children[1].diff(&mut self.popup);
230    }
231
232    fn size(&self) -> Size<Length> {
233        self.content.size()
234    }
235
236    fn layout(&mut self, tree: &mut widget::Tree, renderer: &Renderer, limits: &layout::Limits) {
237        self.content.layout(&mut tree.children[0], renderer, limits);
238
239        tree.size = tree.children[0].size;
240    }
241
242    fn update(
243        &mut self,
244        tree: &mut widget::Tree,
245        event: &Event,
246        layout: Layout,
247        cursor: mouse::Cursor,
248        renderer: &Renderer,
249        shell: &mut Shell<'_, Message>,
250        viewport: &Rectangle,
251    ) {
252        self.content.update(
253            &mut tree.children[0],
254            event,
255            layout,
256            cursor,
257            renderer,
258            shell,
259            viewport,
260        );
261    }
262
263    fn draw(
264        &self,
265        tree: &widget::Tree,
266        renderer: &mut Renderer,
267        theme: &Theme,
268        style: &renderer::Style,
269        layout: Layout,
270        cursor: mouse::Cursor,
271        viewport: &Rectangle,
272    ) {
273        self.content.draw(
274            &tree.children[0],
275            renderer,
276            theme,
277            style,
278            layout,
279            cursor,
280            viewport,
281        );
282    }
283
284    fn mouse_interaction(
285        &self,
286        tree: &widget::Tree,
287        layout: Layout,
288        cursor: mouse::Cursor,
289        viewport: &Rectangle,
290        renderer: &Renderer,
291    ) -> mouse::Interaction {
292        self.content
293            .mouse_interaction(&tree.children[0], layout, cursor, viewport, renderer)
294    }
295
296    fn operate(
297        &mut self,
298        tree: &mut widget::Tree,
299        layout: Layout,
300        viewport: &Rectangle,
301        renderer: &Renderer,
302        operation: &mut dyn widget::Operation,
303    ) {
304        operation.container(None, layout.bounds(), viewport);
305        operation.traverse(&mut |operation| {
306            self.content
307                .operate(&mut tree.children[0], layout, viewport, renderer, operation);
308        });
309    }
310
311    fn overlay<'b>(
312        &'b mut self,
313        tree: &'b mut widget::Tree,
314        layout: Layout,
315        renderer: &Renderer,
316        viewport: &Rectangle,
317        translation: Vector,
318        window: Size,
319    ) -> Vec<overlay::Element<'b, Message, Theme, Renderer>> {
320        let mut children = tree.children.iter_mut();
321
322        let base = children.next().unwrap();
323        let popup_tree = children.next();
324
325        let mut overlays =
326            self.content
327                .overlay(base, layout, renderer, viewport, translation, window);
328
329        let viewport = *viewport + translation;
330
331        if let (Some(popup), Some(popup_tree)) = (self.popup.as_mut(), popup_tree) {
332            let state = tree.state.downcast_mut::<State>();
333
334            // (Re)compute the popup's layout if it was invalidated by
335            // `Widget::diff`
336            if state.needs_relayout {
337                popup.layout(
338                    popup_tree,
339                    renderer,
340                    &layout::Limits::new(
341                        Size::ZERO,
342                        if self.snap_within_viewport {
343                            viewport.size()
344                        } else {
345                            Size::INFINITE
346                        },
347                    ),
348                );
349
350                state.needs_relayout = false;
351            }
352
353            let popup_size = popup_tree.size;
354
355            let content_bounds = layout.bounds() + translation;
356
357            let popup_bounds = crate::overlay::Position::from(self.position).resolve(
358                content_bounds,
359                popup_size,
360                self.gap,
361                Point::ORIGIN,
362                viewport,
363                self.snap_within_viewport,
364            );
365
366            overlays.push(overlay::Element::new(Box::new(Overlay {
367                content: popup,
368                tree: popup_tree,
369                layout: Layout::new(popup_size).move_to(popup_bounds.position()),
370                content_bounds,
371                on_close: self.on_close.clone(),
372                viewport,
373                window,
374            })));
375        }
376
377        overlays
378    }
379}
380
381/// The position of the popover.
382#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
383pub enum Position {
384    /// The popover will appear on the side of the widget with the most
385    /// available space.
386    #[default]
387    Auto,
388    /// The popover will appear on the top of the widget.
389    Top,
390    /// The popover will appear on the bottom of the widget.
391    Bottom,
392    /// The popover will appear on the left of the widget.
393    Left,
394    /// The popover will appear on the right of the widget.
395    Right,
396}
397
398impl From<Position> for crate::overlay::Position {
399    fn from(position: Position) -> Self {
400        match position {
401            Position::Auto => Self::Auto {
402                preference: crate::overlay::Side::default(),
403            },
404            Position::Top => Self::Top,
405            Position::Bottom => Self::Bottom,
406            Position::Left => Self::Left,
407            Position::Right => Self::Right,
408        }
409    }
410}
411
412struct State {
413    needs_relayout: bool,
414}
415
416struct Overlay<'b, V, Message> {
417    content: &'b mut V,
418    tree: &'b mut widget::Tree,
419    layout: Layout,
420    content_bounds: Rectangle,
421    on_close: Option<Message>,
422    viewport: Rectangle,
423    window: Size,
424}
425
426impl<V, Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer>
427    for Overlay<'_, V, Message>
428where
429    V: Widget<Message, Theme, Renderer>,
430    Renderer: crate::core::Renderer,
431{
432    fn update(
433        &mut self,
434        event: &Event,
435        cursor: mouse::Cursor,
436        renderer: &Renderer,
437        shell: &mut Shell<'_, Message>,
438    ) {
439        let cursor_position = cursor.position();
440
441        let is_inside =
442            cursor_position.is_some_and(|position| self.layout.bounds().contains(position));
443
444        let is_over_base =
445            cursor_position.is_some_and(|position| self.content_bounds.contains(position));
446
447        if matches!(
448            event,
449            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
450                | Event::Touch(touch::Event::FingerPressed { .. })
451        ) && !is_inside
452        {
453            if is_over_base {
454                // The base is responsible for its own behavior, so let the
455                // event reach it.
456                return;
457            }
458
459            // The user clicked outside of the popover: notify the application
460            // of a close request.
461            if let Some(on_close) = self.on_close.take() {
462                shell.publish(on_close);
463            }
464
465            return;
466        }
467
468        self.content.update(
469            self.tree,
470            event,
471            self.layout,
472            cursor,
473            renderer,
474            shell,
475            &self.viewport,
476        );
477    }
478
479    fn draw(
480        &self,
481        renderer: &mut Renderer,
482        theme: &Theme,
483        style: &renderer::Style,
484        cursor_position: mouse::Cursor,
485    ) {
486        renderer.with_layer(self.viewport, |renderer| {
487            self.content.draw(
488                self.tree,
489                renderer,
490                theme,
491                style,
492                self.layout,
493                cursor_position,
494                &self.viewport,
495            );
496        });
497    }
498
499    fn mouse_interaction(&self, cursor: mouse::Cursor, renderer: &Renderer) -> mouse::Interaction {
500        if !cursor.is_over(self.layout.bounds()) {
501            return mouse::Interaction::None;
502        }
503
504        self.content
505            .mouse_interaction(self.tree, self.layout, cursor, &self.viewport, renderer)
506    }
507
508    fn operate(&mut self, renderer: &Renderer, operation: &mut dyn widget::Operation) {
509        self.content
510            .operate(self.tree, self.layout, &self.viewport, renderer, operation);
511    }
512
513    fn overlay<'c>(
514        &'c mut self,
515        renderer: &Renderer,
516    ) -> Vec<overlay::Element<'c, Message, Theme, Renderer>> {
517        self.content.overlay(
518            self.tree,
519            self.layout,
520            renderer,
521            &self.viewport,
522            Vector::ZERO,
523            self.window,
524        )
525    }
526
527    /// Draws the popover on top of other overlays.
528    fn index(&self) -> f32 {
529        2.0
530    }
531}