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(
148 anchor: Rectangle,
149 size: Size,
150 viewport: Rectangle,
151 placement: Placement,
152) -> Rectangle {
153 let area = viewport.shrink(MARGIN);
154 let room = |side: Side| match side {
155 Side::Top => anchor.y - placement.gap - area.y,
156 Side::Bottom => area.y + area.height - (anchor.y + anchor.height + placement.gap),
157 Side::Left => anchor.x - placement.gap - area.x,
158 Side::Right => area.x + area.width - (anchor.x + anchor.width + placement.gap),
159 };
160 let needed = |side: Side| {
161 if side.is_vertical() {
162 size.height
163 } else {
164 size.width
165 }
166 };
167
168 let requested = placement.side;
169 let opposite = requested.opposite();
170 let side = if room(requested) >= needed(requested) {
171 requested
172 } else if room(opposite) >= needed(opposite) || room(opposite) > room(requested) {
173 opposite
174 } else {
175 requested
176 };
177
178 let main = match side {
179 Side::Top => anchor.y - placement.gap - size.height,
180 Side::Bottom => anchor.y + anchor.height + placement.gap,
181 Side::Left => anchor.x - placement.gap - size.width,
182 Side::Right => anchor.x + anchor.width + placement.gap,
183 };
184
185 let (start, length, extent, low, high) = if side.is_vertical() {
186 (
187 anchor.x,
188 anchor.width,
189 size.width,
190 area.x,
191 area.x + area.width,
192 )
193 } else {
194 (
195 anchor.y,
196 anchor.height,
197 size.height,
198 area.y,
199 area.y + area.height,
200 )
201 };
202 let cross_at = |align: Align| match align {
203 Align::Start => start + placement.offset,
204 Align::Center => start + (length - extent) / 2.0 + placement.offset,
205 Align::End => start + length - extent - placement.offset,
206 };
207 let fits = |value: f32| value >= low && value + extent <= high;
208 let preferred = cross_at(placement.align);
209 let mirrored = cross_at(placement.align.mirrored());
210 let cross = if !fits(preferred) && fits(mirrored) {
211 mirrored
212 } else {
213 preferred
214 };
215
216 let (x, y) = if side.is_vertical() {
217 (cross, main)
218 } else {
219 (main, cross)
220 };
221 Rectangle::new(
222 Point::new(
223 clamp(x, area.x, area.x + area.width - size.width),
224 clamp(y, area.y, area.y + area.height - size.height),
225 ),
226 size,
227 )
228}
229
230fn clamp(value: f32, low: f32, high: f32) -> f32 {
232 value.min(high).max(low)
233}
234
235pub fn surface_style(tokens: &Tokens) -> container::Style {
238 container::Style {
239 background: Some(Background::Color(tokens.popover)),
240 text_color: Some(tokens.foreground),
241 border: Border {
242 color: tokens.border,
243 width: 1.0,
244 radius: radius::MD.into(),
245 },
246 shadow: shadow(tokens),
247 snap: true,
248 }
249}
250
251pub fn shadow(tokens: &Tokens) -> Shadow {
253 Shadow {
254 color: fade(Color::BLACK, if tokens.is_dark { 0.5 } else { 0.1 }),
255 offset: Vector::new(0.0, 4.0),
256 blur_radius: 12.0,
257 }
258}
259
260type OnKey<'a, Message> = Box<dyn Fn(&keys::Event) -> Option<Message> + 'a>;
261
262pub(crate) struct Behaviour<'a, Message> {
265 pub(crate) placement: Placement,
266 pub(crate) point: Option<Point>,
267 pub(crate) match_width: bool,
268 pub(crate) on_dismiss: Option<Message>,
269 pub(crate) dismiss_keys: Vec<Chord>,
270 pub(crate) dismiss_on_anchor_press: bool,
271 pub(crate) dismiss_on_blur: bool,
272 pub(crate) pass_through: bool,
273 pub(crate) closes_itself: bool,
276 pub(crate) on_key: Option<OnKey<'a, Message>>,
277 pub(crate) on_anchor_press: Option<Message>,
278 pub(crate) on_focus: Option<OnFocus<'a, Message>>,
279}
280
281type OnFocus<'a, Message> = Box<dyn Fn(bool) -> Message + 'a>;
282
283impl<Message> Default for Behaviour<'_, Message> {
284 fn default() -> Self {
285 Self {
286 placement: Placement::default(),
287 point: None,
288 match_width: false,
289 on_dismiss: None,
290 dismiss_keys: vec![Chord::named(Named::Escape)],
291 dismiss_on_anchor_press: false,
292 dismiss_on_blur: false,
293 pass_through: false,
294 closes_itself: false,
295 on_key: None,
296 on_anchor_press: None,
297 on_focus: None,
298 }
299 }
300}
301
302impl<Message> Behaviour<'_, Message> {
303 fn can_dismiss(&self) -> bool {
304 self.on_dismiss.is_some() || self.closes_itself
305 }
306
307 fn watches_focus(&self) -> bool {
308 self.on_key.is_some() || self.dismiss_on_blur || self.on_focus.is_some()
309 }
310
311 fn focus_message(&self, was: bool, is: bool) -> Option<Message> {
314 if was == is {
315 return None;
316 }
317 self.on_focus.as_ref().map(|on_focus| on_focus(is))
318 }
319}
320
321#[derive(Debug, Default)]
323pub(crate) struct State {
324 focused: bool,
325 unfocus: bool,
328 pub(crate) open: bool,
330}
331
332pub struct Anchored<'a, Message> {
335 anchor: Element<'a, Message>,
336 content: Option<Element<'a, Message>>,
337 behaviour: Behaviour<'a, Message>,
338}
339
340impl<Message> std::fmt::Debug for Anchored<'_, Message> {
341 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
342 let behaviour = &self.behaviour;
343 f.debug_struct("Anchored")
344 .field("open", &self.content.is_some())
345 .field("placement", &behaviour.placement)
346 .field("point", &behaviour.point)
347 .field("match_width", &behaviour.match_width)
348 .field("dismissable", &behaviour.on_dismiss.is_some())
349 .field("dismiss_keys", &behaviour.dismiss_keys)
350 .field("pass_through", &behaviour.pass_through)
351 .finish_non_exhaustive()
352 }
353}
354
355pub fn anchored<'a, Message>(anchor: impl Into<Element<'a, Message>>) -> Anchored<'a, Message> {
357 Anchored {
358 anchor: anchor.into(),
359 content: None,
360 behaviour: Behaviour::default(),
361 }
362}
363
364impl<'a, Message> Anchored<'a, Message> {
365 pub fn content(mut self, content: Option<Element<'a, Message>>) -> Self {
367 self.content = content;
368 self
369 }
370
371 pub fn placement(mut self, placement: Placement) -> Self {
372 self.behaviour.placement = placement;
373 self
374 }
375
376 pub fn at(mut self, point: Point) -> Self {
379 self.behaviour.point = Some(point);
380 self
381 }
382
383 pub fn match_width(mut self, match_width: bool) -> Self {
386 self.behaviour.match_width = match_width;
387 self
388 }
389
390 pub fn on_dismiss(self, message: Message) -> Self {
392 self.on_dismiss_maybe(Some(message))
393 }
394
395 pub fn on_dismiss_maybe(mut self, message: Option<Message>) -> Self {
398 self.behaviour.on_dismiss = message;
399 self
400 }
401
402 pub fn dismiss_keys(mut self, chords: impl IntoIterator<Item = Chord>) -> Self {
407 self.behaviour.dismiss_keys = chords.into_iter().collect();
408 self
409 }
410
411 pub fn dismiss_on_anchor_press(mut self, dismiss: bool) -> Self {
415 self.behaviour.dismiss_on_anchor_press = dismiss;
416 self
417 }
418
419 pub fn dismiss_on_blur(mut self, dismiss: bool) -> Self {
422 self.behaviour.dismiss_on_blur = dismiss;
423 self
424 }
425
426 pub fn pass_through(mut self, pass_through: bool) -> Self {
429 self.behaviour.pass_through = pass_through;
430 self
431 }
432
433 pub fn on_key(mut self, on_key: impl Fn(&keys::Event) -> Option<Message> + 'a) -> Self {
437 self.behaviour.on_key = Some(Box::new(on_key));
438 self
439 }
440
441 pub fn on_focus(mut self, on_focus: impl Fn(bool) -> Message + 'a) -> Self {
444 self.behaviour.on_focus = Some(Box::new(on_focus));
445 self
446 }
447
448 pub fn on_anchor_press_maybe(mut self, message: Option<Message>) -> Self {
451 self.behaviour.on_anchor_press = message;
452 self
453 }
454}
455
456impl<'a, Message: Clone + 'a> From<Anchored<'a, Message>> for Element<'a, Message> {
457 fn from(anchored: Anchored<'a, Message>) -> Self {
458 Element::new(anchored)
459 }
460}
461
462impl<Message: Clone> Widget<Message, Theme, Renderer> for Anchored<'_, Message> {
463 fn tag(&self) -> tree::Tag {
464 tree::Tag::of::<State>()
465 }
466
467 fn state(&self) -> tree::State {
468 tree::State::new(State::default())
469 }
470
471 fn size(&self) -> Size<Length> {
472 self.anchor.as_widget().size()
473 }
474
475 fn size_hint(&self) -> Size<Length> {
476 self.anchor.as_widget().size_hint()
477 }
478
479 fn children(&self) -> Vec<Tree> {
480 std::iter::once(&self.anchor)
481 .chain(&self.content)
482 .map(Tree::new)
483 .collect()
484 }
485
486 fn diff(&self, tree: &mut Tree) {
487 match &self.content {
488 Some(content) => tree.diff_children(&[&self.anchor, content]),
489 None => tree.diff_children(&[&self.anchor]),
490 }
491 }
492
493 fn layout(
494 &mut self,
495 tree: &mut Tree,
496 renderer: &Renderer,
497 limits: &layout::Limits,
498 ) -> layout::Node {
499 self.anchor
500 .as_widget_mut()
501 .layout(&mut tree.children[0], renderer, limits)
502 }
503
504 fn draw(
505 &self,
506 tree: &Tree,
507 renderer: &mut Renderer,
508 theme: &Theme,
509 style: &renderer::Style,
510 layout: Layout<'_>,
511 cursor: mouse::Cursor,
512 viewport: &Rectangle,
513 ) {
514 self.anchor.as_widget().draw(
515 &tree.children[0],
516 renderer,
517 theme,
518 style,
519 layout,
520 cursor,
521 viewport,
522 );
523 }
524
525 fn operate(
526 &mut self,
527 tree: &mut Tree,
528 layout: Layout<'_>,
529 renderer: &Renderer,
530 operation: &mut dyn Operation,
531 ) {
532 self.anchor
533 .as_widget_mut()
534 .operate(&mut tree.children[0], layout, renderer, operation);
535 }
536
537 fn update(
538 &mut self,
539 tree: &mut Tree,
540 event: &Event,
541 layout: Layout<'_>,
542 cursor: mouse::Cursor,
543 renderer: &Renderer,
544 clipboard: &mut dyn Clipboard,
545 shell: &mut Shell<'_, Message>,
546 viewport: &Rectangle,
547 ) {
548 let Tree {
549 state, children, ..
550 } = tree;
551 let state = state.downcast_mut::<State>();
552 let Some(anchor_tree) = children.first_mut() else {
553 return;
554 };
555 let behaviour = &self.behaviour;
556
557 if state.unfocus {
558 state.unfocus = false;
559 if let Some(message) = behaviour.focus_message(state.focused, false) {
560 shell.publish(message);
561 }
562 state.focused = false;
563 self.anchor
564 .as_widget_mut()
565 .operate(anchor_tree, layout, renderer, &mut Unfocus);
566 shell.request_redraw();
567 }
568
569 if state.focused
570 && let Some(message) = key_message(behaviour, event)
571 {
572 shell.publish(message);
573 shell.capture_event();
574 return;
575 }
576
577 if is_press(event)
578 && cursor.is_over(layout.bounds())
579 && let Some(message) = &behaviour.on_anchor_press
580 {
581 shell.publish(message.clone());
582 }
583
584 self.anchor.as_widget_mut().update(
585 anchor_tree,
586 event,
587 layout,
588 cursor,
589 renderer,
590 clipboard,
591 shell,
592 viewport,
593 );
594
595 if !behaviour.watches_focus() {
596 return;
597 }
598 let mut focus = FindFocus::default();
599 self.anchor
600 .as_widget_mut()
601 .operate(anchor_tree, layout, renderer, &mut focus);
602 if state.focused
603 && !focus.0
604 && behaviour.dismiss_on_blur
605 && let Some(message) = &behaviour.on_dismiss
606 {
607 shell.publish(message.clone());
608 }
609 if let Some(message) = behaviour.focus_message(state.focused, focus.0) {
610 shell.publish(message);
611 }
612 state.focused = focus.0;
613 }
614
615 fn mouse_interaction(
616 &self,
617 tree: &Tree,
618 layout: Layout<'_>,
619 cursor: mouse::Cursor,
620 viewport: &Rectangle,
621 renderer: &Renderer,
622 ) -> mouse::Interaction {
623 self.anchor.as_widget().mouse_interaction(
624 &tree.children[0],
625 layout,
626 cursor,
627 viewport,
628 renderer,
629 )
630 }
631
632 fn overlay<'b>(
633 &'b mut self,
634 tree: &'b mut Tree,
635 layout: Layout<'b>,
636 renderer: &Renderer,
637 viewport: &Rectangle,
638 translation: Vector,
639 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
640 let Self {
641 anchor,
642 content,
643 behaviour,
644 } = self;
645 let Tree {
646 state, children, ..
647 } = tree;
648 let mut trees = children.iter_mut();
649 let anchor_tree = trees.next()?;
650 let trigger = layout.bounds() + translation;
651
652 let Some((content, tree)) = content.as_mut().zip(trees.next()) else {
655 return anchor.as_widget_mut().overlay(
656 anchor_tree,
657 layout,
658 renderer,
659 viewport,
660 translation,
661 );
662 };
663 Some(floating(
664 content,
665 tree,
666 state.downcast_mut::<State>(),
667 behaviour,
668 trigger,
669 ))
670 }
671}
672
673pub(crate) fn floating<'a, 'b, Message: Clone>(
676 content: &'b mut Element<'a, Message>,
677 tree: &'b mut Tree,
678 state: &'b mut State,
679 behaviour: &'b Behaviour<'a, Message>,
680 trigger: Rectangle,
681) -> overlay::Element<'b, Message, Theme, Renderer> {
682 let target = match behaviour.point {
683 Some(point) => Rectangle::new(
684 trigger.position() + Vector::new(point.x, point.y),
685 Size::ZERO,
686 ),
687 None => trigger,
688 };
689 overlay::Element::new(Box::new(Floating {
690 content,
691 tree,
692 state,
693 behaviour,
694 target,
695 trigger,
696 }))
697}
698
699struct Floating<'a, 'b, Message> {
702 content: &'b mut Element<'a, Message>,
703 tree: &'b mut Tree,
704 state: &'b mut State,
705 behaviour: &'b Behaviour<'a, Message>,
706 target: Rectangle,
707 trigger: Rectangle,
708}
709
710impl<Message: Clone> Floating<'_, '_, Message> {
711 fn dismiss(&mut self, shell: &mut Shell<'_, Message>) {
712 if self.behaviour.closes_itself {
713 self.state.open = false;
714 shell.request_redraw();
715 }
716 if self.behaviour.dismiss_on_blur {
717 self.state.unfocus = true;
718 }
719 if let Some(message) = &self.behaviour.on_dismiss {
720 shell.publish(message.clone());
721 }
722 }
723}
724
725fn key_message<Message>(behaviour: &Behaviour<'_, Message>, event: &Event) -> Option<Message> {
727 let Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) = event else {
728 return None;
729 };
730 let on_key = behaviour.on_key.as_ref()?;
731 on_key(&keys::Event {
732 key: key.clone(),
733 modifiers: *modifiers,
734 })
735}
736
737fn is_dismiss_key<Message>(behaviour: &Behaviour<'_, Message>, event: &Event) -> bool {
738 let Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) = event else {
739 return false;
740 };
741 behaviour
742 .dismiss_keys
743 .iter()
744 .any(|chord| chord.matches(key, *modifiers))
745}
746
747fn is_press(event: &Event) -> bool {
748 matches!(
749 event,
750 Event::Mouse(mouse::Event::ButtonPressed(_))
751 | Event::Touch(touch::Event::FingerPressed { .. })
752 )
753}
754
755fn is_release(event: &Event) -> bool {
756 matches!(
757 event,
758 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
759 | Event::Touch(touch::Event::FingerLifted { .. })
760 )
761}
762
763fn press_position(event: &Event, cursor: mouse::Cursor) -> Option<Point> {
765 match event {
766 Event::Mouse(mouse::Event::ButtonPressed(_)) => cursor.position(),
767 Event::Touch(touch::Event::FingerPressed { position, .. }) => Some(*position),
768 _ => None,
769 }
770}
771
772fn is_pointer_input(event: &Event) -> bool {
773 matches!(
774 event,
775 Event::Mouse(
776 mouse::Event::ButtonPressed(_)
777 | mouse::Event::ButtonReleased(_)
778 | mouse::Event::WheelScrolled { .. }
779 ) | Event::Touch(_)
780 )
781}
782
783impl<Message: Clone> overlay::Overlay<Message, Theme, Renderer> for Floating<'_, '_, Message> {
784 fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
785 let viewport = Rectangle::with_size(bounds);
786 let room = viewport.shrink(MARGIN).size();
787 let room = Size::new(room.width.max(0.0), room.height.max(0.0));
788 let limits = if self.behaviour.match_width {
789 let width = self.trigger.width.min(room.width);
790 layout::Limits::new(Size::new(width, 0.0), Size::new(width, room.height))
791 } else {
792 layout::Limits::new(Size::ZERO, room)
793 };
794 let node = self
795 .content
796 .as_widget_mut()
797 .layout(self.tree, renderer, &limits);
798 let panel = place(self.target, node.size(), viewport, self.behaviour.placement);
799 layout::Node::with_children(bounds, vec![node.move_to(panel.position())])
800 }
801
802 fn draw(
803 &self,
804 renderer: &mut Renderer,
805 theme: &Theme,
806 style: &renderer::Style,
807 layout: Layout<'_>,
808 cursor: mouse::Cursor,
809 ) {
810 let Some(panel) = layout.children().next() else {
811 return;
812 };
813 self.content.as_widget().draw(
814 self.tree,
815 renderer,
816 theme,
817 style,
818 panel,
819 cursor,
820 &layout.bounds(),
821 );
822 }
823
824 fn operate(&mut self, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation) {
825 let Some(panel) = layout.children().next() else {
826 return;
827 };
828 self.content
829 .as_widget_mut()
830 .operate(self.tree, panel, renderer, operation);
831 }
832
833 fn update(
834 &mut self,
835 event: &Event,
836 layout: Layout<'_>,
837 cursor: mouse::Cursor,
838 renderer: &Renderer,
839 clipboard: &mut dyn Clipboard,
840 shell: &mut Shell<'_, Message>,
841 ) {
842 let Some(panel) = layout.children().next() else {
843 return;
844 };
845
846 self.content.as_widget_mut().update(
847 self.tree,
848 event,
849 panel,
850 cursor,
851 renderer,
852 clipboard,
853 shell,
854 &layout.bounds(),
855 );
856 if shell.is_event_captured() {
857 if self.behaviour.closes_itself && is_release(event) {
859 self.state.open = false;
860 shell.request_redraw();
861 }
862 return;
863 }
864
865 if let Some(message) = key_message(self.behaviour, event) {
866 shell.publish(message);
867 shell.capture_event();
868 return;
869 }
870 if is_dismiss_key(self.behaviour, event) && self.behaviour.can_dismiss() {
871 self.dismiss(shell);
872 shell.capture_event();
873 return;
874 }
875 if !is_pointer_input(event) {
876 return;
877 }
878
879 let bounds = panel.bounds();
880 if cursor.is_over(bounds) {
881 shell.capture_event();
882 return;
883 }
884 let Some(position) = press_position(event, cursor) else {
885 return;
886 };
887 if bounds.contains(position) {
888 shell.capture_event();
889 return;
890 }
891 let on_anchor = self.trigger.contains(position);
892 if (on_anchor && !self.behaviour.dismiss_on_anchor_press) || !self.behaviour.can_dismiss() {
893 return;
894 }
895 self.dismiss(shell);
896 if !on_anchor && !self.behaviour.pass_through {
897 shell.capture_event();
898 }
899 }
900
901 fn mouse_interaction(
902 &self,
903 layout: Layout<'_>,
904 cursor: mouse::Cursor,
905 renderer: &Renderer,
906 ) -> mouse::Interaction {
907 let Some(panel) = layout.children().next() else {
908 return mouse::Interaction::None;
909 };
910 if !cursor.is_over(panel.bounds()) {
911 return mouse::Interaction::None;
912 }
913 self.content
914 .as_widget()
915 .mouse_interaction(self.tree, panel, cursor, &layout.bounds(), renderer)
916 .max(mouse::Interaction::Idle)
917 }
918
919 fn overlay<'c>(
920 &'c mut self,
921 layout: Layout<'c>,
922 renderer: &Renderer,
923 ) -> Option<overlay::Element<'c, Message, Theme, Renderer>> {
924 let panel = layout.children().next()?;
925 let viewport = layout.bounds();
926 self.content
927 .as_widget_mut()
928 .overlay(self.tree, panel, renderer, &viewport, Vector::ZERO)
929 }
930}
931
932#[derive(Default)]
934struct FindFocus(bool);
935
936impl Operation for FindFocus {
937 fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) {
938 operate(self);
939 }
940
941 fn focusable(&mut self, _id: Option<&Id>, _bounds: Rectangle, state: &mut dyn Focusable) {
942 self.0 |= state.is_focused();
943 }
944}
945
946struct Unfocus;
948
949impl Operation for Unfocus {
950 fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) {
951 operate(self);
952 }
953
954 fn focusable(&mut self, _id: Option<&Id>, _bounds: Rectangle, state: &mut dyn Focusable) {
955 if state.is_focused() {
956 state.unfocus();
957 }
958 }
959}
960
961#[cfg(test)]
962mod tests {
963 use super::*;
964 use crate::theme::{dark, light};
965 use iced::keyboard::{Key, Modifiers};
966
967 const VIEWPORT: Rectangle = Rectangle {
968 x: 0.0,
969 y: 0.0,
970 width: 400.0,
971 height: 300.0,
972 };
973
974 fn anchor() -> Rectangle {
975 Rectangle::new(Point::new(150.0, 100.0), Size::new(100.0, 40.0))
976 }
977
978 const PANEL: Size = Size::new(60.0, 50.0);
979
980 fn at(side: Side, align: Align) -> Point {
981 place(anchor(), PANEL, VIEWPORT, Placement::new(side, align)).position()
982 }
983
984 #[test]
985 fn defaults_to_bottom_centre_with_a_small_gap() {
986 let placement = Placement::default();
987 assert_eq!(placement.side, Side::Bottom);
988 assert_eq!(placement.align, Align::Center);
989 assert_eq!(placement.gap, GAP);
990 assert_eq!(placement.offset, 0.0);
991 }
992
993 #[test]
994 fn each_side_sits_outside_the_anchor_with_the_gap() {
995 assert_eq!(at(Side::Bottom, Align::Start), Point::new(150.0, 144.0));
996 assert_eq!(at(Side::Top, Align::Start), Point::new(150.0, 46.0));
997 assert_eq!(at(Side::Left, Align::Start), Point::new(86.0, 100.0));
998 assert_eq!(at(Side::Right, Align::Start), Point::new(254.0, 100.0));
999 }
1000
1001 #[test]
1002 fn alignment_moves_along_the_side() {
1003 assert_eq!(at(Side::Bottom, Align::Center).x, 170.0);
1004 assert_eq!(at(Side::Bottom, Align::End).x, 190.0);
1005 assert_eq!(at(Side::Right, Align::Center).y, 95.0);
1006 assert_eq!(at(Side::Right, Align::End).y, 90.0);
1007 }
1008
1009 #[test]
1010 fn offset_shifts_away_from_the_aligned_edge() {
1011 let placement = Placement::new(Side::Bottom, Align::Start).offset(5.0);
1012 let start = place(anchor(), PANEL, VIEWPORT, placement);
1013 assert_eq!(start.x, 155.0);
1014 let placement = Placement::new(Side::Bottom, Align::End).offset(5.0);
1015 let end = place(anchor(), PANEL, VIEWPORT, placement);
1016 assert_eq!(end.x, 185.0);
1017 }
1018
1019 #[test]
1020 fn flips_to_the_opposite_side_when_there_is_no_room() {
1021 let low = Rectangle::new(Point::new(150.0, 230.0), Size::new(100.0, 40.0));
1022 let panel = place(
1023 low,
1024 PANEL,
1025 VIEWPORT,
1026 Placement::new(Side::Bottom, Align::Start),
1027 );
1028 assert_eq!(panel.y, 230.0 - GAP - PANEL.height);
1029
1030 let left_edge = Rectangle::new(Point::new(20.0, 100.0), Size::new(40.0, 40.0));
1031 let panel = place(
1032 left_edge,
1033 PANEL,
1034 VIEWPORT,
1035 Placement::new(Side::Left, Align::Start),
1036 );
1037 assert_eq!(panel.x, 60.0 + GAP);
1038 }
1039
1040 #[test]
1041 fn stays_put_when_neither_side_fits_and_the_requested_one_is_larger() {
1042 let tall = Size::new(60.0, 290.0);
1043 let panel = place(
1044 anchor(),
1045 tall,
1046 VIEWPORT,
1047 Placement::new(Side::Bottom, Align::Start),
1048 );
1049 assert_eq!(panel.y, MARGIN, "shifted up to fit, pinned at the margin");
1050 }
1051
1052 #[test]
1053 fn mirrors_the_alignment_when_it_would_overflow() {
1054 let right_edge = Rectangle::new(Point::new(360.0, 100.0), Size::new(30.0, 30.0));
1055 let panel = place(
1056 right_edge,
1057 PANEL,
1058 VIEWPORT,
1059 Placement::new(Side::Bottom, Align::Start),
1060 );
1061 assert_eq!(panel.x, 390.0 - PANEL.width);
1062 }
1063
1064 #[test]
1065 fn shifts_inside_the_margin_when_nothing_else_fits() {
1066 let corner = Rectangle::new(Point::new(0.0, 0.0), Size::new(10.0, 10.0));
1067 let panel = place(
1068 corner,
1069 PANEL,
1070 VIEWPORT,
1071 Placement::new(Side::Left, Align::Center),
1072 );
1073 assert_eq!(panel.x, 10.0 + GAP, "flipped to the right");
1074 assert_eq!(panel.y, MARGIN, "shifted down to the margin");
1075 }
1076
1077 #[test]
1078 fn a_list_near_the_bottom_keeps_the_margin() {
1079 let field = Rectangle::new(Point::new(20.0, 200.0), Size::new(200.0, 36.0));
1080 let list = Size::new(200.0, 80.0);
1081 let placement = Placement::new(Side::Bottom, Align::Start);
1082 let panel = place(field, list, VIEWPORT, placement);
1083 assert_eq!(panel.y, 200.0 - GAP - list.height, "flips above the field");
1084
1085 let wide = Size::new(420.0, 40.0);
1086 let panel = place(field, wide, VIEWPORT, placement);
1087 assert_eq!(panel.x, MARGIN, "clamped inside the window");
1088 }
1089
1090 #[test]
1091 fn a_point_anchor_opens_below_and_right_then_flips_near_edges() {
1092 let point = |x, y| Rectangle::new(Point::new(x, y), Size::ZERO);
1093 let placement = Placement::new(Side::Bottom, Align::Start).gap(0.0);
1094 assert_eq!(
1095 place(point(100.0, 100.0), PANEL, VIEWPORT, placement).position(),
1096 Point::new(100.0, 100.0)
1097 );
1098 assert_eq!(
1099 place(point(380.0, 280.0), PANEL, VIEWPORT, placement).position(),
1100 Point::new(320.0, 230.0)
1101 );
1102 }
1103
1104 #[test]
1105 fn a_panel_larger_than_the_window_is_pinned_top_left() {
1106 let huge = Size::new(1000.0, 1000.0);
1107 let panel = place(anchor(), huge, VIEWPORT, Placement::default());
1108 assert_eq!(panel.position(), Point::new(MARGIN, MARGIN));
1109 assert_eq!(panel.size(), huge);
1110 }
1111
1112 #[test]
1113 fn sides_and_alignments_mirror() {
1114 for side in Side::ALL {
1115 assert_eq!(side.opposite().opposite(), side);
1116 assert_ne!(side.opposite(), side);
1117 }
1118 assert_eq!(Align::Center.mirrored(), Align::Center);
1119 assert_eq!(Align::Start.mirrored(), Align::End);
1120 }
1121
1122 #[test]
1123 fn surface_uses_the_popover_colour_a_border_and_a_shadow() {
1124 for theme in [light(), dark()] {
1125 let tokens = Tokens::of(&theme);
1126 let style = surface_style(&tokens);
1127 assert_eq!(style.background, Some(Background::Color(tokens.popover)));
1128 assert_eq!(style.text_color, Some(tokens.foreground));
1129 assert_eq!(style.border.color, tokens.border);
1130 assert_eq!(style.border.width, 1.0);
1131 assert_eq!(style.border.radius, radius::MD.into());
1132 assert_eq!(style.shadow, shadow(&tokens));
1133 assert_eq!(style.shadow.offset, Vector::new(0.0, 4.0));
1134 assert_eq!(style.shadow.blur_radius, 12.0);
1135 }
1136 let light = shadow(&Tokens::of(&light())).color.a;
1137 let dark = shadow(&Tokens::of(&dark())).color.a;
1138 assert!((light - 0.1).abs() < 1e-6);
1139 assert!((dark - 0.5).abs() < 1e-6);
1140 }
1141
1142 #[test]
1143 fn builder_starts_closed_and_undismissable_with_escape_bound() {
1144 let anchored: Anchored<'_, ()> = anchored(iced::widget::text("Anchor"));
1145 let behaviour = &anchored.behaviour;
1146 assert!(anchored.content.is_none());
1147 assert!(behaviour.on_dismiss.is_none());
1148 assert!(behaviour.point.is_none());
1149 assert!(!behaviour.dismiss_on_anchor_press);
1150 assert!(!behaviour.dismiss_on_blur);
1151 assert!(!behaviour.pass_through);
1152 assert!(!behaviour.match_width);
1153 assert_eq!(behaviour.dismiss_keys, vec![Chord::named(Named::Escape)]);
1154 assert!(!behaviour.can_dismiss());
1155 assert!(!behaviour.watches_focus());
1156 }
1157
1158 #[test]
1159 fn builder_sets_every_option() {
1160 let anchored: Anchored<'_, u8> = anchored(iced::widget::text("Anchor"))
1161 .at(Point::new(3.0, 4.0))
1162 .match_width(true)
1163 .on_dismiss(1)
1164 .dismiss_keys([Chord::character('q')])
1165 .dismiss_on_anchor_press(true)
1166 .dismiss_on_blur(true)
1167 .pass_through(true)
1168 .on_anchor_press_maybe(Some(2))
1169 .on_key(|_| Some(3))
1170 .on_focus(|focused| if focused { 4 } else { 5 });
1171 let behaviour = &anchored.behaviour;
1172 assert_eq!(behaviour.focus_message(false, true), Some(4));
1173 assert_eq!(behaviour.focus_message(true, false), Some(5));
1174 assert_eq!(behaviour.focus_message(true, true), None);
1175 assert_eq!(behaviour.point, Some(Point::new(3.0, 4.0)));
1176 assert!(behaviour.match_width && behaviour.pass_through);
1177 assert!(behaviour.dismiss_on_anchor_press && behaviour.dismiss_on_blur);
1178 assert_eq!(behaviour.on_dismiss, Some(1));
1179 assert_eq!(behaviour.on_anchor_press, Some(2));
1180 assert_eq!(behaviour.dismiss_keys, vec![Chord::character('q')]);
1181 assert!(behaviour.can_dismiss() && behaviour.watches_focus());
1182 }
1183
1184 fn key_press(key: Key, modifiers: Modifiers) -> Event {
1185 Event::Keyboard(keyboard::Event::KeyPressed {
1186 key: key.clone(),
1187 modified_key: key,
1188 physical_key: keyboard::key::Physical::Unidentified(
1189 keyboard::key::NativeCode::Unidentified,
1190 ),
1191 location: keyboard::Location::Standard,
1192 modifiers,
1193 text: None,
1194 repeat: false,
1195 })
1196 }
1197
1198 #[test]
1199 fn keys_resolve_through_on_key_then_the_dismiss_chords() {
1200 let behaviour: Behaviour<'_, u8> = Behaviour {
1201 on_key: Some(Box::new(|key: &keys::Event| {
1202 (key.key == Key::Named(Named::ArrowDown)).then_some(7)
1203 })),
1204 ..Behaviour::default()
1205 };
1206 let down = key_press(Key::Named(Named::ArrowDown), Modifiers::empty());
1207 let escape = key_press(Key::Named(Named::Escape), Modifiers::empty());
1208 let shift_escape = key_press(Key::Named(Named::Escape), Modifiers::SHIFT);
1209 assert_eq!(key_message(&behaviour, &down), Some(7));
1210 assert_eq!(key_message(&behaviour, &escape), None);
1211 assert!(is_dismiss_key(&behaviour, &escape));
1212 assert!(!is_dismiss_key(&behaviour, &shift_escape));
1213 assert!(!is_dismiss_key(&behaviour, &down));
1214
1215 let unbound: Behaviour<'_, u8> = Behaviour {
1216 dismiss_keys: Vec::new(),
1217 ..Behaviour::default()
1218 };
1219 assert!(!is_dismiss_key(&unbound, &escape));
1220 }
1221
1222 #[test]
1223 fn press_position_only_reports_presses() {
1224 let cursor = mouse::Cursor::Available(Point::new(3.0, 4.0));
1225 let pressed = Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right));
1226 let released = Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left));
1227 assert_eq!(press_position(&pressed, cursor), Some(Point::new(3.0, 4.0)));
1228 assert_eq!(press_position(&released, cursor), None);
1229 assert_eq!(press_position(&pressed, mouse::Cursor::Unavailable), None);
1230 assert!(is_pointer_input(&released));
1231 assert!(is_release(&released));
1232 assert!(is_press(&pressed));
1233 assert!(!is_release(&pressed));
1234 assert!(!is_pointer_input(&Event::Mouse(mouse::Event::CursorLeft)));
1235 }
1236}