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