1use crate::context::TreeProps;
2use crate::prelude::*;
3use bitflags::bitflags;
4
5use crate::vg;
6
7#[derive(Debug, Default, Clone)]
9pub struct PopupData {
10 pub is_open: bool,
12}
13
14impl From<PopupData> for bool {
15 fn from(value: PopupData) -> Self {
16 value.is_open
17 }
18}
19
20impl Model for PopupData {
21 fn event(&mut self, _: &mut EventContext, event: &mut Event) {
22 event.map(|popup_event, meta| match popup_event {
23 PopupEvent::Open => {
24 self.is_open = true;
25 meta.consume();
26 }
27
28 PopupEvent::Close => {
29 self.is_open = false;
30 meta.consume();
31 }
32
33 PopupEvent::Switch => {
34 self.is_open ^= true;
35 meta.consume();
36 }
37 });
38 }
39}
40
41#[derive(Debug)]
43pub enum PopupEvent {
44 Open,
46 Close,
48 Switch,
50}
51
52pub struct Popover {
54 placement: Signal<Placement>,
55 show_arrow: Signal<bool>,
56 arrow_size: Signal<Length>,
57 should_reposition: Signal<bool>,
58}
59
60impl Popover {
61 pub fn new(cx: &mut Context, content: impl FnOnce(&mut Context)) -> Handle<Self> {
63 let placement = Signal::new(Placement::Bottom);
64 let show_arrow = Signal::new(true);
65 let arrow_size = Signal::new(Length::Value(LengthValue::Px(8.0)));
66 let should_reposition = Signal::new(true);
67
68 Self { placement, show_arrow, arrow_size, should_reposition }
69 .build(cx, |cx| {
70 (content)(cx);
71 Binding::new(cx, show_arrow, move |cx| {
72 let show_arrow = show_arrow.get();
73 if show_arrow {
74 Arrow::new(cx, placement, arrow_size);
75 }
76 });
77 })
78 .position_type(PositionType::Absolute)
79 .ignore_clipping(true)
80 .space(Pixels(0.0))
81 }
82}
83
84impl View for Popover {
85 fn element(&self) -> Option<&'static str> {
86 Some("popup")
87 }
88
89 fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
90 event.map(|window_event, _| match window_event {
91 WindowEvent::GeometryChanged(_) => {
93 let parent_bounds = cx.parent_transformed_bounds();
94 let bounds = cx.bounds();
95 let window_bounds = cx.cache.get_bounds(cx.parent_window());
96 let scale = cx.scale_factor();
97 let arrow_size = self.arrow_size.get().to_px().unwrap() * cx.scale_factor();
98
99 let shift = if self.should_reposition.get() {
100 let mut available = AvailablePlacement::all();
101
102 let top_start_bounds = BoundingBox::from_min_max(
103 parent_bounds.left(),
104 parent_bounds.top() - bounds.height() - arrow_size,
105 parent_bounds.left() + bounds.width(),
106 parent_bounds.top(),
107 );
108
109 available.set(
110 AvailablePlacement::TOP_START,
111 window_bounds.contains(&top_start_bounds),
112 );
113
114 let top_bounds = BoundingBox::from_min_max(
115 parent_bounds.center().0 - bounds.width() / 2.0,
116 parent_bounds.top() - bounds.height() - arrow_size,
117 parent_bounds.center().0 + bounds.width() / 2.0,
118 parent_bounds.top(),
119 );
120
121 available.set(AvailablePlacement::TOP, window_bounds.contains(&top_bounds));
122
123 let top_end_bounds = BoundingBox::from_min_max(
124 parent_bounds.right() - bounds.width(),
125 parent_bounds.top() - bounds.height() - arrow_size,
126 parent_bounds.right(),
127 parent_bounds.top(),
128 );
129
130 available
131 .set(AvailablePlacement::TOP_END, window_bounds.contains(&top_end_bounds));
132
133 let bottom_start_bounds = BoundingBox::from_min_max(
134 parent_bounds.left(),
135 parent_bounds.bottom(),
136 parent_bounds.left() + bounds.width(),
137 parent_bounds.bottom() + bounds.height() + arrow_size,
138 );
139
140 available.set(
141 AvailablePlacement::BOTTOM_START,
142 window_bounds.contains(&bottom_start_bounds),
143 );
144
145 let bottom_bounds = BoundingBox::from_min_max(
146 parent_bounds.center().0 - bounds.width() / 2.0,
147 parent_bounds.bottom(),
148 parent_bounds.center().0 + bounds.width() / 2.0,
149 parent_bounds.bottom() + bounds.height() + arrow_size,
150 );
151
152 available
153 .set(AvailablePlacement::BOTTOM, window_bounds.contains(&bottom_bounds));
154
155 let bottom_end_bounds = BoundingBox::from_min_max(
156 parent_bounds.right() - bounds.width(),
157 parent_bounds.bottom(),
158 parent_bounds.right(),
159 parent_bounds.bottom() + bounds.height() + arrow_size,
160 );
161
162 available.set(
163 AvailablePlacement::BOTTOM_END,
164 window_bounds.contains(&bottom_end_bounds),
165 );
166
167 let left_start_bounds = BoundingBox::from_min_max(
168 parent_bounds.left() - bounds.width() - arrow_size,
169 parent_bounds.top(),
170 parent_bounds.left(),
171 parent_bounds.top() + bounds.height(),
172 );
173
174 available.set(
175 AvailablePlacement::LEFT_START,
176 window_bounds.contains(&left_start_bounds),
177 );
178
179 let left_bounds = BoundingBox::from_min_max(
180 parent_bounds.left() - bounds.width() - arrow_size,
181 parent_bounds.center().1 - bounds.height() / 2.0,
182 parent_bounds.left(),
183 parent_bounds.center().1 + bounds.height() / 2.0,
184 );
185
186 available.set(AvailablePlacement::LEFT, window_bounds.contains(&left_bounds));
187
188 let left_end_bounds = BoundingBox::from_min_max(
189 parent_bounds.left() - bounds.width() - arrow_size,
190 parent_bounds.bottom() - bounds.height(),
191 parent_bounds.left(),
192 parent_bounds.bottom(),
193 );
194
195 available.set(
196 AvailablePlacement::LEFT_END,
197 window_bounds.contains(&left_end_bounds),
198 );
199
200 let right_start_bounds = BoundingBox::from_min_max(
201 parent_bounds.right(),
202 parent_bounds.top(),
203 parent_bounds.right() + bounds.width() + arrow_size,
204 parent_bounds.top() + bounds.height(),
205 );
206
207 available.set(
208 AvailablePlacement::RIGHT_START,
209 window_bounds.contains(&right_start_bounds),
210 );
211
212 let right_bounds = BoundingBox::from_min_max(
213 parent_bounds.right(),
214 parent_bounds.center().1 - bounds.height() / 2.0,
215 parent_bounds.right() + bounds.width() + arrow_size,
216 parent_bounds.center().1 + bounds.height() / 2.0,
217 );
218
219 available.set(AvailablePlacement::RIGHT, window_bounds.contains(&right_bounds));
220
221 let right_end_bounds = BoundingBox::from_min_max(
222 parent_bounds.right(),
223 parent_bounds.bottom() - bounds.height(),
224 parent_bounds.right() + bounds.width() + arrow_size,
225 parent_bounds.bottom(),
226 );
227
228 available.set(
229 AvailablePlacement::RIGHT_END,
230 window_bounds.contains(&right_end_bounds),
231 );
232
233 self.placement.get().place(available)
234 } else {
235 if let Some(first_child) = cx.tree.get_layout_first_child(cx.current) {
236 let mut child_bounds = cx.cache.get_bounds(first_child);
237 child_bounds.h = window_bounds.bottom()
238 - parent_bounds.bottom()
239 - arrow_size * scale
240 - 8.0;
241 cx.style.max_height.insert(first_child, Pixels(child_bounds.h / scale));
242 }
243 self.placement.get()
244 };
245
246 let arrow_size = self.arrow_size.get().to_px().unwrap();
247
248 let translate = match shift {
249 Placement::Top => (
250 -(bounds.width() - parent_bounds.width()) / (2.0 * scale),
251 -bounds.height() / scale - arrow_size,
252 ),
253 Placement::TopStart => (0.0, -bounds.height() / scale - arrow_size),
254 Placement::TopEnd => (
255 -(bounds.width() - parent_bounds.width()) / scale,
256 -bounds.height() / scale - arrow_size,
257 ),
258 Placement::Bottom => (
259 -(bounds.width() - parent_bounds.width()) / (2.0 * scale),
260 parent_bounds.height() / scale + arrow_size,
261 ),
262 Placement::BottomStart => (0.0, parent_bounds.height() / scale + arrow_size),
263 Placement::BottomEnd => (
264 -(bounds.width() - parent_bounds.width()) / scale,
265 parent_bounds.height() / scale + arrow_size,
266 ),
267 Placement::LeftStart => (-(bounds.width() / scale) - arrow_size, 0.0),
268 Placement::Left => (
269 -(bounds.width() / scale) - arrow_size,
270 -(bounds.height() - parent_bounds.height()) / (2.0 * scale),
271 ),
272 Placement::LeftEnd => (
273 -(bounds.width() / scale) - arrow_size,
274 -(bounds.height() - parent_bounds.height()) / scale,
275 ),
276 Placement::RightStart => ((parent_bounds.width() / scale) + arrow_size, 0.0),
277 Placement::Right => (
278 (parent_bounds.width() / scale) + arrow_size,
279 -(bounds.height() - parent_bounds.height()) / (2.0 * scale),
280 ),
281 Placement::RightEnd => (
282 (parent_bounds.width() / scale) + arrow_size,
283 -(bounds.height() - parent_bounds.height()) / scale,
284 ),
285
286 Placement::Cursor => {
287 let cursor_x = cx.mouse().cursor_x;
288 let cursor_y = cx.mouse().cursor_y;
289
290 let max_x = window_bounds.right() - bounds.width();
291 let max_y = window_bounds.bottom() - bounds.height();
292
293 let clamped_x = if max_x < window_bounds.left() {
294 window_bounds.left()
295 } else {
296 cursor_x.clamp(window_bounds.left(), max_x)
297 };
298
299 let clamped_y = if max_y < window_bounds.top() {
300 window_bounds.top()
301 } else {
302 cursor_y.clamp(window_bounds.top(), max_y)
303 };
304
305 ((clamped_x - bounds.x) / scale, (clamped_y - bounds.y) / scale)
306 }
307
308 _ => (0.0, 0.0),
309 };
310 cx.set_translate((Pixels(translate.0.round()), Pixels(translate.1.round())));
311 }
312
313 _ => {}
314 });
315 }
316}
317
318bitflags! {
319 #[derive(Debug, Clone, Copy)]
320 pub(crate) struct AvailablePlacement: u16 {
321 const TOP_START = 1 << 0;
322 const TOP = 1 << 1;
323 const TOP_END = 1 << 2;
324 const LEFT_START = 1 << 3;
325 const LEFT = 1 << 4;
326 const LEFT_END = 1 << 5;
327 const BOTTOM_START = 1 << 6;
328 const BOTTOM = 1 << 7;
329 const BOTTOM_END = 1 << 8;
330 const RIGHT_START = 1 << 9;
331 const RIGHT = 1 << 10;
332 const RIGHT_END = 1 << 11;
333 }
334}
335
336impl AvailablePlacement {
337 fn can_place(&self, placement: Placement) -> bool {
338 match placement {
339 Placement::Bottom => self.contains(AvailablePlacement::BOTTOM),
340 Placement::BottomStart => self.contains(AvailablePlacement::BOTTOM_START),
341 Placement::BottomEnd => self.contains(AvailablePlacement::BOTTOM_END),
342 Placement::Top => self.contains(AvailablePlacement::TOP),
343 Placement::TopStart => self.contains(AvailablePlacement::TOP_START),
344 Placement::TopEnd => self.contains(AvailablePlacement::TOP_END),
345 Placement::Left => self.contains(AvailablePlacement::LEFT),
346 Placement::LeftStart => self.contains(AvailablePlacement::LEFT_START),
347 Placement::LeftEnd => self.contains(AvailablePlacement::LEFT_END),
348 Placement::Right => self.contains(AvailablePlacement::RIGHT),
349 Placement::RightStart => self.contains(AvailablePlacement::RIGHT_START),
350 Placement::RightEnd => self.contains(AvailablePlacement::RIGHT_END),
351 _ => false,
352 }
353 }
354}
355
356impl Placement {
357 fn from_int(int: u16) -> Placement {
358 match int {
359 0 => Placement::TopStart,
360 1 => Placement::Top,
361 2 => Placement::TopEnd,
362 3 => Placement::BottomStart,
363 4 => Placement::Bottom,
364 5 => Placement::BottomEnd,
365 6 => Placement::RightStart,
366 7 => Placement::Right,
367 8 => Placement::RightEnd,
368 9 => Placement::LeftStart,
369 10 => Placement::Left,
370 11 => Placement::LeftEnd,
371 12 => Placement::Over,
372 _ => Placement::Cursor,
373 }
374 }
375
376 pub(crate) fn place(&self, available: AvailablePlacement) -> Placement {
377 if *self == Placement::Over || *self == Placement::Cursor {
378 return *self;
379 }
380
381 if available.is_empty() {
382 return Placement::Over;
383 }
384
385 let mut placement = *self;
386
387 while !available.can_place(placement) {
388 placement = placement.next(*self);
389 }
390
391 placement
392 }
393
394 fn next(&self, original: Self) -> Self {
395 const TOP_START: [u16; 12] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
396 const TOP: [u16; 12] = [2, 0, 4, 5, 3, 7, 8, 6, 10, 11, 9, 12];
397 const TOP_END: [u16; 12] = [5, 0, 1, 8, 3, 4, 11, 6, 7, 12, 9, 10];
398 const BOTTOM_START: [u16; 12] = [1, 2, 6, 4, 5, 0, 7, 8, 9, 10, 11, 12];
399 const BOTTOM: [u16; 12] = [2, 0, 7, 5, 3, 1, 8, 6, 10, 11, 9, 12];
400 const BOTTOM_END: [u16; 12] = [8, 0, 1, 2, 3, 4, 11, 6, 7, 12, 9, 10];
401 const LEFT_START: [u16; 12] = [1, 2, 12, 4, 5, 0, 7, 8, 3, 10, 11, 6];
402 const LEFT: [u16; 12] = [2, 0, 12, 5, 3, 1, 8, 6, 4, 11, 9, 7];
403 const LEFT_END: [u16; 12] = [12, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
404 const RIGHT_START: [u16; 12] = [1, 2, 12, 4, 5, 0, 7, 8, 9, 10, 11, 3];
405 const RIGHT: [u16; 12] = [2, 0, 12, 5, 3, 1, 8, 6, 10, 11, 9, 4];
406 const RIGHT_END: [u16; 12] = [12, 0, 1, 2, 3, 4, 11, 6, 7, 5, 9, 10];
407
408 let states = match original {
409 Placement::TopStart => TOP_START,
410 Placement::Top => TOP,
411 Placement::TopEnd => TOP_END,
412 Placement::BottomStart => BOTTOM_START,
413 Placement::Bottom => BOTTOM,
414 Placement::BottomEnd => BOTTOM_END,
415 Placement::RightStart => RIGHT_START,
416 Placement::Right => RIGHT,
417 Placement::RightEnd => RIGHT_END,
418 Placement::LeftStart => LEFT_START,
419 Placement::Left => LEFT,
420 Placement::LeftEnd => LEFT_END,
421 _ => unreachable!(),
422 };
423
424 Placement::from_int(states[*self as usize])
425 }
426}
427
428pub trait PopoverModifiers: Sized {
430 fn placement(self, placement: impl Res<Placement> + 'static) -> Self;
433
434 fn show_arrow(self, show_arrow: impl Res<bool> + 'static) -> Self;
436
437 fn arrow_size<U: Into<Length> + Clone + 'static>(self, size: impl Res<U> + 'static) -> Self;
439
440 fn should_reposition(self, should_reposition: impl Res<bool> + 'static) -> Self;
442
443 fn on_blur<F>(self, f: F) -> Self
446 where
447 F: 'static + Fn(&mut EventContext);
448}
449
450impl PopoverModifiers for Handle<'_, Popover> {
451 fn placement(self, placement: impl Res<Placement> + 'static) -> Self {
452 let placement = placement.to_signal(self.cx);
453 self.bind(placement, move |handle| {
454 let placement = placement.get();
455 handle.modify(|popup| {
456 popup.placement.set(placement);
457 });
458 })
459 }
460
461 fn show_arrow(self, show_arrow: impl Res<bool> + 'static) -> Self {
462 let show_arrow = show_arrow.to_signal(self.cx);
463 self.bind(show_arrow, move |handle| {
464 let show_arrow = show_arrow.get();
465 handle.modify(|popup| popup.show_arrow.set(show_arrow));
466 })
467 }
468
469 fn arrow_size<U: Into<Length> + Clone + 'static>(self, size: impl Res<U> + 'static) -> Self {
470 let size = size.to_signal(self.cx);
471 self.bind(size, move |handle| {
472 let size = size.get();
473 let size = size.into();
474 handle.modify(|popup| popup.arrow_size.set(size));
475 })
476 }
477
478 fn should_reposition(self, should_reposition: impl Res<bool> + 'static) -> Self {
479 let should_reposition = should_reposition.to_signal(self.cx);
480 self.bind(should_reposition, move |handle| {
481 let should_reposition = should_reposition.get();
482 handle.modify(|popup| popup.should_reposition.set(should_reposition));
483 })
484 }
485
486 fn on_blur<F>(self, f: F) -> Self
487 where
488 F: 'static + Fn(&mut EventContext),
489 {
490 let focus_event = Box::new(f);
491 self.cx.with_current(self.entity, |cx| {
492 cx.add_listener(move |_: &mut Popover, cx, event| {
493 event.map(|window_event, meta| match window_event {
494 WindowEvent::MouseDown(_) => {
495 if meta.origin != cx.current() {
496 if !cx.hovered.is_descendant_of(cx.tree, cx.current) {
498 (focus_event)(cx);
499 meta.consume();
500 }
501 }
502 }
503
504 WindowEvent::KeyDown(code, _) => {
505 if *code == Code::Escape {
506 (focus_event)(cx);
507 }
508 }
509
510 _ => {}
511 });
512 });
513 });
514
515 self
516 }
517}
518
519pub(crate) struct Arrow {
521 placement: Signal<Placement>,
522}
523
524impl Arrow {
525 pub(crate) fn new(
526 cx: &mut Context,
527 placement: Signal<Placement>,
528 arrow_size: Signal<Length>,
529 ) -> Handle<Self> {
530 Self { placement }.build(cx, |_| {}).position_type(PositionType::Absolute).bind(
531 placement,
532 move |mut handle| {
533 let placement = placement.get();
534 let (t, b) = match placement {
535 Placement::TopStart | Placement::Top | Placement::TopEnd => {
536 (Percentage(100.0), Stretch(1.0))
537 }
538 Placement::BottomStart | Placement::Bottom | Placement::BottomEnd => {
539 (Stretch(1.0), Percentage(100.0))
540 }
541 _ => (Stretch(1.0), Stretch(1.0)),
542 };
543
544 let (l, r) = match placement {
545 Placement::LeftStart | Placement::Left | Placement::LeftEnd => {
546 (Percentage(100.0), Stretch(1.0))
547 }
548 Placement::RightStart | Placement::Right | Placement::RightEnd => {
549 (Stretch(1.0), Percentage(100.0))
550 }
551 Placement::TopStart | Placement::BottomStart => {
552 (Pixels(8.0), Stretch(1.0))
554 }
555 Placement::TopEnd | Placement::BottomEnd => {
556 (Stretch(1.0), Pixels(8.0))
558 }
559 _ => (Stretch(1.0), Stretch(1.0)),
560 };
561
562 handle = handle
563 .top(t)
564 .bottom(b)
565 .left(l)
566 .right(r)
567 .position_type(PositionType::Absolute)
568 .hoverable(false);
569
570 handle.bind(arrow_size, move |handle| {
571 let arrow_size = arrow_size.get();
572 let arrow_size = arrow_size.to_px().unwrap_or(8.0);
573 let (w, h) = match placement {
574 Placement::Top
575 | Placement::Bottom
576 | Placement::TopStart
577 | Placement::BottomStart
578 | Placement::TopEnd
579 | Placement::BottomEnd => (Pixels(arrow_size * 2.0), Pixels(arrow_size)),
580
581 _ => (Pixels(arrow_size), Pixels(arrow_size * 2.0)),
582 };
583
584 handle.width(w).height(h);
585 });
586 },
587 )
588 }
589}
590
591impl View for Arrow {
592 fn element(&self) -> Option<&'static str> {
593 Some("arrow")
594 }
595 fn draw(&self, cx: &mut DrawContext, canvas: &Canvas) {
596 let bounds = cx.bounds();
597 let mut path = vg::PathBuilder::new();
598 match self.placement.get() {
599 Placement::Bottom | Placement::BottomStart | Placement::BottomEnd => {
600 path.move_to(bounds.bottom_left());
601 path.line_to(bounds.center_top());
602 path.line_to(bounds.bottom_right());
603 path.line_to(bounds.bottom_left());
604 }
605
606 Placement::Top | Placement::TopStart | Placement::TopEnd => {
607 path.move_to(bounds.top_left());
608 path.line_to(bounds.center_bottom());
609 path.line_to(bounds.top_right());
610 path.line_to(bounds.top_left());
611 }
612
613 Placement::Left | Placement::LeftStart | Placement::LeftEnd => {
614 path.move_to(bounds.top_left());
615 path.line_to(bounds.center_right());
616 path.line_to(bounds.bottom_left());
617 path.line_to(bounds.top_left());
618 }
619
620 Placement::Right | Placement::RightStart | Placement::RightEnd => {
621 path.move_to(bounds.top_right());
622 path.line_to(bounds.center_left());
623 path.line_to(bounds.bottom_right());
624 path.line_to(bounds.top_right());
625 }
626
627 _ => {}
628 }
629 path.close();
630
631 let bg = cx.background_color();
632 let mut paint = vg::Paint::default();
633 paint.set_color(bg);
634 let path = path.detach();
635 canvas.draw_path(&path, &paint);
636 }
637}