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}