1use iced::advanced::layout::{self, Layout};
27use iced::advanced::overlay;
28use iced::advanced::renderer;
29use iced::advanced::widget::operation::Focusable;
30use iced::advanced::widget::{Id, Operation, Tree, Widget, tree};
31use iced::advanced::{Clipboard, Shell};
32use iced::keyboard::{self, key::Named};
33use iced::widget::container;
34use iced::{
35 Background, Border, Color, Element, Event, Length, Point, Rectangle, Renderer, Shadow, Size,
36 Theme, Vector, mouse, touch,
37};
38
39use crate::keys::{self, Chord};
40use crate::theme::{Tokens, fade, radius};
41
42pub const GAP: f32 = 4.0;
44
45pub const MARGIN: f32 = 8.0;
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
50pub enum Side {
51 Top,
52 #[default]
53 Bottom,
54 Left,
55 Right,
56}
57
58impl Side {
59 pub const ALL: [Side; 4] = [Side::Top, Side::Bottom, Side::Left, Side::Right];
60
61 pub fn opposite(self) -> Side {
62 match self {
63 Side::Top => Side::Bottom,
64 Side::Bottom => Side::Top,
65 Side::Left => Side::Right,
66 Side::Right => Side::Left,
67 }
68 }
69
70 fn is_vertical(self) -> bool {
71 matches!(self, Side::Top | Side::Bottom)
72 }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
77pub enum Align {
78 Start,
80 #[default]
81 Center,
82 End,
84}
85
86impl Align {
87 pub const ALL: [Align; 3] = [Align::Start, Align::Center, Align::End];
88
89 fn mirrored(self) -> Align {
90 match self {
91 Align::Start => Align::End,
92 Align::Center => Align::Center,
93 Align::End => Align::Start,
94 }
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq)]
100pub struct Placement {
101 pub side: Side,
102 pub align: Align,
103 pub gap: f32,
105 pub offset: f32,
107}
108
109impl Default for Placement {
110 fn default() -> Self {
111 Self {
112 side: Side::default(),
113 align: Align::default(),
114 gap: GAP,
115 offset: 0.0,
116 }
117 }
118}
119
120impl Placement {
121 pub fn new(side: Side, align: Align) -> Self {
122 Self {
123 side,
124 align,
125 ..Self::default()
126 }
127 }
128
129 pub fn gap(mut self, gap: f32) -> Self {
130 self.gap = gap;
131 self
132 }
133
134 pub fn offset(mut self, offset: f32) -> Self {
135 self.offset = offset;
136 self
137 }
138}
139
140pub fn place(
150 anchor: Rectangle,
151 size: Size,
152 viewport: Rectangle,
153 placement: Placement,
154) -> Rectangle {
155 let area = viewport.shrink(MARGIN);
156 let side = choose_side(anchor, size, area, placement);
157 let offset = if side.is_vertical() == placement.side.is_vertical() {
160 placement.offset
161 } else {
162 0.0
163 };
164
165 let main = match side {
166 Side::Top => anchor.y - placement.gap - size.height,
167 Side::Bottom => anchor.y + anchor.height + placement.gap,
168 Side::Left => anchor.x - placement.gap - size.width,
169 Side::Right => anchor.x + anchor.width + placement.gap,
170 };
171
172 let (start, length, extent, low, high) = if side.is_vertical() {
173 (
174 anchor.x,
175 anchor.width,
176 size.width,
177 area.x,
178 area.x + area.width,
179 )
180 } else {
181 (
182 anchor.y,
183 anchor.height,
184 size.height,
185 area.y,
186 area.y + area.height,
187 )
188 };
189 let cross_at = |align: Align| match align {
190 Align::Start => start + offset,
191 Align::Center => start + (length - extent) / 2.0 + offset,
192 Align::End => start + length - extent - offset,
193 };
194 let fits = |value: f32| value >= low && value + extent <= high;
195 let preferred = cross_at(placement.align);
196 let mirrored = cross_at(placement.align.mirrored());
197 let cross = if !fits(preferred) && fits(mirrored) {
198 mirrored
199 } else {
200 preferred
201 };
202
203 let (x, y) = if side.is_vertical() {
204 (cross, main)
205 } else {
206 (main, cross)
207 };
208 Rectangle::new(
209 Point::new(
210 clamp(x, area.x, area.x + area.width - size.width),
211 clamp(y, area.y, area.y + area.height - size.height),
212 ),
213 size,
214 )
215}
216
217fn choose_side(anchor: Rectangle, size: Size, area: Rectangle, placement: Placement) -> Side {
222 let room = |side: Side| room(anchor, area, placement.gap, side);
223 let fits = |side: Side| room(side) >= needed(size, side);
224 let roomier = |side: Side| {
225 if room(side.opposite()) > room(side) {
226 side.opposite()
227 } else {
228 side
229 }
230 };
231
232 let requested = placement.side;
233 let opposite = requested.opposite();
234 if fits(requested) {
235 return requested;
236 }
237 if fits(opposite) {
238 return opposite;
239 }
240 if requested.is_vertical() {
241 return roomier(requested);
242 }
243 [Side::Bottom, Side::Top]
244 .into_iter()
245 .find(|side| fits(*side))
246 .unwrap_or_else(|| roomier(Side::Bottom))
247}
248
249fn needed(size: Size, side: Side) -> f32 {
251 if side.is_vertical() {
252 size.height
253 } else {
254 size.width
255 }
256}
257
258fn room(anchor: Rectangle, area: Rectangle, gap: f32, side: Side) -> f32 {
260 match side {
261 Side::Top => anchor.y - gap - area.y,
262 Side::Bottom => area.y + area.height - (anchor.y + anchor.height + gap),
263 Side::Left => anchor.x - gap - area.x,
264 Side::Right => area.x + area.width - (anchor.x + anchor.width + gap),
265 }
266}
267
268pub fn fitted_height(
274 anchor: Rectangle,
275 size: Size,
276 viewport: Rectangle,
277 placement: Placement,
278) -> Option<f32> {
279 let area = viewport.shrink(MARGIN);
280 let side = placement.side;
281 let beside = |side: Side| room(anchor, area, placement.gap, side) >= size.width;
282 if !side.is_vertical() && (beside(side) || beside(side.opposite())) {
283 return None;
284 }
285 let above = room(anchor, area, placement.gap, Side::Top);
286 let below = room(anchor, area, placement.gap, Side::Bottom);
287 if above >= size.height || below >= size.height {
288 return None;
289 }
290 Some(above.max(below).max(0.0))
291}
292
293fn clamp(value: f32, low: f32, high: f32) -> f32 {
295 value.min(high).max(low)
296}
297
298pub fn surface_style(tokens: &Tokens) -> container::Style {
301 container::Style {
302 background: Some(Background::Color(tokens.popover)),
303 text_color: Some(tokens.foreground),
304 border: Border {
305 color: tokens.border,
306 width: 1.0,
307 radius: radius::MD.into(),
308 },
309 shadow: shadow(tokens),
310 snap: true,
311 }
312}
313
314pub fn shadow(tokens: &Tokens) -> Shadow {
316 Shadow {
317 color: fade(Color::BLACK, if tokens.is_dark { 0.5 } else { 0.1 }),
318 offset: Vector::new(0.0, 4.0),
319 blur_radius: 12.0,
320 }
321}
322
323type OnKey<'a, Message> = Box<dyn Fn(&keys::Event) -> Option<Message> + 'a>;
324
325pub(crate) struct Behaviour<'a, Message> {
328 pub(crate) placement: Placement,
329 pub(crate) point: Option<Point>,
330 pub(crate) match_width: bool,
331 pub(crate) fit_height: bool,
332 pub(crate) on_dismiss: Option<Message>,
333 pub(crate) dismiss_keys: Vec<Chord>,
334 pub(crate) dismiss_on_anchor_press: bool,
335 pub(crate) dismiss_on_blur: bool,
336 pub(crate) pass_through: bool,
337 pub(crate) closes_itself: bool,
340 pub(crate) on_key: Option<OnKey<'a, Message>>,
341 pub(crate) on_anchor_press: Option<Message>,
342 pub(crate) on_focus: Option<OnFocus<'a, Message>>,
343}
344
345type OnFocus<'a, Message> = Box<dyn Fn(bool) -> Message + 'a>;
346
347impl<Message> Default for Behaviour<'_, Message> {
348 fn default() -> Self {
349 Self {
350 placement: Placement::default(),
351 point: None,
352 match_width: false,
353 fit_height: false,
354 on_dismiss: None,
355 dismiss_keys: vec![Chord::named(Named::Escape)],
356 dismiss_on_anchor_press: false,
357 dismiss_on_blur: false,
358 pass_through: false,
359 closes_itself: false,
360 on_key: None,
361 on_anchor_press: None,
362 on_focus: None,
363 }
364 }
365}
366
367impl<Message> Behaviour<'_, Message> {
368 fn can_dismiss(&self) -> bool {
369 self.on_dismiss.is_some() || self.closes_itself
370 }
371
372 fn watches_focus(&self) -> bool {
373 self.on_key.is_some() || self.dismiss_on_blur || self.on_focus.is_some()
374 }
375
376 fn focus_message(&self, was: bool, is: bool) -> Option<Message> {
379 if was == is {
380 return None;
381 }
382 self.on_focus.as_ref().map(|on_focus| on_focus(is))
383 }
384}
385
386#[derive(Debug, Default)]
388pub(crate) struct State {
389 focused: bool,
390 unfocus: bool,
393 pub(crate) open: bool,
395}
396
397pub struct Anchored<'a, Message> {
400 anchor: Element<'a, Message>,
401 content: Option<Element<'a, Message>>,
402 behaviour: Behaviour<'a, Message>,
403}
404
405impl<Message> std::fmt::Debug for Anchored<'_, Message> {
406 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
407 let behaviour = &self.behaviour;
408 f.debug_struct("Anchored")
409 .field("open", &self.content.is_some())
410 .field("placement", &behaviour.placement)
411 .field("point", &behaviour.point)
412 .field("match_width", &behaviour.match_width)
413 .field("fit_height", &behaviour.fit_height)
414 .field("dismissable", &behaviour.on_dismiss.is_some())
415 .field("dismiss_keys", &behaviour.dismiss_keys)
416 .field("pass_through", &behaviour.pass_through)
417 .finish_non_exhaustive()
418 }
419}
420
421pub fn anchored<'a, Message>(anchor: impl Into<Element<'a, Message>>) -> Anchored<'a, Message> {
423 Anchored {
424 anchor: anchor.into(),
425 content: None,
426 behaviour: Behaviour::default(),
427 }
428}
429
430impl<'a, Message> Anchored<'a, Message> {
431 pub fn content(mut self, content: Option<Element<'a, Message>>) -> Self {
433 self.content = content;
434 self
435 }
436
437 pub fn placement(mut self, placement: Placement) -> Self {
438 self.behaviour.placement = placement;
439 self
440 }
441
442 pub fn at(mut self, point: Point) -> Self {
445 self.behaviour.point = Some(point);
446 self
447 }
448
449 pub fn match_width(mut self, match_width: bool) -> Self {
452 self.behaviour.match_width = match_width;
453 self
454 }
455
456 pub fn fit_height(mut self, fit: bool) -> Self {
461 self.behaviour.fit_height = fit;
462 self
463 }
464
465 pub fn on_dismiss(self, message: Message) -> Self {
467 self.on_dismiss_maybe(Some(message))
468 }
469
470 pub fn on_dismiss_maybe(mut self, message: Option<Message>) -> Self {
473 self.behaviour.on_dismiss = message;
474 self
475 }
476
477 pub fn dismiss_keys(mut self, chords: impl IntoIterator<Item = Chord>) -> Self {
482 self.behaviour.dismiss_keys = chords.into_iter().collect();
483 self
484 }
485
486 pub fn dismiss_on_anchor_press(mut self, dismiss: bool) -> Self {
490 self.behaviour.dismiss_on_anchor_press = dismiss;
491 self
492 }
493
494 pub fn dismiss_on_blur(mut self, dismiss: bool) -> Self {
497 self.behaviour.dismiss_on_blur = dismiss;
498 self
499 }
500
501 pub fn pass_through(mut self, pass_through: bool) -> Self {
504 self.behaviour.pass_through = pass_through;
505 self
506 }
507
508 pub fn on_key(mut self, on_key: impl Fn(&keys::Event) -> Option<Message> + 'a) -> Self {
512 self.behaviour.on_key = Some(Box::new(on_key));
513 self
514 }
515
516 pub fn on_focus(mut self, on_focus: impl Fn(bool) -> Message + 'a) -> Self {
519 self.behaviour.on_focus = Some(Box::new(on_focus));
520 self
521 }
522
523 pub fn on_anchor_press_maybe(mut self, message: Option<Message>) -> Self {
526 self.behaviour.on_anchor_press = message;
527 self
528 }
529}
530
531impl<'a, Message: Clone + 'a> From<Anchored<'a, Message>> for Element<'a, Message> {
532 fn from(anchored: Anchored<'a, Message>) -> Self {
533 Element::new(anchored)
534 }
535}
536
537impl<Message: Clone> Widget<Message, Theme, Renderer> for Anchored<'_, Message> {
538 fn tag(&self) -> tree::Tag {
539 tree::Tag::of::<State>()
540 }
541
542 fn state(&self) -> tree::State {
543 tree::State::new(State::default())
544 }
545
546 fn size(&self) -> Size<Length> {
547 self.anchor.as_widget().size()
548 }
549
550 fn size_hint(&self) -> Size<Length> {
551 self.anchor.as_widget().size_hint()
552 }
553
554 fn children(&self) -> Vec<Tree> {
555 std::iter::once(&self.anchor)
556 .chain(&self.content)
557 .map(Tree::new)
558 .collect()
559 }
560
561 fn diff(&self, tree: &mut Tree) {
562 match &self.content {
563 Some(content) => tree.diff_children(&[&self.anchor, content]),
564 None => tree.diff_children(&[&self.anchor]),
565 }
566 }
567
568 fn layout(
569 &mut self,
570 tree: &mut Tree,
571 renderer: &Renderer,
572 limits: &layout::Limits,
573 ) -> layout::Node {
574 self.anchor
575 .as_widget_mut()
576 .layout(&mut tree.children[0], renderer, limits)
577 }
578
579 fn draw(
580 &self,
581 tree: &Tree,
582 renderer: &mut Renderer,
583 theme: &Theme,
584 style: &renderer::Style,
585 layout: Layout<'_>,
586 cursor: mouse::Cursor,
587 viewport: &Rectangle,
588 ) {
589 self.anchor.as_widget().draw(
590 &tree.children[0],
591 renderer,
592 theme,
593 style,
594 layout,
595 cursor,
596 viewport,
597 );
598 }
599
600 fn operate(
601 &mut self,
602 tree: &mut Tree,
603 layout: Layout<'_>,
604 renderer: &Renderer,
605 operation: &mut dyn Operation,
606 ) {
607 self.anchor
608 .as_widget_mut()
609 .operate(&mut tree.children[0], layout, renderer, operation);
610 }
611
612 fn update(
613 &mut self,
614 tree: &mut Tree,
615 event: &Event,
616 layout: Layout<'_>,
617 cursor: mouse::Cursor,
618 renderer: &Renderer,
619 clipboard: &mut dyn Clipboard,
620 shell: &mut Shell<'_, Message>,
621 viewport: &Rectangle,
622 ) {
623 let Tree {
624 state, children, ..
625 } = tree;
626 let state = state.downcast_mut::<State>();
627 let Some(anchor_tree) = children.first_mut() else {
628 return;
629 };
630 let behaviour = &self.behaviour;
631
632 if state.unfocus {
633 state.unfocus = false;
634 if let Some(message) = behaviour.focus_message(state.focused, false) {
635 shell.publish(message);
636 }
637 state.focused = false;
638 self.anchor
639 .as_widget_mut()
640 .operate(anchor_tree, layout, renderer, &mut Unfocus);
641 shell.request_redraw();
642 }
643
644 if state.focused
645 && let Some(message) = key_message(behaviour, event)
646 {
647 shell.publish(message);
648 shell.capture_event();
649 return;
650 }
651
652 if is_press(event)
653 && cursor.is_over(layout.bounds())
654 && let Some(message) = &behaviour.on_anchor_press
655 {
656 shell.publish(message.clone());
657 }
658
659 self.anchor.as_widget_mut().update(
660 anchor_tree,
661 event,
662 layout,
663 cursor,
664 renderer,
665 clipboard,
666 shell,
667 viewport,
668 );
669
670 if !behaviour.watches_focus() {
671 return;
672 }
673 let mut focus = FindFocus::default();
674 self.anchor
675 .as_widget_mut()
676 .operate(anchor_tree, layout, renderer, &mut focus);
677 if state.focused
678 && !focus.0
679 && behaviour.dismiss_on_blur
680 && let Some(message) = &behaviour.on_dismiss
681 {
682 shell.publish(message.clone());
683 }
684 if let Some(message) = behaviour.focus_message(state.focused, focus.0) {
685 shell.publish(message);
686 }
687 state.focused = focus.0;
688 }
689
690 fn mouse_interaction(
691 &self,
692 tree: &Tree,
693 layout: Layout<'_>,
694 cursor: mouse::Cursor,
695 viewport: &Rectangle,
696 renderer: &Renderer,
697 ) -> mouse::Interaction {
698 self.anchor.as_widget().mouse_interaction(
699 &tree.children[0],
700 layout,
701 cursor,
702 viewport,
703 renderer,
704 )
705 }
706
707 fn overlay<'b>(
708 &'b mut self,
709 tree: &'b mut Tree,
710 layout: Layout<'b>,
711 renderer: &Renderer,
712 viewport: &Rectangle,
713 translation: Vector,
714 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
715 let Self {
716 anchor,
717 content,
718 behaviour,
719 } = self;
720 let Tree {
721 state, children, ..
722 } = tree;
723 let mut trees = children.iter_mut();
724 let anchor_tree = trees.next()?;
725 let trigger = layout.bounds() + translation;
726
727 let Some((content, tree)) = content.as_mut().zip(trees.next()) else {
730 return anchor.as_widget_mut().overlay(
731 anchor_tree,
732 layout,
733 renderer,
734 viewport,
735 translation,
736 );
737 };
738 Some(floating(
739 content,
740 tree,
741 state.downcast_mut::<State>(),
742 behaviour,
743 trigger,
744 ))
745 }
746}
747
748pub(crate) fn floating<'a, 'b, Message: Clone>(
751 content: &'b mut Element<'a, Message>,
752 tree: &'b mut Tree,
753 state: &'b mut State,
754 behaviour: &'b Behaviour<'a, Message>,
755 trigger: Rectangle,
756) -> overlay::Element<'b, Message, Theme, Renderer> {
757 let target = match behaviour.point {
758 Some(point) => Rectangle::new(
759 trigger.position() + Vector::new(point.x, point.y),
760 Size::ZERO,
761 ),
762 None => trigger,
763 };
764 overlay::Element::new(Box::new(Floating {
765 content,
766 tree,
767 state,
768 behaviour,
769 target,
770 trigger,
771 }))
772}
773
774struct Floating<'a, 'b, Message> {
777 content: &'b mut Element<'a, Message>,
778 tree: &'b mut Tree,
779 state: &'b mut State,
780 behaviour: &'b Behaviour<'a, Message>,
781 target: Rectangle,
782 trigger: Rectangle,
783}
784
785impl<Message: Clone> Floating<'_, '_, Message> {
786 fn dismiss(&mut self, shell: &mut Shell<'_, Message>) {
787 if self.behaviour.closes_itself {
788 self.state.open = false;
789 shell.request_redraw();
790 }
791 if self.behaviour.dismiss_on_blur {
792 self.state.unfocus = true;
793 }
794 if let Some(message) = &self.behaviour.on_dismiss {
795 shell.publish(message.clone());
796 }
797 }
798}
799
800fn key_message<Message>(behaviour: &Behaviour<'_, Message>, event: &Event) -> Option<Message> {
802 let Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) = event else {
803 return None;
804 };
805 let on_key = behaviour.on_key.as_ref()?;
806 on_key(&keys::Event {
807 key: key.clone(),
808 modifiers: *modifiers,
809 })
810}
811
812fn is_dismiss_key<Message>(behaviour: &Behaviour<'_, Message>, event: &Event) -> bool {
813 let Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) = event else {
814 return false;
815 };
816 behaviour
817 .dismiss_keys
818 .iter()
819 .any(|chord| chord.matches(key, *modifiers))
820}
821
822fn is_press(event: &Event) -> bool {
823 matches!(
824 event,
825 Event::Mouse(mouse::Event::ButtonPressed(_))
826 | Event::Touch(touch::Event::FingerPressed { .. })
827 )
828}
829
830fn is_release(event: &Event) -> bool {
831 matches!(
832 event,
833 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
834 | Event::Touch(touch::Event::FingerLifted { .. })
835 )
836}
837
838fn press_position(event: &Event, cursor: mouse::Cursor) -> Option<Point> {
840 match event {
841 Event::Mouse(mouse::Event::ButtonPressed(_)) => cursor.position(),
842 Event::Touch(touch::Event::FingerPressed { position, .. }) => Some(*position),
843 _ => None,
844 }
845}
846
847fn is_pointer_input(event: &Event) -> bool {
848 matches!(
849 event,
850 Event::Mouse(
851 mouse::Event::ButtonPressed(_)
852 | mouse::Event::ButtonReleased(_)
853 | mouse::Event::WheelScrolled { .. }
854 ) | Event::Touch(_)
855 )
856}
857
858impl<Message: Clone> overlay::Overlay<Message, Theme, Renderer> for Floating<'_, '_, Message> {
859 fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
860 let viewport = Rectangle::with_size(bounds);
861 let room = viewport.shrink(MARGIN).size();
862 let room = Size::new(room.width.max(0.0), room.height.max(0.0));
863 let match_width = self.behaviour.match_width;
864 let trigger_width = self.trigger.width;
865 let limits = |height: f32| {
866 if match_width {
867 let width = trigger_width.min(room.width);
868 layout::Limits::new(Size::new(width, 0.0), Size::new(width, height))
869 } else {
870 layout::Limits::new(Size::ZERO, Size::new(room.width, height))
871 }
872 };
873 let placement = self.behaviour.placement;
874 let mut node =
875 self.content
876 .as_widget_mut()
877 .layout(self.tree, renderer, &limits(room.height));
878 let fitted = self
879 .behaviour
880 .fit_height
881 .then(|| fitted_height(self.target, node.size(), viewport, placement))
882 .flatten();
883 if let Some(height) = fitted {
884 node = self
885 .content
886 .as_widget_mut()
887 .layout(self.tree, renderer, &limits(height));
888 }
889 let panel = place(self.target, node.size(), viewport, placement);
890 layout::Node::with_children(bounds, vec![node.move_to(panel.position())])
891 }
892
893 fn draw(
894 &self,
895 renderer: &mut Renderer,
896 theme: &Theme,
897 style: &renderer::Style,
898 layout: Layout<'_>,
899 cursor: mouse::Cursor,
900 ) {
901 let Some(panel) = layout.children().next() else {
902 return;
903 };
904 self.content.as_widget().draw(
905 self.tree,
906 renderer,
907 theme,
908 style,
909 panel,
910 cursor,
911 &layout.bounds(),
912 );
913 }
914
915 fn operate(&mut self, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation) {
916 let Some(panel) = layout.children().next() else {
917 return;
918 };
919 self.content
920 .as_widget_mut()
921 .operate(self.tree, panel, renderer, operation);
922 }
923
924 fn update(
925 &mut self,
926 event: &Event,
927 layout: Layout<'_>,
928 cursor: mouse::Cursor,
929 renderer: &Renderer,
930 clipboard: &mut dyn Clipboard,
931 shell: &mut Shell<'_, Message>,
932 ) {
933 let Some(panel) = layout.children().next() else {
934 return;
935 };
936
937 self.content.as_widget_mut().update(
938 self.tree,
939 event,
940 panel,
941 cursor,
942 renderer,
943 clipboard,
944 shell,
945 &layout.bounds(),
946 );
947 if shell.is_event_captured() {
948 if self.behaviour.closes_itself && is_release(event) {
950 self.state.open = false;
951 shell.request_redraw();
952 }
953 return;
954 }
955
956 if let Some(message) = key_message(self.behaviour, event) {
957 shell.publish(message);
958 shell.capture_event();
959 return;
960 }
961 if is_dismiss_key(self.behaviour, event) && self.behaviour.can_dismiss() {
962 self.dismiss(shell);
963 shell.capture_event();
964 return;
965 }
966 if !is_pointer_input(event) {
967 return;
968 }
969
970 let bounds = panel.bounds();
971 if cursor.is_over(bounds) {
972 shell.capture_event();
973 return;
974 }
975 let Some(position) = press_position(event, cursor) else {
976 return;
977 };
978 if bounds.contains(position) {
979 shell.capture_event();
980 return;
981 }
982 let on_anchor = self.trigger.contains(position);
983 if (on_anchor && !self.behaviour.dismiss_on_anchor_press) || !self.behaviour.can_dismiss() {
984 return;
985 }
986 self.dismiss(shell);
987 if !on_anchor && !self.behaviour.pass_through {
988 shell.capture_event();
989 }
990 }
991
992 fn mouse_interaction(
993 &self,
994 layout: Layout<'_>,
995 cursor: mouse::Cursor,
996 renderer: &Renderer,
997 ) -> mouse::Interaction {
998 let Some(panel) = layout.children().next() else {
999 return mouse::Interaction::None;
1000 };
1001 if !cursor.is_over(panel.bounds()) {
1002 return mouse::Interaction::None;
1003 }
1004 self.content
1005 .as_widget()
1006 .mouse_interaction(self.tree, panel, cursor, &layout.bounds(), renderer)
1007 .max(mouse::Interaction::Idle)
1008 }
1009
1010 fn overlay<'c>(
1011 &'c mut self,
1012 layout: Layout<'c>,
1013 renderer: &Renderer,
1014 ) -> Option<overlay::Element<'c, Message, Theme, Renderer>> {
1015 let panel = layout.children().next()?;
1016 let viewport = layout.bounds();
1017 self.content
1018 .as_widget_mut()
1019 .overlay(self.tree, panel, renderer, &viewport, Vector::ZERO)
1020 }
1021}
1022
1023#[derive(Default)]
1025struct FindFocus(bool);
1026
1027impl Operation for FindFocus {
1028 fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) {
1029 operate(self);
1030 }
1031
1032 fn focusable(&mut self, _id: Option<&Id>, _bounds: Rectangle, state: &mut dyn Focusable) {
1033 self.0 |= state.is_focused();
1034 }
1035}
1036
1037struct Unfocus;
1039
1040impl Operation for Unfocus {
1041 fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) {
1042 operate(self);
1043 }
1044
1045 fn focusable(&mut self, _id: Option<&Id>, _bounds: Rectangle, state: &mut dyn Focusable) {
1046 if state.is_focused() {
1047 state.unfocus();
1048 }
1049 }
1050}
1051
1052#[cfg(test)]
1053mod tests {
1054 use super::*;
1055 use crate::theme::{dark, light};
1056 use iced::keyboard::{Key, Modifiers};
1057
1058 const VIEWPORT: Rectangle = Rectangle {
1059 x: 0.0,
1060 y: 0.0,
1061 width: 400.0,
1062 height: 300.0,
1063 };
1064
1065 fn anchor() -> Rectangle {
1066 Rectangle::new(Point::new(150.0, 100.0), Size::new(100.0, 40.0))
1067 }
1068
1069 const PANEL: Size = Size::new(60.0, 50.0);
1070
1071 fn at(side: Side, align: Align) -> Point {
1072 place(anchor(), PANEL, VIEWPORT, Placement::new(side, align)).position()
1073 }
1074
1075 #[test]
1076 fn defaults_to_bottom_centre_with_a_small_gap() {
1077 let placement = Placement::default();
1078 assert_eq!(placement.side, Side::Bottom);
1079 assert_eq!(placement.align, Align::Center);
1080 assert_eq!(placement.gap, GAP);
1081 assert_eq!(placement.offset, 0.0);
1082 }
1083
1084 #[test]
1085 fn each_side_sits_outside_the_anchor_with_the_gap() {
1086 assert_eq!(at(Side::Bottom, Align::Start), Point::new(150.0, 144.0));
1087 assert_eq!(at(Side::Top, Align::Start), Point::new(150.0, 46.0));
1088 assert_eq!(at(Side::Left, Align::Start), Point::new(86.0, 100.0));
1089 assert_eq!(at(Side::Right, Align::Start), Point::new(254.0, 100.0));
1090 }
1091
1092 #[test]
1093 fn alignment_moves_along_the_side() {
1094 assert_eq!(at(Side::Bottom, Align::Center).x, 170.0);
1095 assert_eq!(at(Side::Bottom, Align::End).x, 190.0);
1096 assert_eq!(at(Side::Right, Align::Center).y, 95.0);
1097 assert_eq!(at(Side::Right, Align::End).y, 90.0);
1098 }
1099
1100 #[test]
1101 fn offset_shifts_away_from_the_aligned_edge() {
1102 let placement = Placement::new(Side::Bottom, Align::Start).offset(5.0);
1103 let start = place(anchor(), PANEL, VIEWPORT, placement);
1104 assert_eq!(start.x, 155.0);
1105 let placement = Placement::new(Side::Bottom, Align::End).offset(5.0);
1106 let end = place(anchor(), PANEL, VIEWPORT, placement);
1107 assert_eq!(end.x, 185.0);
1108 }
1109
1110 #[test]
1111 fn flips_to_the_opposite_side_when_there_is_no_room() {
1112 let low = Rectangle::new(Point::new(150.0, 230.0), Size::new(100.0, 40.0));
1113 let panel = place(
1114 low,
1115 PANEL,
1116 VIEWPORT,
1117 Placement::new(Side::Bottom, Align::Start),
1118 );
1119 assert_eq!(panel.y, 230.0 - GAP - PANEL.height);
1120
1121 let left_edge = Rectangle::new(Point::new(20.0, 100.0), Size::new(40.0, 40.0));
1122 let panel = place(
1123 left_edge,
1124 PANEL,
1125 VIEWPORT,
1126 Placement::new(Side::Left, Align::Start),
1127 );
1128 assert_eq!(panel.x, 60.0 + GAP);
1129 }
1130
1131 #[test]
1132 fn stays_put_when_neither_side_fits_and_the_requested_one_is_larger() {
1133 let tall = Size::new(60.0, 290.0);
1134 let panel = place(
1135 anchor(),
1136 tall,
1137 VIEWPORT,
1138 Placement::new(Side::Bottom, Align::Start),
1139 );
1140 assert_eq!(panel.y, MARGIN, "shifted up to fit, pinned at the margin");
1141 }
1142
1143 #[test]
1144 fn fitted_height_keeps_a_tall_panel_off_its_anchor() {
1145 let below = Placement::new(Side::Bottom, Align::Start);
1146 assert_eq!(fitted_height(anchor(), PANEL, VIEWPORT, below), None);
1147 let tall = Size::new(60.0, 290.0);
1148 let room_below = VIEWPORT.height - MARGIN - (140.0 + GAP);
1149 assert_eq!(
1150 fitted_height(anchor(), tall, VIEWPORT, below),
1151 Some(room_below)
1152 );
1153 let panel = place(anchor(), Size::new(60.0, room_below), VIEWPORT, below);
1154 assert_eq!(panel.y, 140.0 + GAP, "stays below the anchor");
1155
1156 let low = Rectangle::new(Point::new(150.0, 200.0), Size::new(100.0, 40.0));
1157 let room_above = 200.0 - GAP - MARGIN;
1158 assert_eq!(fitted_height(low, tall, VIEWPORT, below), Some(room_above));
1159 let panel = place(low, Size::new(60.0, room_above), VIEWPORT, below);
1160 assert_eq!(panel.y + panel.height, 200.0 - GAP, "flips above it");
1161
1162 let beside = Placement::new(Side::Right, Align::Start);
1163 assert_eq!(fitted_height(anchor(), tall, VIEWPORT, beside), None);
1164 }
1165
1166 #[test]
1167 fn fitted_height_uses_the_roomier_side_however_little_room_it_has() {
1168 let below = Placement::new(Side::Bottom, Align::Start);
1169 let tall = Size::new(60.0, 290.0);
1170 let squeezed = Rectangle::new(Point::new(150.0, 100.0), Size::new(100.0, 120.0));
1171 let room_above = 100.0 - GAP - MARGIN;
1172 assert_eq!(
1173 fitted_height(squeezed, tall, VIEWPORT, below),
1174 Some(room_above)
1175 );
1176 let panel = place(squeezed, Size::new(60.0, room_above), VIEWPORT, below);
1177 assert_eq!(panel.y + panel.height, 100.0 - GAP, "clear of the anchor");
1178
1179 let filling = Rectangle::new(Point::new(0.0, 0.0), Size::new(400.0, 300.0));
1180 assert_eq!(fitted_height(filling, tall, VIEWPORT, below), Some(0.0));
1181 }
1182
1183 #[test]
1184 fn a_menu_low_in_the_window_opens_above_at_full_height() {
1185 let point = Rectangle::new(Point::new(100.0, 230.0), Size::ZERO);
1186 let menu = Size::new(180.0, 200.0);
1187 let placement = Placement::new(Side::Bottom, Align::Start).gap(0.0);
1188 assert_eq!(fitted_height(point, menu, VIEWPORT, placement), None);
1189 let panel = place(point, menu, VIEWPORT, placement);
1190 assert_eq!(panel.y + panel.height, 230.0, "flipped above the point");
1191 }
1192
1193 #[test]
1194 fn a_side_panel_that_fits_neither_side_moves_below_its_anchor() {
1195 let narrow = Rectangle::new(Point::new(0.0, 0.0), Size::new(320.0, 300.0));
1196 let row = Rectangle::new(Point::new(40.0, 100.0), Size::new(216.0, 32.0));
1197 let submenu = Size::new(224.0, 120.0);
1198 let placement = Placement::new(Side::Right, Align::Start).gap(4.0);
1199 let panel = place(row, submenu, narrow, placement);
1200 assert_eq!(panel.y, 132.0 + 4.0, "below the row, clear of its label");
1201 assert_eq!(panel.x, 40.0, "lined up with the row");
1202
1203 let low_row = Rectangle::new(Point::new(40.0, 200.0), Size::new(216.0, 32.0));
1204 let panel = place(low_row, submenu, narrow, placement);
1205 assert_eq!(panel.y + panel.height, 200.0 - 4.0, "above the row");
1206
1207 let wide = Rectangle::new(Point::new(0.0, 0.0), Size::new(600.0, 300.0));
1208 let panel = place(row, submenu, wide, placement);
1209 assert_eq!(panel.x, 256.0 + 4.0, "beside the row when it fits");
1210 assert_eq!(fitted_height(row, submenu, wide, placement), None);
1211 }
1212
1213 #[test]
1214 fn a_tall_side_panel_without_room_beside_scrolls_on_the_roomier_side() {
1215 let narrow = Rectangle::new(Point::new(0.0, 0.0), Size::new(320.0, 300.0));
1216 let row = Rectangle::new(Point::new(40.0, 160.0), Size::new(216.0, 32.0));
1217 let submenu = Size::new(224.0, 180.0);
1218 let placement = Placement::new(Side::Right, Align::Start).gap(4.0);
1219 assert_eq!(
1220 fitted_height(row, submenu, narrow, placement),
1221 Some(160.0 - 4.0 - MARGIN)
1222 );
1223 let fitted = Size::new(224.0, 160.0 - 4.0 - MARGIN);
1224 let panel = place(row, fitted, narrow, placement);
1225 assert_eq!(panel.y, MARGIN);
1226 assert_eq!(panel.y + panel.height, 160.0 - 4.0, "clear of the row");
1227 }
1228
1229 #[test]
1230 fn mirrors_the_alignment_when_it_would_overflow() {
1231 let right_edge = Rectangle::new(Point::new(360.0, 100.0), Size::new(30.0, 30.0));
1232 let panel = place(
1233 right_edge,
1234 PANEL,
1235 VIEWPORT,
1236 Placement::new(Side::Bottom, Align::Start),
1237 );
1238 assert_eq!(panel.x, 390.0 - PANEL.width);
1239 }
1240
1241 #[test]
1242 fn shifts_inside_the_margin_when_nothing_else_fits() {
1243 let corner = Rectangle::new(Point::new(0.0, 0.0), Size::new(10.0, 10.0));
1244 let panel = place(
1245 corner,
1246 PANEL,
1247 VIEWPORT,
1248 Placement::new(Side::Left, Align::Center),
1249 );
1250 assert_eq!(panel.x, 10.0 + GAP, "flipped to the right");
1251 assert_eq!(panel.y, MARGIN, "shifted down to the margin");
1252 }
1253
1254 #[test]
1255 fn a_list_near_the_bottom_keeps_the_margin() {
1256 let field = Rectangle::new(Point::new(20.0, 200.0), Size::new(200.0, 36.0));
1257 let list = Size::new(200.0, 80.0);
1258 let placement = Placement::new(Side::Bottom, Align::Start);
1259 let panel = place(field, list, VIEWPORT, placement);
1260 assert_eq!(panel.y, 200.0 - GAP - list.height, "flips above the field");
1261
1262 let wide = Size::new(420.0, 40.0);
1263 let panel = place(field, wide, VIEWPORT, placement);
1264 assert_eq!(panel.x, MARGIN, "clamped inside the window");
1265 }
1266
1267 #[test]
1268 fn a_point_anchor_opens_below_and_right_then_flips_near_edges() {
1269 let point = |x, y| Rectangle::new(Point::new(x, y), Size::ZERO);
1270 let placement = Placement::new(Side::Bottom, Align::Start).gap(0.0);
1271 assert_eq!(
1272 place(point(100.0, 100.0), PANEL, VIEWPORT, placement).position(),
1273 Point::new(100.0, 100.0)
1274 );
1275 assert_eq!(
1276 place(point(380.0, 280.0), PANEL, VIEWPORT, placement).position(),
1277 Point::new(320.0, 230.0)
1278 );
1279 }
1280
1281 #[test]
1282 fn a_panel_larger_than_the_window_is_pinned_top_left() {
1283 let huge = Size::new(1000.0, 1000.0);
1284 let panel = place(anchor(), huge, VIEWPORT, Placement::default());
1285 assert_eq!(panel.position(), Point::new(MARGIN, MARGIN));
1286 assert_eq!(panel.size(), huge);
1287 }
1288
1289 #[test]
1290 fn sides_and_alignments_mirror() {
1291 for side in Side::ALL {
1292 assert_eq!(side.opposite().opposite(), side);
1293 assert_ne!(side.opposite(), side);
1294 }
1295 assert_eq!(Align::Center.mirrored(), Align::Center);
1296 assert_eq!(Align::Start.mirrored(), Align::End);
1297 }
1298
1299 #[test]
1300 fn surface_uses_the_popover_colour_a_border_and_a_shadow() {
1301 for theme in [light(), dark()] {
1302 let tokens = Tokens::of(&theme);
1303 let style = surface_style(&tokens);
1304 assert_eq!(style.background, Some(Background::Color(tokens.popover)));
1305 assert_eq!(style.text_color, Some(tokens.foreground));
1306 assert_eq!(style.border.color, tokens.border);
1307 assert_eq!(style.border.width, 1.0);
1308 assert_eq!(style.border.radius, radius::MD.into());
1309 assert_eq!(style.shadow, shadow(&tokens));
1310 assert_eq!(style.shadow.offset, Vector::new(0.0, 4.0));
1311 assert_eq!(style.shadow.blur_radius, 12.0);
1312 }
1313 let light = shadow(&Tokens::of(&light())).color.a;
1314 let dark = shadow(&Tokens::of(&dark())).color.a;
1315 assert!((light - 0.1).abs() < 1e-6);
1316 assert!((dark - 0.5).abs() < 1e-6);
1317 }
1318
1319 #[test]
1320 fn builder_starts_closed_and_undismissable_with_escape_bound() {
1321 let anchored: Anchored<'_, ()> = anchored(iced::widget::text("Anchor"));
1322 let behaviour = &anchored.behaviour;
1323 assert!(anchored.content.is_none());
1324 assert!(behaviour.on_dismiss.is_none());
1325 assert!(behaviour.point.is_none());
1326 assert!(!behaviour.dismiss_on_anchor_press);
1327 assert!(!behaviour.dismiss_on_blur);
1328 assert!(!behaviour.pass_through);
1329 assert!(!behaviour.match_width);
1330 assert_eq!(behaviour.dismiss_keys, vec![Chord::named(Named::Escape)]);
1331 assert!(!behaviour.can_dismiss());
1332 assert!(!behaviour.watches_focus());
1333 }
1334
1335 #[test]
1336 fn builder_sets_every_option() {
1337 let anchored: Anchored<'_, u8> = anchored(iced::widget::text("Anchor"))
1338 .at(Point::new(3.0, 4.0))
1339 .match_width(true)
1340 .on_dismiss(1)
1341 .dismiss_keys([Chord::character('q')])
1342 .dismiss_on_anchor_press(true)
1343 .dismiss_on_blur(true)
1344 .pass_through(true)
1345 .on_anchor_press_maybe(Some(2))
1346 .on_key(|_| Some(3))
1347 .on_focus(|focused| if focused { 4 } else { 5 });
1348 let behaviour = &anchored.behaviour;
1349 assert_eq!(behaviour.focus_message(false, true), Some(4));
1350 assert_eq!(behaviour.focus_message(true, false), Some(5));
1351 assert_eq!(behaviour.focus_message(true, true), None);
1352 assert_eq!(behaviour.point, Some(Point::new(3.0, 4.0)));
1353 assert!(behaviour.match_width && behaviour.pass_through);
1354 assert!(behaviour.dismiss_on_anchor_press && behaviour.dismiss_on_blur);
1355 assert_eq!(behaviour.on_dismiss, Some(1));
1356 assert_eq!(behaviour.on_anchor_press, Some(2));
1357 assert_eq!(behaviour.dismiss_keys, vec![Chord::character('q')]);
1358 assert!(behaviour.can_dismiss() && behaviour.watches_focus());
1359 }
1360
1361 fn key_press(key: Key, modifiers: Modifiers) -> Event {
1362 Event::Keyboard(keyboard::Event::KeyPressed {
1363 key: key.clone(),
1364 modified_key: key,
1365 physical_key: keyboard::key::Physical::Unidentified(
1366 keyboard::key::NativeCode::Unidentified,
1367 ),
1368 location: keyboard::Location::Standard,
1369 modifiers,
1370 text: None,
1371 repeat: false,
1372 })
1373 }
1374
1375 #[test]
1376 fn keys_resolve_through_on_key_then_the_dismiss_chords() {
1377 let behaviour: Behaviour<'_, u8> = Behaviour {
1378 on_key: Some(Box::new(|key: &keys::Event| {
1379 (key.key == Key::Named(Named::ArrowDown)).then_some(7)
1380 })),
1381 ..Behaviour::default()
1382 };
1383 let down = key_press(Key::Named(Named::ArrowDown), Modifiers::empty());
1384 let escape = key_press(Key::Named(Named::Escape), Modifiers::empty());
1385 let shift_escape = key_press(Key::Named(Named::Escape), Modifiers::SHIFT);
1386 assert_eq!(key_message(&behaviour, &down), Some(7));
1387 assert_eq!(key_message(&behaviour, &escape), None);
1388 assert!(is_dismiss_key(&behaviour, &escape));
1389 assert!(!is_dismiss_key(&behaviour, &shift_escape));
1390 assert!(!is_dismiss_key(&behaviour, &down));
1391
1392 let unbound: Behaviour<'_, u8> = Behaviour {
1393 dismiss_keys: Vec::new(),
1394 ..Behaviour::default()
1395 };
1396 assert!(!is_dismiss_key(&unbound, &escape));
1397 }
1398
1399 #[test]
1400 fn press_position_only_reports_presses() {
1401 let cursor = mouse::Cursor::Available(Point::new(3.0, 4.0));
1402 let pressed = Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right));
1403 let released = Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left));
1404 assert_eq!(press_position(&pressed, cursor), Some(Point::new(3.0, 4.0)));
1405 assert_eq!(press_position(&released, cursor), None);
1406 assert_eq!(press_position(&pressed, mouse::Cursor::Unavailable), None);
1407 assert!(is_pointer_input(&released));
1408 assert!(is_release(&released));
1409 assert!(is_press(&pressed));
1410 assert!(!is_release(&pressed));
1411 assert!(!is_pointer_input(&Event::Mouse(mouse::Event::CursorLeft)));
1412 }
1413}