kas_core/popup.rs
1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License in the LICENSE-APACHE file or at:
4//     https://www.apache.org/licenses/LICENSE-2.0
5
6//! Popup root
7
8use crate::dir::Direction;
9use crate::event::{ConfigCx, Event, EventCx, IsUsed, Scroll, Unused, Used};
10use crate::{Events, Id, LayoutExt, Widget, WindowId};
11use kas_macros::{autoimpl, impl_scope, widget_index};
12
13#[allow(unused)] use crate::event::EventState;
14
15#[derive(Clone, Debug)]
16pub(crate) struct PopupDescriptor {
17    pub id: Id,
18    pub parent: Id,
19    pub direction: Direction,
20}
21
22impl_scope! {
23    /// A popup (e.g. menu or tooltip)
24    ///
25    /// A pop-up is a box used for things like tool-tips and menus which escapes
26    /// the parent's rect. This widget is the root of any popup UI.
27    ///
28    /// This widget must be excluded from the parent's layout.
29    ///
30    /// Depending on the platform, the pop-up may be a special window or emulate
31    /// this with a layer drawn in an existing window. Both approaches should
32    /// exhibit similar behaviour except that the former approach allows the
33    /// popup to escape the bounds of the parent window.
34    /// NOTE: currently only the emulated approach is implemented.
35    ///
36    /// A popup receives input data from its parent like any other widget.
37    #[autoimpl(Deref, DerefMut using self.inner)]
38    #[widget {
39        layout = frame!(self.inner, style = kas::theme::FrameStyle::Popup);
40    }]
41    pub struct Popup<W: Widget> {
42        core: widget_core!(),
43        direction: Direction,
44        #[widget]
45        inner: W,
46        win_id: Option<WindowId>,
47    }
48
49    impl Events for Self {
50        type Data = W::Data;
51
52        fn configure(&mut self, cx: &mut ConfigCx) {
53            cx.new_access_layer(self.id(), true);
54        }
55
56        fn configure_recurse(&mut self, cx: &mut ConfigCx, data: &Self::Data) {
57            if self.win_id.is_some() {
58                let id = self.make_child_id(widget_index!(self.inner));
59                cx.configure(self.inner.as_node(data), id)
60            }
61        }
62
63        fn update_recurse(&mut self, cx: &mut ConfigCx, data: &Self::Data) {
64            if self.win_id.is_some() {
65                cx.update(self.inner.as_node(data))
66            }
67        }
68
69        fn handle_event(&mut self, cx: &mut EventCx, _: &W::Data, event: Event) -> IsUsed {
70            match event {
71                Event::PressStart { press } => {
72                    if press.id.as_ref().map(|id| self.is_ancestor_of(id)).unwrap_or(false) {
73                        Unused
74                    } else {
75                        self.close(cx);
76                        Unused
77                    }
78                }
79                Event::PopupClosed(id) => {
80                    debug_assert_eq!(Some(id), self.win_id);
81                    self.win_id = None;
82                    Used
83                }
84                _ => Unused,
85            }
86        }
87
88        fn handle_scroll(&mut self, cx: &mut EventCx, _: &Self::Data, _: Scroll) {
89            // Scroll of the popup does not affect ancestor nodes
90            cx.set_scroll(Scroll::None);
91        }
92    }
93
94    impl Self {
95        /// Construct a popup over a `W: Widget`
96        pub fn new(inner: W, direction: Direction) -> Self {
97            Popup {
98                core: Default::default(),
99                direction,
100                inner,
101                win_id: None,
102            }
103        }
104
105        /// Get direction
106        pub fn direction(&self) -> Direction {
107            self.direction
108        }
109
110        /// Set direction
111        pub fn set_direction(&mut self, direction: Direction) {
112            self.direction = direction;
113        }
114
115        /// Query whether the popup is open
116        pub fn is_open(&self) -> bool {
117            self.win_id.is_some()
118        }
119
120        /// Open the popup
121        ///
122        /// The popup is positioned next to the `parent`'s rect in the specified
123        /// direction (if this is not possible, the direction may be reversed).
124        ///
125        /// The `parent` is marked as depressed (pushed down) while the popup is
126        /// open.
127        ///
128        /// Returns `true` when the popup is newly opened. In this case, the
129        /// caller may wish to call [`EventState::next_nav_focus`] next.
130        pub fn open(&mut self, cx: &mut EventCx, data: &W::Data, parent: Id) -> bool {
131            if self.win_id.is_some() {
132                return false;
133            }
134
135            let id = self.make_child_id(widget_index!(self.inner));
136            cx.configure(self.inner.as_node(data), id);
137
138            self.win_id = Some(cx.add_popup(kas::PopupDescriptor {
139                id: self.id(),
140                parent,
141                direction: self.direction,
142            }));
143
144            true
145        }
146
147        /// Close the popup
148        ///
149        /// Navigation focus will return to whichever widget had focus before
150        /// the popup was open.
151        pub fn close(&mut self, cx: &mut EventCx) {
152            if let Some(id) = self.win_id {
153                cx.close_window(id);
154            }
155        }
156    }
157}