Skip to main content

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::{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            ChildIndices::none()
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        #[inline]
83        fn recurse_indices(&self) -> ChildIndices {
84            if self.win_id.is_some() {
85                ChildIndices::one(widget_index!(self.inner))
86            } else {
87                ChildIndices::none()
88            }
89        }
90
91        fn handle_event(&mut self, _: &mut EventCx, _: &W::Data, event: Event) -> IsUsed {
92            match event {
93                Event::PopupClosed(_) => {
94                    self.win_id = None;
95                    Used
96                }
97                _ => Unused,
98            }
99        }
100
101        fn handle_scroll(&mut self, cx: &mut EventCx, _: &Self::Data, _: Scroll) {
102            // Scroll of the popup does not affect ancestor nodes
103            cx.set_scroll(Scroll::None);
104        }
105    }
106
107    impl Self {
108        /// Construct a popup over a `W: Widget`
109        ///
110        /// Popup placement is determined by `direction` and alignment (see
111        /// [`Self::align`]).
112        pub fn new(inner: W, direction: Direction) -> Self {
113            Popup {
114                core: Default::default(),
115                direction,
116                align: Align::Default,
117                inner,
118                win_id: None,
119            }
120        }
121
122        /// Get direction
123        pub fn direction(&self) -> Direction {
124            self.direction
125        }
126
127        /// Set direction
128        ///
129        /// The popup is placed next to a reference [`Rect`] in this
130        /// `direction`, if possible; otherwise it is placed in the opposite
131        /// direction. See also [`Self::align`].
132        pub fn set_direction(&mut self, direction: Direction) {
133            self.direction = direction;
134        }
135
136        /// Get alignment
137        #[inline]
138        pub fn alignment(&self) -> Align {
139            self.align
140        }
141
142        /// Set alignment (inline)
143        ///
144        /// After the popup is placed to one side of a reference [`Rect`] (see
145        /// [`Self::set_direction`]), it is aligned according to `align`:
146        ///
147        /// - [`Align::Default`], [`Align::TL`]: align with the top/left edge of the reference [`Rect`]
148        /// - [`Align::BR`]: align with the bottom/right edge of the reference [`Rect`]
149        /// - [`Align::Center`]: center relative to the reference [`Rect`]
150        /// - [`Align::Stretch`]: align like [`Align::Default`] but ensuring the popup is no shorter
151        ///   than the reference [`Rect`] on the touching side
152        #[must_use]
153        #[inline]
154        pub fn align(mut self, align: Align) -> Self {
155            self.align = align;
156            self
157        }
158
159        /// Query whether the popup is open
160        pub fn is_open(&self) -> bool {
161            self.win_id.is_some()
162        }
163
164        /// Open or reposition the popup
165        ///
166        /// The popup is positioned next to the `parent`'s rect (see [`Self::set_direction`],
167        /// [`Self::align`]).
168        ///
169        /// The `parent` is marked as depressed (pushed down) while the popup is
170        /// open.
171        ///
172        /// Returns `true` when the popup is newly opened. In this case, the
173        /// caller may wish to call [`EventCx::next_nav_focus`] next.
174        pub fn open(
175            &mut self,
176            cx: &mut EventCx,
177            data: &W::Data,
178            parent: Id,
179            set_focus: bool,
180        ) -> bool {
181            let desc = PopupDescriptor {
182                id: self.id(),
183                parent,
184                direction: self.direction,
185                align: self.align,
186            };
187
188            if let Some(id) = self.win_id {
189                cx.reposition_popup(id, desc);
190                return false;
191            }
192
193            let index = widget_index!(self.inner);
194            assert_eq!(index, POPUP_INNER_INDEX);
195            let id = self.make_child_id(index);
196            cx.configure(self.inner.as_node(data), id);
197
198            self.win_id = Some(cx.add_popup(desc, set_focus));
199
200            true
201        }
202
203        /// Close the popup
204        ///
205        /// Navigation focus will return to whichever widget had focus before
206        /// the popup was open.
207        pub fn close(&mut self, cx: &mut EventCx) {
208            if let Some(id) = self.win_id.take() {
209                cx.close_window(id);
210            }
211        }
212    }
213}