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