kas_core/window/
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};
10#[allow(unused)] use crate::geom::Rect;
11use crate::layout::Align;
12use crate::window::WindowId;
13use crate::{ChildIndices, Events, Id, Tile, Widget};
14use kas_macros::{impl_self, widget_index};
15
16#[allow(unused)] use crate::event::EventState;
17
18#[derive(Clone, Debug)]
19pub(crate) struct PopupDescriptor {
20    /// Widget of type [`Popup`]
21    pub id: Id,
22    /// Visual parent: what the popup is placed next to
23    pub parent: Id,
24    /// Direction to place relative to [`Self::parent`]
25    pub direction: Direction,
26    /// Alignment relative to [`Self::parent`]
27    ///
28    /// This applies to the opposite axis to the direction; for example if
29    /// [`Self::direction`] is [`Direction::Right`] then this is vertical
30    /// alignment. In this case, [`Align::TL`] would imply that the top of the
31    /// popup would be aligned to the top of the parent.
32    pub align: Align,
33}
34
35pub(crate) const POPUP_INNER_INDEX: usize = 0;
36
37#[impl_self]
38mod Popup {
39    /// A popup (e.g. menu or tooltip)
40    ///
41    /// A pop-up is a box used for things like tool-tips and menus which escapes
42    /// the parent's rect. This widget is the root of any popup UI.
43    ///
44    /// This widget must be excluded from the parent's layout.
45    ///
46    /// Depending on the platform, the pop-up may be a special window or emulate
47    /// this with a layer drawn in an existing window. Both approaches should
48    /// exhibit similar behaviour except that the former approach allows the
49    /// popup to escape the bounds of the parent window.
50    /// NOTE: currently only the emulated approach is implemented.
51    ///
52    /// A popup receives input data from its parent like any other widget.
53    #[widget]
54    #[layout(frame!(self.inner).with_style(kas::theme::FrameStyle::Popup))]
55    pub struct Popup<W: Widget> {
56        core: widget_core!(),
57        direction: Direction,
58        align: Align,
59        /// The inner widget
60        #[widget]
61        pub inner: W,
62        win_id: Option<WindowId>,
63    }
64
65    impl Tile for Self {
66        fn child_indices(&self) -> ChildIndices {
67            // Child is not visible. We handle configuration and updates directly.
68            (0..0).into()
69        }
70
71        fn find_child_index(&self, id: &Id) -> Option<usize> {
72            let index = Some(widget_index!(self.inner));
73            if self.win_id.is_none() || id.next_key_after(self.id_ref()) != index {
74                return None;
75            }
76            index
77        }
78    }
79
80    impl Events for Self {
81        type Data = W::Data;
82
83        fn configure_recurse(&mut self, cx: &mut ConfigCx, data: &Self::Data) {
84            if self.win_id.is_some() {
85                let id = self.make_child_id(widget_index!(self.inner));
86                if id.is_valid() {
87                    cx.configure(self.inner.as_node(data), id);
88                }
89            }
90        }
91
92        fn update_recurse(&mut self, cx: &mut ConfigCx, data: &Self::Data) {
93            if self.win_id.is_some() {
94                cx.update(self.inner.as_node(data));
95            }
96        }
97
98        fn handle_event(&mut self, _: &mut EventCx, _: &W::Data, event: Event) -> IsUsed {
99            match event {
100                Event::PopupClosed(_) => {
101                    self.win_id = None;
102                    Used
103                }
104                _ => Unused,
105            }
106        }
107
108        fn handle_scroll(&mut self, cx: &mut EventCx, _: &Self::Data, _: Scroll) {
109            // Scroll of the popup does not affect ancestor nodes
110            cx.set_scroll(Scroll::None);
111        }
112    }
113
114    impl Self {
115        /// Construct a popup over a `W: Widget`
116        ///
117        /// Popup placement is determined by `direction` and alignment (see
118        /// [`Self::align`]).
119        pub fn new(inner: W, direction: Direction) -> Self {
120            Popup {
121                core: Default::default(),
122                direction,
123                align: Align::Default,
124                inner,
125                win_id: None,
126            }
127        }
128
129        /// Get direction
130        pub fn direction(&self) -> Direction {
131            self.direction
132        }
133
134        /// Set direction
135        ///
136        /// The popup is placed next to a reference [`Rect`] in this
137        /// `direction`, if possible; otherwise it is placed in the opposite
138        /// direction. See also [`Self::align`].
139        pub fn set_direction(&mut self, direction: Direction) {
140            self.direction = direction;
141        }
142
143        /// Get alignment
144        #[inline]
145        pub fn alignment(&self) -> Align {
146            self.align
147        }
148
149        /// Set alignment (inline)
150        ///
151        /// After the popup is placed to one side of a reference [`Rect`] (see
152        /// [`Self::set_direction`]), it is aligned according to `align`:
153        ///
154        /// - [`Align::Default`], [`Align::TL`]: align with the top/left edge of the reference [`Rect`]
155        /// - [`Align::BR`]: align with the bottom/right edge of the reference [`Rect`]
156        /// - [`Align::Center`]: center relative to the reference [`Rect`]
157        /// - [`Align::Stretch`]: align like [`Align::Default`] but ensuring the popup is no shorter
158        ///   than the reference [`Rect`] on the touching side
159        #[must_use]
160        #[inline]
161        pub fn align(mut self, align: Align) -> Self {
162            self.align = align;
163            self
164        }
165
166        /// Query whether the popup is open
167        pub fn is_open(&self) -> bool {
168            self.win_id.is_some()
169        }
170
171        /// Open or reposition the popup
172        ///
173        /// The popup is positioned next to the `parent`'s rect (see [`Self::set_direction`],
174        /// [`Self::align`]).
175        ///
176        /// The `parent` is marked as depressed (pushed down) while the popup is
177        /// open.
178        ///
179        /// Returns `true` when the popup is newly opened. In this case, the
180        /// caller may wish to call [`EventState::next_nav_focus`] next.
181        pub fn open(
182            &mut self,
183            cx: &mut EventCx,
184            data: &W::Data,
185            parent: Id,
186            set_focus: bool,
187        ) -> bool {
188            let desc = PopupDescriptor {
189                id: self.id(),
190                parent,
191                direction: self.direction,
192                align: self.align,
193            };
194
195            if let Some(id) = self.win_id {
196                cx.reposition_popup(id, desc);
197                return false;
198            }
199
200            let index = widget_index!(self.inner);
201            assert_eq!(index, POPUP_INNER_INDEX);
202            let id = self.make_child_id(index);
203            cx.configure(self.inner.as_node(data), id);
204
205            self.win_id = Some(cx.add_popup(desc, set_focus));
206
207            true
208        }
209
210        /// Close the popup
211        ///
212        /// Navigation focus will return to whichever widget had focus before
213        /// the popup was open.
214        pub fn close(&mut self, cx: &mut EventCx) {
215            if let Some(id) = self.win_id.take() {
216                cx.close_window(id);
217            }
218        }
219    }
220}