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