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}