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