Skip to main content

vizia_core/views/
popup.rs

1use crate::context::TreeProps;
2use crate::prelude::*;
3use bitflags::bitflags;
4
5use crate::vg;
6
7/// A model which can be used by views which contain a popup.
8#[derive(Debug, Default, Clone)]
9pub struct PopupData {
10    /// The open state of the popup.
11    pub is_open: bool,
12}
13
14impl From<PopupData> for bool {
15    fn from(value: PopupData) -> Self {
16        value.is_open
17    }
18}
19
20impl Model for PopupData {
21    fn event(&mut self, _: &mut EventContext, event: &mut Event) {
22        event.map(|popup_event, meta| match popup_event {
23            PopupEvent::Open => {
24                self.is_open = true;
25                meta.consume();
26            }
27
28            PopupEvent::Close => {
29                self.is_open = false;
30                meta.consume();
31            }
32
33            PopupEvent::Switch => {
34                self.is_open ^= true;
35                meta.consume();
36            }
37        });
38    }
39}
40
41/// Events used by the [Popover] view.
42#[derive(Debug)]
43pub enum PopupEvent {
44    /// Opens the popup.
45    Open,
46    /// Closes the popup.
47    Close,
48    /// Switches the state of the popup from closed to open or open to closed.
49    Switch,
50}
51
52/// A view for displaying popup content.
53pub struct Popover {
54    placement: Signal<Placement>,
55    show_arrow: Signal<bool>,
56    arrow_size: Signal<Length>,
57    should_reposition: Signal<bool>,
58}
59
60impl Popover {
61    /// Creates a new [Popover] view.
62    pub fn new(cx: &mut Context, content: impl FnOnce(&mut Context)) -> Handle<Self> {
63        let placement = Signal::new(Placement::Bottom);
64        let show_arrow = Signal::new(true);
65        let arrow_size = Signal::new(Length::Value(LengthValue::Px(8.0)));
66        let should_reposition = Signal::new(true);
67
68        Self { placement, show_arrow, arrow_size, should_reposition }
69            .build(cx, |cx| {
70                (content)(cx);
71                Binding::new(cx, show_arrow, move |cx| {
72                    let show_arrow = show_arrow.get();
73                    if show_arrow {
74                        Arrow::new(cx, placement, arrow_size);
75                    }
76                });
77            })
78            .position_type(PositionType::Absolute)
79            .space(Pixels(0.0))
80    }
81}
82
83impl View for Popover {
84    fn element(&self) -> Option<&'static str> {
85        Some("popup")
86    }
87
88    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
89        event.map(|window_event, _| match window_event {
90            // Reposition popup if there isn't enough room for it.
91            WindowEvent::GeometryChanged(_) => {
92                let parent = cx.parent();
93                let parent_bounds = cx.cache.get_bounds(parent);
94                let bounds = cx.bounds();
95                let window_bounds = cx.cache.get_bounds(cx.parent_window());
96                let scale = cx.scale_factor();
97                let arrow_size = self.arrow_size.get().to_px().unwrap() * cx.scale_factor();
98
99                let shift = if self.should_reposition.get() {
100                    let mut available = AvailablePlacement::all();
101
102                    let top_start_bounds = BoundingBox::from_min_max(
103                        parent_bounds.left(),
104                        parent_bounds.top() - bounds.height() - arrow_size,
105                        parent_bounds.left() + bounds.width(),
106                        parent_bounds.top(),
107                    );
108
109                    available.set(
110                        AvailablePlacement::TOP_START,
111                        window_bounds.contains(&top_start_bounds),
112                    );
113
114                    let top_bounds = BoundingBox::from_min_max(
115                        parent_bounds.center().0 - bounds.width() / 2.0,
116                        parent_bounds.top() - bounds.height() - arrow_size,
117                        parent_bounds.center().0 + bounds.width() / 2.0,
118                        parent_bounds.top(),
119                    );
120
121                    available.set(AvailablePlacement::TOP, window_bounds.contains(&top_bounds));
122
123                    let top_end_bounds = BoundingBox::from_min_max(
124                        parent_bounds.right() - bounds.width(),
125                        parent_bounds.top() - bounds.height() - arrow_size,
126                        parent_bounds.right(),
127                        parent_bounds.top(),
128                    );
129
130                    available
131                        .set(AvailablePlacement::TOP_END, window_bounds.contains(&top_end_bounds));
132
133                    let bottom_start_bounds = BoundingBox::from_min_max(
134                        parent_bounds.left(),
135                        parent_bounds.bottom(),
136                        parent_bounds.left() + bounds.width(),
137                        parent_bounds.bottom() + bounds.height() + arrow_size,
138                    );
139
140                    available.set(
141                        AvailablePlacement::BOTTOM_START,
142                        window_bounds.contains(&bottom_start_bounds),
143                    );
144
145                    let bottom_bounds = BoundingBox::from_min_max(
146                        parent_bounds.center().0 - bounds.width() / 2.0,
147                        parent_bounds.bottom(),
148                        parent_bounds.center().0 + bounds.width() / 2.0,
149                        parent_bounds.bottom() + bounds.height() + arrow_size,
150                    );
151
152                    available
153                        .set(AvailablePlacement::BOTTOM, window_bounds.contains(&bottom_bounds));
154
155                    let bottom_end_bounds = BoundingBox::from_min_max(
156                        parent_bounds.right() - bounds.width(),
157                        parent_bounds.bottom(),
158                        parent_bounds.right(),
159                        parent_bounds.bottom() + bounds.height() + arrow_size,
160                    );
161
162                    available.set(
163                        AvailablePlacement::BOTTOM_END,
164                        window_bounds.contains(&bottom_end_bounds),
165                    );
166
167                    let left_start_bounds = BoundingBox::from_min_max(
168                        parent_bounds.left() - bounds.width() - arrow_size,
169                        parent_bounds.top(),
170                        parent_bounds.left(),
171                        parent_bounds.top() + bounds.height(),
172                    );
173
174                    available.set(
175                        AvailablePlacement::LEFT_START,
176                        window_bounds.contains(&left_start_bounds),
177                    );
178
179                    let left_bounds = BoundingBox::from_min_max(
180                        parent_bounds.left() - bounds.width() - arrow_size,
181                        parent_bounds.center().1 - bounds.height() / 2.0,
182                        parent_bounds.left(),
183                        parent_bounds.center().1 + bounds.height() / 2.0,
184                    );
185
186                    available.set(AvailablePlacement::LEFT, window_bounds.contains(&left_bounds));
187
188                    let left_end_bounds = BoundingBox::from_min_max(
189                        parent_bounds.left() - bounds.width() - arrow_size,
190                        parent_bounds.bottom() - bounds.height(),
191                        parent_bounds.left(),
192                        parent_bounds.bottom(),
193                    );
194
195                    available.set(
196                        AvailablePlacement::LEFT_END,
197                        window_bounds.contains(&left_end_bounds),
198                    );
199
200                    let right_start_bounds = BoundingBox::from_min_max(
201                        parent_bounds.right(),
202                        parent_bounds.top(),
203                        parent_bounds.right() + bounds.width() + arrow_size,
204                        parent_bounds.top() + bounds.height(),
205                    );
206
207                    available.set(
208                        AvailablePlacement::RIGHT_START,
209                        window_bounds.contains(&right_start_bounds),
210                    );
211
212                    let right_bounds = BoundingBox::from_min_max(
213                        parent_bounds.right(),
214                        parent_bounds.center().1 - bounds.height() / 2.0,
215                        parent_bounds.right() + bounds.width() + arrow_size,
216                        parent_bounds.center().1 + bounds.height() / 2.0,
217                    );
218
219                    available.set(AvailablePlacement::RIGHT, window_bounds.contains(&right_bounds));
220
221                    let right_end_bounds = BoundingBox::from_min_max(
222                        parent_bounds.right(),
223                        parent_bounds.bottom() - bounds.height(),
224                        parent_bounds.right() + bounds.width() + arrow_size,
225                        parent_bounds.bottom(),
226                    );
227
228                    available.set(
229                        AvailablePlacement::RIGHT_END,
230                        window_bounds.contains(&right_end_bounds),
231                    );
232
233                    self.placement.get().place(available)
234                } else {
235                    if let Some(first_child) = cx.tree.get_layout_first_child(cx.current) {
236                        let mut child_bounds = cx.cache.get_bounds(first_child);
237                        child_bounds.h = window_bounds.bottom()
238                            - parent_bounds.bottom()
239                            - arrow_size * scale
240                            - 8.0;
241                        cx.style.max_height.insert(first_child, Pixels(child_bounds.h / scale));
242                    }
243                    self.placement.get()
244                };
245
246                let arrow_size = self.arrow_size.get().to_px().unwrap();
247
248                let translate = match shift {
249                    Placement::Top => (
250                        -(bounds.width() - parent_bounds.width()) / (2.0 * scale),
251                        -bounds.height() / scale - arrow_size,
252                    ),
253                    Placement::TopStart => (0.0, -bounds.height() / scale - arrow_size),
254                    Placement::TopEnd => (
255                        -(bounds.width() - parent_bounds.width()) / scale,
256                        -bounds.height() / scale - arrow_size,
257                    ),
258                    Placement::Bottom => (
259                        -(bounds.width() - parent_bounds.width()) / (2.0 * scale),
260                        parent_bounds.height() / scale + arrow_size,
261                    ),
262                    Placement::BottomStart => (0.0, parent_bounds.height() / scale + arrow_size),
263                    Placement::BottomEnd => (
264                        -(bounds.width() - parent_bounds.width()) / scale,
265                        parent_bounds.height() / scale + arrow_size,
266                    ),
267                    Placement::LeftStart => (-(bounds.width() / scale) - arrow_size, 0.0),
268                    Placement::Left => (
269                        -(bounds.width() / scale) - arrow_size,
270                        -(bounds.height() - parent_bounds.height()) / (2.0 * scale),
271                    ),
272                    Placement::LeftEnd => (
273                        -(bounds.width() / scale) - arrow_size,
274                        -(bounds.height() - parent_bounds.height()) / scale,
275                    ),
276                    Placement::RightStart => ((parent_bounds.width() / scale) + arrow_size, 0.0),
277                    Placement::Right => (
278                        (parent_bounds.width() / scale) + arrow_size,
279                        -(bounds.height() - parent_bounds.height()) / (2.0 * scale),
280                    ),
281                    Placement::RightEnd => (
282                        (parent_bounds.width() / scale) + arrow_size,
283                        -(bounds.height() - parent_bounds.height()) / scale,
284                    ),
285
286                    _ => (0.0, 0.0),
287                };
288                cx.set_translate((Pixels(translate.0.round()), Pixels(translate.1.round())));
289            }
290
291            _ => {}
292        });
293    }
294}
295
296bitflags! {
297    #[derive(Debug, Clone, Copy)]
298    pub(crate) struct AvailablePlacement: u16 {
299        const TOP_START = 1 << 0;
300        const TOP = 1 << 1;
301        const TOP_END = 1 << 2;
302        const LEFT_START = 1 << 3;
303        const LEFT = 1 << 4;
304        const LEFT_END = 1 << 5;
305        const BOTTOM_START = 1 << 6;
306        const BOTTOM = 1 << 7;
307        const BOTTOM_END = 1 << 8;
308        const RIGHT_START = 1 << 9;
309        const RIGHT = 1 << 10;
310        const RIGHT_END = 1 << 11;
311    }
312}
313
314impl AvailablePlacement {
315    fn can_place(&self, placement: Placement) -> bool {
316        match placement {
317            Placement::Bottom => self.contains(AvailablePlacement::BOTTOM),
318            Placement::BottomStart => self.contains(AvailablePlacement::BOTTOM_START),
319            Placement::BottomEnd => self.contains(AvailablePlacement::BOTTOM_END),
320            Placement::Top => self.contains(AvailablePlacement::TOP),
321            Placement::TopStart => self.contains(AvailablePlacement::TOP_START),
322            Placement::TopEnd => self.contains(AvailablePlacement::TOP_END),
323            Placement::Left => self.contains(AvailablePlacement::LEFT),
324            Placement::LeftStart => self.contains(AvailablePlacement::LEFT_START),
325            Placement::LeftEnd => self.contains(AvailablePlacement::LEFT_END),
326            Placement::Right => self.contains(AvailablePlacement::RIGHT),
327            Placement::RightStart => self.contains(AvailablePlacement::RIGHT_START),
328            Placement::RightEnd => self.contains(AvailablePlacement::RIGHT_END),
329            _ => false,
330        }
331    }
332}
333
334impl Placement {
335    fn from_int(int: u16) -> Placement {
336        match int {
337            0 => Placement::TopStart,
338            1 => Placement::Top,
339            2 => Placement::TopEnd,
340            3 => Placement::BottomStart,
341            4 => Placement::Bottom,
342            5 => Placement::BottomEnd,
343            6 => Placement::RightStart,
344            7 => Placement::Right,
345            8 => Placement::RightEnd,
346            9 => Placement::LeftStart,
347            10 => Placement::Left,
348            11 => Placement::LeftEnd,
349            12 => Placement::Over,
350            _ => Placement::Cursor,
351        }
352    }
353
354    pub(crate) fn place(&self, available: AvailablePlacement) -> Placement {
355        if *self == Placement::Over || *self == Placement::Cursor {
356            return *self;
357        }
358
359        if available.is_empty() {
360            return Placement::Over;
361        }
362
363        let mut placement = *self;
364
365        while !available.can_place(placement) {
366            placement = placement.next(*self);
367        }
368
369        placement
370    }
371
372    fn next(&self, original: Self) -> Self {
373        const TOP_START: [u16; 12] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
374        const TOP: [u16; 12] = [2, 0, 4, 5, 3, 7, 8, 6, 10, 11, 9, 12];
375        const TOP_END: [u16; 12] = [5, 0, 1, 8, 3, 4, 11, 6, 7, 12, 9, 10];
376        const BOTTOM_START: [u16; 12] = [1, 2, 6, 4, 5, 0, 7, 8, 9, 10, 11, 12];
377        const BOTTOM: [u16; 12] = [2, 0, 7, 5, 3, 1, 8, 6, 10, 11, 9, 12];
378        const BOTTOM_END: [u16; 12] = [8, 0, 1, 2, 3, 4, 11, 6, 7, 12, 9, 10];
379        const LEFT_START: [u16; 12] = [1, 2, 12, 4, 5, 0, 7, 8, 3, 10, 11, 6];
380        const LEFT: [u16; 12] = [2, 0, 12, 5, 3, 1, 8, 6, 4, 11, 9, 7];
381        const LEFT_END: [u16; 12] = [12, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
382        const RIGHT_START: [u16; 12] = [1, 2, 12, 4, 5, 0, 7, 8, 9, 10, 11, 3];
383        const RIGHT: [u16; 12] = [2, 0, 12, 5, 3, 1, 8, 6, 10, 11, 9, 4];
384        const RIGHT_END: [u16; 12] = [12, 0, 1, 2, 3, 4, 11, 6, 7, 5, 9, 10];
385
386        let states = match original {
387            Placement::TopStart => TOP_START,
388            Placement::Top => TOP,
389            Placement::TopEnd => TOP_END,
390            Placement::BottomStart => BOTTOM_START,
391            Placement::Bottom => BOTTOM,
392            Placement::BottomEnd => BOTTOM_END,
393            Placement::RightStart => RIGHT_START,
394            Placement::Right => RIGHT,
395            Placement::RightEnd => RIGHT_END,
396            Placement::LeftStart => LEFT_START,
397            Placement::Left => LEFT,
398            Placement::LeftEnd => LEFT_END,
399            _ => unreachable!(),
400        };
401
402        Placement::from_int(states[*self as usize])
403    }
404}
405
406/// Modifiers for configuring [Popover] behavior and positioning.
407pub trait PopoverModifiers: Sized {
408    /// Sets the position where the popup should appear relative to its parent element.
409    /// Defaults to `Placement::Bottom`.
410    fn placement(self, placement: impl Res<Placement> + 'static) -> Self;
411
412    /// Sets whether the popup should include an arrow. Defaults to true.
413    fn show_arrow(self, show_arrow: impl Res<bool> + 'static) -> Self;
414
415    /// Sets the size of the popup arrow, or gap if the arrow is hidden.
416    fn arrow_size<U: Into<Length> + Clone + 'static>(self, size: impl Res<U> + 'static) -> Self;
417
418    /// Set to whether the popup should reposition to always be visible.
419    fn should_reposition(self, should_reposition: impl Res<bool> + 'static) -> Self;
420
421    /// Registers a callback for when the user clicks off of the popup, usually with the intent of
422    /// closing it.
423    fn on_blur<F>(self, f: F) -> Self
424    where
425        F: 'static + Fn(&mut EventContext);
426}
427
428impl PopoverModifiers for Handle<'_, Popover> {
429    fn placement(self, placement: impl Res<Placement> + 'static) -> Self {
430        let placement = placement.to_signal(self.cx);
431        self.bind(placement, move |handle| {
432            let placement = placement.get();
433            handle.modify(|popup| {
434                popup.placement.set(placement);
435            });
436        })
437    }
438
439    fn show_arrow(self, show_arrow: impl Res<bool> + 'static) -> Self {
440        let show_arrow = show_arrow.to_signal(self.cx);
441        self.bind(show_arrow, move |handle| {
442            let show_arrow = show_arrow.get();
443            handle.modify(|popup| popup.show_arrow.set(show_arrow));
444        })
445    }
446
447    fn arrow_size<U: Into<Length> + Clone + 'static>(self, size: impl Res<U> + 'static) -> Self {
448        let size = size.to_signal(self.cx);
449        self.bind(size, move |handle| {
450            let size = size.get();
451            let size = size.into();
452            handle.modify(|popup| popup.arrow_size.set(size));
453        })
454    }
455
456    fn should_reposition(self, should_reposition: impl Res<bool> + 'static) -> Self {
457        let should_reposition = should_reposition.to_signal(self.cx);
458        self.bind(should_reposition, move |handle| {
459            let should_reposition = should_reposition.get();
460            handle.modify(|popup| popup.should_reposition.set(should_reposition));
461        })
462    }
463
464    fn on_blur<F>(self, f: F) -> Self
465    where
466        F: 'static + Fn(&mut EventContext),
467    {
468        let focus_event = Box::new(f);
469        self.cx.with_current(self.entity, |cx| {
470            cx.add_listener(move |_: &mut Popover, cx, event| {
471                event.map(|window_event, meta| match window_event {
472                    WindowEvent::MouseDown(_) => {
473                        if meta.origin != cx.current() {
474                            // Check if the mouse was pressed outside of any descendants
475                            if !cx.hovered.is_descendant_of(cx.tree, cx.current) {
476                                (focus_event)(cx);
477                                meta.consume();
478                            }
479                        }
480                    }
481
482                    WindowEvent::KeyDown(code, _) => {
483                        if *code == Code::Escape {
484                            (focus_event)(cx);
485                        }
486                    }
487
488                    _ => {}
489                });
490            });
491        });
492
493        self
494    }
495}
496
497/// An arrow view used by the Popover view.
498pub(crate) struct Arrow {
499    placement: Signal<Placement>,
500}
501
502impl Arrow {
503    pub(crate) fn new(
504        cx: &mut Context,
505        placement: Signal<Placement>,
506        arrow_size: Signal<Length>,
507    ) -> Handle<Self> {
508        Self { placement }.build(cx, |_| {}).position_type(PositionType::Absolute).bind(
509            placement,
510            move |mut handle| {
511                let placement = placement.get();
512                let (t, b) = match placement {
513                    Placement::TopStart | Placement::Top | Placement::TopEnd => {
514                        (Percentage(100.0), Stretch(1.0))
515                    }
516                    Placement::BottomStart | Placement::Bottom | Placement::BottomEnd => {
517                        (Stretch(1.0), Percentage(100.0))
518                    }
519                    _ => (Stretch(1.0), Stretch(1.0)),
520                };
521
522                let (l, r) = match placement {
523                    Placement::LeftStart | Placement::Left | Placement::LeftEnd => {
524                        (Percentage(100.0), Stretch(1.0))
525                    }
526                    Placement::RightStart | Placement::Right | Placement::RightEnd => {
527                        (Stretch(1.0), Percentage(100.0))
528                    }
529                    Placement::TopStart | Placement::BottomStart => {
530                        // TODO: Use border radius
531                        (Pixels(8.0), Stretch(1.0))
532                    }
533                    Placement::TopEnd | Placement::BottomEnd => {
534                        // TODO: Use border radius
535                        (Stretch(1.0), Pixels(8.0))
536                    }
537                    _ => (Stretch(1.0), Stretch(1.0)),
538                };
539
540                handle = handle
541                    .top(t)
542                    .bottom(b)
543                    .left(l)
544                    .right(r)
545                    .position_type(PositionType::Absolute)
546                    .hoverable(false);
547
548                handle.bind(arrow_size, move |handle| {
549                    let arrow_size = arrow_size.get();
550                    let arrow_size = arrow_size.to_px().unwrap_or(8.0);
551                    let (w, h) = match placement {
552                        Placement::Top
553                        | Placement::Bottom
554                        | Placement::TopStart
555                        | Placement::BottomStart
556                        | Placement::TopEnd
557                        | Placement::BottomEnd => (Pixels(arrow_size * 2.0), Pixels(arrow_size)),
558
559                        _ => (Pixels(arrow_size), Pixels(arrow_size * 2.0)),
560                    };
561
562                    handle.width(w).height(h);
563                });
564            },
565        )
566    }
567}
568
569impl View for Arrow {
570    fn element(&self) -> Option<&'static str> {
571        Some("arrow")
572    }
573    fn draw(&self, cx: &mut DrawContext, canvas: &Canvas) {
574        let bounds = cx.bounds();
575        let mut path = vg::PathBuilder::new();
576        match self.placement.get() {
577            Placement::Bottom | Placement::BottomStart | Placement::BottomEnd => {
578                path.move_to(bounds.bottom_left());
579                path.line_to(bounds.center_top());
580                path.line_to(bounds.bottom_right());
581                path.line_to(bounds.bottom_left());
582            }
583
584            Placement::Top | Placement::TopStart | Placement::TopEnd => {
585                path.move_to(bounds.top_left());
586                path.line_to(bounds.center_bottom());
587                path.line_to(bounds.top_right());
588                path.line_to(bounds.top_left());
589            }
590
591            Placement::Left | Placement::LeftStart | Placement::LeftEnd => {
592                path.move_to(bounds.top_left());
593                path.line_to(bounds.center_right());
594                path.line_to(bounds.bottom_left());
595                path.line_to(bounds.top_left());
596            }
597
598            Placement::Right | Placement::RightStart | Placement::RightEnd => {
599                path.move_to(bounds.top_right());
600                path.line_to(bounds.center_left());
601                path.line_to(bounds.bottom_right());
602                path.line_to(bounds.top_right());
603            }
604
605            _ => {}
606        }
607        path.close();
608
609        let bg = cx.background_color();
610        let mut paint = vg::Paint::default();
611        paint.set_color(bg);
612        let path = path.detach();
613        canvas.draw_path(&path, &paint);
614    }
615}