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