1use std::fmt;
9
10use densa_core::{Point, Rect, Size};
11
12use crate::ui::State;
13use crate::{EventCx, PaintCx, Position, Style, Ui, UiError, Widget, WidgetId};
14
15#[derive(Debug, Clone, Copy, PartialEq)]
17pub struct Anchor {
18 rect: Rect,
19 side: Side,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23enum Side {
24 Below,
25 Beside,
26}
27
28impl Anchor {
29 pub fn below(rect: Rect) -> Self {
32 Self {
33 rect,
34 side: Side::Below,
35 }
36 }
37
38 pub fn beside(rect: Rect) -> Self {
41 Self {
42 rect,
43 side: Side::Beside,
44 }
45 }
46
47 pub fn at(point: Point) -> Self {
49 Self::below(Rect::new(point.x, point.y, 0.0, 0.0))
50 }
51
52 pub(crate) fn place(&self, size: Size, viewport: Size) -> Point {
54 let fit = |start: f32, length: f32, room: f32| start.min(room - length).max(0.0);
55 let rect = self.rect;
56 match self.side {
57 Side::Below => {
58 let below = rect.bottom();
59 let y = if below + size.height <= viewport.height {
60 below
61 } else if rect.top() - size.height >= 0.0 {
62 rect.top() - size.height
63 } else {
64 fit(below, size.height, viewport.height)
65 };
66 Point::new(fit(rect.left(), size.width, viewport.width), y)
67 }
68 Side::Beside => {
69 let right = rect.right();
70 let x = if right + size.width <= viewport.width {
71 right
72 } else if rect.left() - size.width >= 0.0 {
73 rect.left() - size.width
74 } else {
75 fit(right, size.width, viewport.width)
76 };
77 Point::new(x, fit(rect.top(), size.height, viewport.height))
78 }
79 }
80 }
81}
82
83#[derive(Debug, Clone, Copy)]
85pub(crate) struct Popup {
86 pub(crate) id: WidgetId,
87 pub(crate) anchor: Anchor,
88 pub(crate) opener: Option<WidgetId>,
91 pub(crate) focus_before: Option<WidgetId>,
93}
94
95#[derive(Default)]
97pub(crate) struct Incoming(pub(crate) Vec<(WidgetId, Box<dyn Widget>)>);
98
99impl fmt::Debug for Incoming {
100 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101 f.debug_list()
102 .entries(self.0.iter().map(|(id, _)| id))
103 .finish()
104 }
105}
106
107impl State {
108 pub(crate) fn open_popup(
111 &mut self,
112 widget: Box<dyn Widget>,
113 anchor: Anchor,
114 style: Style,
115 opener: Option<WidgetId>,
116 ) -> Result<WidgetId, UiError> {
117 let style = Style {
118 position: Position::Absolute,
119 ..style
120 };
121 let layer = self.layer;
122 let end = self.nodes[layer.slot()].children.len();
123 let id = self.new_node(layer, end, style)?;
124 let focus_before = self.requested_focus.unwrap_or(self.focus);
127 if widget.accepts_focus() {
128 self.requested_focus = Some(Some(id));
129 }
130 self.popups.push(Popup {
131 id,
132 anchor,
133 opener,
134 focus_before,
135 });
136 self.incoming.0.push((id, widget));
137 Ok(id)
138 }
139
140 pub(crate) fn close_popup(&mut self, id: WidgetId) -> bool {
144 match self.popups.iter().position(|popup| popup.id == id) {
145 Some(index) => {
146 self.close_from(index);
147 true
148 }
149 None => false,
150 }
151 }
152
153 pub(crate) fn close_from(&mut self, index: usize) {
155 let Some(first) = self.popups.get(index).copied() else {
156 return;
157 };
158 let focus_inside = self.focus.is_some_and(|focus| {
159 self.popups[index..]
160 .iter()
161 .any(|popup| self.is_within(focus, popup.id))
162 });
163 for popup in self.popups.drain(index..).rev() {
164 self.closing.push(popup.id);
165 }
166 if focus_inside {
167 self.requested_focus = Some(first.focus_before);
168 }
169 self.needs_paint = true;
170 }
171
172 pub(crate) fn is_popup_open(&self, id: WidgetId) -> bool {
174 self.popups.iter().any(|popup| popup.id == id) && self.is_live(id)
175 }
176
177 pub(crate) fn popup_origin(&self, id: WidgetId, size: Size) -> Option<Point> {
179 let popup = self.popups.iter().find(|popup| popup.id == id)?;
180 Some(popup.anchor.place(size, self.viewport()))
181 }
182}
183
184impl Ui {
185 pub fn open_popup(
195 &mut self,
196 widget: impl Widget,
197 anchor: Anchor,
198 style: Style,
199 ) -> Result<WidgetId, UiError> {
200 let id = self
201 .state
202 .open_popup(Box::new(widget), anchor, style, None)?;
203 self.settle();
204 Ok(id)
205 }
206
207 pub fn close_popup(&mut self, id: WidgetId) -> bool {
209 let closed = self.state.close_popup(id);
210 self.settle();
211 closed
212 }
213
214 pub fn close_popups(&mut self) {
216 self.state.close_from(0);
217 self.settle();
218 }
219
220 pub fn popups(&self) -> impl Iterator<Item = WidgetId> + '_ {
222 self.state.popups.iter().map(|popup| popup.id)
223 }
224
225 pub fn is_popup_open(&self, id: WidgetId) -> bool {
227 self.state.is_popup_open(id)
228 }
229
230 pub(crate) fn adopt(&mut self) {
232 let mut incoming = std::mem::take(&mut self.state.incoming.0);
233 for (id, widget) in incoming.drain(..) {
234 if !self.state.is_live(id) {
235 continue;
236 }
237 let slot = id.slot();
238 if slot >= self.widgets.len() {
239 self.widgets.resize_with(slot + 1, || None);
240 }
241 self.widgets[slot] = Some(widget);
242 }
243 if self.state.incoming.0.is_empty() {
245 self.state.incoming.0 = incoming;
246 }
247 let mut closing = std::mem::take(&mut self.state.closing);
248 for id in closing.drain(..) {
249 self.remove_widget(id);
250 }
251 if self.state.closing.is_empty() {
252 self.state.closing = closing;
253 }
254 }
255}
256
257impl EventCx<'_> {
258 pub fn open_popup(
266 &mut self,
267 widget: impl Widget,
268 anchor: Anchor,
269 style: Style,
270 ) -> Result<WidgetId, UiError> {
271 let opener = Some(self.id);
272 self.state
273 .open_popup(Box::new(widget), anchor, style, opener)
274 }
275
276 pub fn close_popup(&mut self, id: WidgetId) -> bool {
279 self.state.close_popup(id)
280 }
281
282 pub fn close_popups(&mut self) {
284 self.state.close_from(0);
285 }
286
287 pub fn is_popup_open(&self, id: WidgetId) -> bool {
289 self.state.is_popup_open(id)
290 }
291}
292
293impl PaintCx<'_> {
294 pub fn is_popup_open(&self, id: WidgetId) -> bool {
296 self.state.is_popup_open(id)
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303
304 const VIEW: Size = Size {
305 width: 800.0,
306 height: 600.0,
307 };
308
309 #[test]
310 fn below_fits_flips_and_clamps() {
311 let size = Size::new(200.0, 100.0);
312 let under = Anchor::below(Rect::new(10.0, 20.0, 80.0, 30.0));
313 assert_eq!(under.place(size, VIEW), Point::new(10.0, 50.0));
314 let low = Anchor::below(Rect::new(700.0, 550.0, 80.0, 30.0));
315 assert_eq!(
316 low.place(size, VIEW),
317 Point::new(600.0, 450.0),
318 "above, moved left"
319 );
320 let tall = Anchor::below(Rect::new(0.0, 50.0, 10.0, 10.0));
321 assert_eq!(
322 tall.place(Size::new(100.0, 580.0), VIEW),
323 Point::new(0.0, 20.0),
324 "neither fits: as low as it can go"
325 );
326 }
327
328 #[test]
329 fn beside_fits_flips_and_clamps() {
330 let size = Size::new(200.0, 100.0);
331 let item = Anchor::beside(Rect::new(100.0, 40.0, 150.0, 24.0));
332 assert_eq!(item.place(size, VIEW), Point::new(250.0, 40.0));
333 let edge = Anchor::beside(Rect::new(500.0, 560.0, 150.0, 24.0));
334 assert_eq!(
335 edge.place(size, VIEW),
336 Point::new(300.0, 500.0),
337 "left, moved up"
338 );
339 let wide = Anchor::beside(Rect::new(100.0, 0.0, 600.0, 24.0));
340 assert_eq!(
341 wide.place(size, VIEW),
342 Point::new(600.0, 0.0),
343 "neither side: pushed in"
344 );
345 }
346
347 #[test]
348 fn at_a_point_is_below_an_empty_rect() {
349 let size = Size::new(200.0, 100.0);
350 assert_eq!(
351 Anchor::at(Point::new(790.0, 590.0)).place(size, VIEW),
352 Point::new(600.0, 490.0)
353 );
354 }
355}