Skip to main content

cosmic/widget/
popover.rs

1// Copyright 2022 System76 <info@system76.com>
2// SPDX-License-Identifier: MPL-2.0
3
4//! A container which displays an overlay when a popup widget is attached.
5
6use iced::widget;
7use iced_core::event::{self, Event};
8use iced_core::widget::{Operation, Tree};
9use iced_core::{
10    Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Size, Vector, Widget, layout,
11    mouse, overlay, renderer, touch,
12};
13
14pub use iced_widget::container::{Catalog, Style};
15
16pub fn popover<'a, Message, Renderer>(
17    content: impl Into<Element<'a, Message, crate::Theme, Renderer>>,
18) -> Popover<'a, Message, Renderer> {
19    Popover::new(content)
20}
21
22#[derive(Clone, Copy, Debug, Default)]
23pub enum Position {
24    #[default]
25    Center,
26    Bottom,
27    Top,
28    Point(Point),
29}
30
31/// A container which displays overlays when a popup widget is assigned.
32#[must_use]
33pub struct Popover<'a, Message, Renderer> {
34    id: widget::Id,
35    content: Element<'a, Message, crate::Theme, Renderer>,
36    modal: bool,
37    popup: Option<Element<'a, Message, crate::Theme, Renderer>>,
38    position: Position,
39    on_close: Option<Message>,
40}
41
42impl<'a, Message, Renderer> Popover<'a, Message, Renderer> {
43    pub fn new(content: impl Into<Element<'a, Message, crate::Theme, Renderer>>) -> Self {
44        Self {
45            id: widget::Id::unique(),
46            content: content.into(),
47            modal: false,
48            popup: None,
49            position: Position::Center,
50            on_close: None,
51        }
52    }
53
54    /// Set the Id
55    #[inline]
56    pub fn id(mut self, id: widget::Id) -> Self {
57        self.id = id;
58        self
59    }
60
61    /// A modal popup intercepts user inputs while a popup is active.
62    #[inline]
63    pub fn modal(mut self, modal: bool) -> Self {
64        self.modal = modal;
65        self
66    }
67
68    /// Emitted when the popup is closed.
69    #[inline]
70    pub fn on_close(mut self, on_close: Message) -> Self {
71        self.on_close = Some(on_close);
72        self
73    }
74
75    #[inline]
76    pub fn popup(mut self, popup: impl Into<Element<'a, Message, crate::Theme, Renderer>>) -> Self {
77        self.popup = Some(popup.into());
78        self
79    }
80
81    #[inline]
82    pub fn position(mut self, position: Position) -> Self {
83        self.position = position;
84        self
85    }
86}
87
88impl<Message: Clone, Renderer> Widget<Message, crate::Theme, Renderer>
89    for Popover<'_, Message, Renderer>
90where
91    Renderer: iced_core::Renderer,
92{
93    fn id(&self) -> Option<widget::Id> {
94        Some(self.id.clone())
95    }
96
97    fn set_id(&mut self, id: widget::Id) {
98        self.id = id;
99    }
100
101    fn children(&self) -> Vec<Tree> {
102        if let Some(popup) = &self.popup {
103            vec![Tree::new(&self.content), Tree::new(popup)]
104        } else {
105            vec![Tree::new(&self.content)]
106        }
107    }
108
109    fn diff(&mut self, tree: &mut Tree) {
110        if let Some(popup) = &mut self.popup {
111            tree.diff_children(&mut [&mut self.content, popup]);
112        } else {
113            tree.diff_children(&mut [&mut self.content]);
114        }
115    }
116
117    fn size(&self) -> Size<Length> {
118        self.content.as_widget().size()
119    }
120
121    fn layout(
122        &mut self,
123        tree: &mut Tree,
124        renderer: &Renderer,
125        limits: &layout::Limits,
126    ) -> layout::Node {
127        let tree = &mut tree.children[0];
128        self.content.as_widget_mut().layout(tree, renderer, limits)
129    }
130
131    fn operate(
132        &mut self,
133        tree: &mut Tree,
134        layout: Layout<'_>,
135        renderer: &Renderer,
136        operation: &mut dyn Operation,
137    ) {
138        // Skip operating on background content, prevents Tab from escaping
139        if self.modal && self.popup.is_some() {
140            return;
141        }
142        self.content
143            .as_widget_mut()
144            .operate(content_tree_mut(tree), layout, renderer, operation);
145    }
146
147    fn update(
148        &mut self,
149        tree: &mut Tree,
150        event: &Event,
151        layout: Layout<'_>,
152        cursor_position: mouse::Cursor,
153        renderer: &Renderer,
154        clipboard: &mut dyn Clipboard,
155        shell: &mut Shell<'_, Message>,
156        viewport: &Rectangle,
157    ) {
158        if self.popup.is_some() {
159            if self.modal {
160                if matches!(event, Event::Mouse(_) | Event::Touch(_)) {
161                    shell.capture_event();
162                    return;
163                }
164            } else if let Some(on_close) = self.on_close.as_ref() {
165                if matches!(
166                    event,
167                    Event::Mouse(mouse::Event::ButtonPressed(_))
168                        | Event::Touch(touch::Event::FingerPressed { .. })
169                ) && !cursor_position.is_over(layout.bounds())
170                {
171                    shell.publish(on_close.clone());
172                }
173            }
174        }
175
176        // Hide cursor from background content when modal popup is active
177        let cursor = if self.modal && self.popup.is_some() {
178            mouse::Cursor::Unavailable
179        } else {
180            cursor_position
181        };
182        self.content.as_widget_mut().update(
183            &mut tree.children[0],
184            event,
185            layout,
186            cursor,
187            renderer,
188            clipboard,
189            shell,
190            viewport,
191        )
192    }
193
194    fn mouse_interaction(
195        &self,
196        tree: &Tree,
197        layout: Layout<'_>,
198        cursor_position: mouse::Cursor,
199        viewport: &Rectangle,
200        renderer: &Renderer,
201    ) -> mouse::Interaction {
202        if self.modal && self.popup.is_some() && cursor_position.is_over(layout.bounds()) {
203            return mouse::Interaction::None;
204        }
205        self.content.as_widget().mouse_interaction(
206            content_tree(tree),
207            layout,
208            cursor_position,
209            viewport,
210            renderer,
211        )
212    }
213
214    fn draw(
215        &self,
216        tree: &Tree,
217        renderer: &mut Renderer,
218        theme: &crate::Theme,
219        renderer_style: &renderer::Style,
220        layout: Layout<'_>,
221        cursor_position: mouse::Cursor,
222        viewport: &Rectangle,
223    ) {
224        // Hide cursor from background content when a modal popup is active
225        let cursor = if self.modal && self.popup.is_some() {
226            mouse::Cursor::Unavailable
227        } else {
228            cursor_position
229        };
230        self.content.as_widget().draw(
231            content_tree(tree),
232            renderer,
233            theme,
234            renderer_style,
235            layout,
236            cursor,
237            viewport,
238        );
239    }
240
241    fn overlay<'b>(
242        &'b mut self,
243        tree: &'b mut Tree,
244        layout: Layout<'b>,
245        renderer: &Renderer,
246        viewport: &Rectangle,
247        mut translation: Vector,
248    ) -> Option<overlay::Element<'b, Message, crate::Theme, Renderer>> {
249        if let Some(popup) = &mut self.popup {
250            let bounds = layout.bounds();
251
252            // Calculate overlay position from relative position
253            let mut overlay_position = match self.position {
254                Position::Center => Point::new(
255                    bounds.x + bounds.width / 2.0,
256                    bounds.y + bounds.height / 2.0,
257                ),
258                Position::Bottom => {
259                    Point::new(bounds.x + bounds.width / 2.0, bounds.y + bounds.height)
260                }
261                Position::Point(relative) => {
262                    bounds.position() + Vector::new(relative.x, relative.y)
263                }
264                Position::Top => Point::new(bounds.x + bounds.width / 2.0, bounds.y),
265            };
266
267            // Round position to prevent rendering issues
268            overlay_position.x = overlay_position.x.round();
269            overlay_position.y = overlay_position.y.round();
270            translation.x += overlay_position.x;
271            translation.y += overlay_position.y;
272            Some(overlay::Element::new(Box::new(Overlay {
273                tree: &mut tree.children[1],
274                content: popup,
275                position: self.position,
276                pos: Point::new(translation.x, translation.y),
277                modal: self.modal,
278            })))
279        } else {
280            self.content.as_widget_mut().overlay(
281                &mut tree.children[0],
282                layout,
283                renderer,
284                viewport,
285                translation,
286            )
287        }
288    }
289
290    fn drag_destinations(
291        &self,
292        tree: &Tree,
293        layout: Layout<'_>,
294        renderer: &Renderer,
295        dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
296    ) {
297        self.content.as_widget().drag_destinations(
298            content_tree(tree),
299            layout,
300            renderer,
301            dnd_rectangles,
302        );
303    }
304
305    #[cfg(feature = "a11y")]
306    /// get the a11y nodes for the widget
307    fn a11y_nodes(
308        &self,
309        layout: Layout<'_>,
310        state: &Tree,
311        p: mouse::Cursor,
312    ) -> iced_accessibility::A11yTree {
313        self.content
314            .as_widget()
315            .a11y_nodes(layout, content_tree(state), p)
316    }
317}
318
319impl<'a, Message, Renderer> From<Popover<'a, Message, Renderer>>
320    for Element<'a, Message, crate::Theme, Renderer>
321where
322    Message: 'static + Clone,
323    Renderer: iced_core::Renderer + 'static,
324{
325    fn from(popover: Popover<'a, Message, Renderer>) -> Self {
326        Self::new(popover)
327    }
328}
329
330pub struct Overlay<'a, 'b, Message, Renderer> {
331    tree: &'a mut Tree,
332    content: &'a mut Element<'b, Message, crate::Theme, Renderer>,
333    position: Position,
334    pos: Point,
335    modal: bool,
336}
337
338impl<Message, Renderer> overlay::Overlay<Message, crate::Theme, Renderer>
339    for Overlay<'_, '_, Message, Renderer>
340where
341    Message: Clone,
342    Renderer: iced_core::Renderer,
343{
344    fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
345        let mut position = self.pos;
346        let limits = layout::Limits::new(Size::UNIT, bounds);
347        let node = self
348            .content
349            .as_widget_mut()
350            .layout(self.tree, renderer, &limits);
351        match self.position {
352            Position::Center => {
353                // Position is set to the center of the widget
354                let width = node.size().width;
355                let height = node.size().height;
356                position.x = (position.x - width / 2.0).clamp(0.0, bounds.width - width);
357                position.y = (position.y - height / 2.0).clamp(0.0, bounds.height - height);
358            }
359            Position::Top => {
360                let width = node.size().width;
361                let height = node.size().height;
362                position.x = (position.x - width / 2.0).clamp(0.0, bounds.width - width);
363                position.y = (position.y - height).clamp(0.0, bounds.height - height);
364            }
365            Position::Bottom => {
366                // Position is set to the center bottom of the widget
367                let width = node.size().width;
368                let height = node.size().height;
369                position.x = (position.x - width / 2.0).clamp(0.0, bounds.width - width);
370                position.y = position.y.clamp(0.0, bounds.height - height);
371            }
372            Position::Point(_) => {
373                // Position is using context menu logic
374                let size = node.size();
375                position.x = position.x.clamp(0.0, bounds.width - size.width);
376                if position.y + size.height > bounds.height {
377                    position.y = (position.y - size.height).clamp(0.0, bounds.height - size.height);
378                }
379            }
380        }
381
382        // Round position to prevent rendering issues
383        position.x = position.x.round();
384        position.y = position.y.round();
385
386        node.move_to(position)
387    }
388
389    fn operate(
390        &mut self,
391        layout: Layout<'_>,
392        renderer: &Renderer,
393        operation: &mut dyn Operation<()>,
394    ) {
395        self.content
396            .as_widget_mut()
397            .operate(self.tree, layout, renderer, operation);
398    }
399
400    fn update(
401        &mut self,
402        event: &Event,
403        layout: Layout<'_>,
404        cursor_position: mouse::Cursor,
405        renderer: &Renderer,
406        clipboard: &mut dyn Clipboard,
407        shell: &mut Shell<'_, Message>,
408    ) {
409        if self.modal
410            && matches!(event, Event::Mouse(_) | Event::Touch(_))
411            && !cursor_position.is_over(layout.bounds())
412        {
413            // Swallow new presses outside the popup, but still forward other
414            // events so an interaction started inside it (such as a selection
415            // drag) receives its release once the cursor leaves the bounds.
416            let is_press = matches!(
417                event,
418                Event::Mouse(mouse::Event::ButtonPressed(_))
419                    | Event::Touch(touch::Event::FingerPressed { .. })
420            );
421            if !is_press {
422                self.content.as_widget_mut().update(
423                    self.tree,
424                    event,
425                    layout,
426                    cursor_position,
427                    renderer,
428                    clipboard,
429                    shell,
430                    &layout.bounds(),
431                );
432            }
433            shell.capture_event();
434            return;
435        }
436
437        self.content.as_widget_mut().update(
438            self.tree,
439            event,
440            layout,
441            cursor_position,
442            renderer,
443            clipboard,
444            shell,
445            &layout.bounds(),
446        )
447    }
448
449    fn mouse_interaction(
450        &self,
451        layout: Layout<'_>,
452        cursor_position: mouse::Cursor,
453        renderer: &Renderer,
454    ) -> mouse::Interaction {
455        if self.modal && !cursor_position.is_over(layout.bounds()) {
456            return mouse::Interaction::None;
457        }
458
459        self.content.as_widget().mouse_interaction(
460            self.tree,
461            layout,
462            cursor_position,
463            &layout.bounds(),
464            renderer,
465        )
466    }
467
468    fn draw(
469        &self,
470        renderer: &mut Renderer,
471        theme: &crate::Theme,
472        style: &renderer::Style,
473        layout: Layout<'_>,
474        cursor_position: mouse::Cursor,
475    ) {
476        let bounds = layout.bounds();
477        self.content.as_widget().draw(
478            self.tree,
479            renderer,
480            theme,
481            style,
482            layout,
483            cursor_position,
484            &bounds,
485        );
486    }
487
488    fn overlay<'c>(
489        &'c mut self,
490        layout: Layout<'c>,
491        renderer: &Renderer,
492    ) -> Option<overlay::Element<'c, Message, crate::Theme, Renderer>> {
493        self.content.as_widget_mut().overlay(
494            self.tree,
495            layout,
496            renderer,
497            &layout.bounds(),
498            Default::default(),
499        )
500    }
501}
502
503/// The local state of a [`Popover`].
504#[derive(Debug, Default)]
505struct State {
506    is_open: bool,
507}
508
509/// The first child in [`Popover::children`] is always the wrapped content.
510fn content_tree(tree: &Tree) -> &Tree {
511    &tree.children[0]
512}
513
514/// The first child in [`Popover::children`] is always the wrapped content.
515fn content_tree_mut(tree: &mut Tree) -> &mut Tree {
516    &mut tree.children[0]
517}