1use frus_core::{Rect, Scene};
5use frus_layout::Style;
6
7use crate::interaction::Status;
8use crate::theme::Theme;
9use crate::widget::Widget;
10
11#[derive(Copy, Clone, Debug, PartialEq, Eq)]
13pub enum Placement {
14 Below,
16 Center,
18 Tooltip,
20 Left,
23 Right,
26 Bottom,
29}
30
31pub struct OverlayPortal<Msg = crate::callback::Callback> {
33 children: Vec<Box<dyn Widget<Msg>>>,
35 placement: Placement,
36 on_dismiss: Option<Msg>,
38}
39
40impl<Msg: 'static> OverlayPortal<Msg> {
41 pub fn new(anchor: impl Widget<Msg> + 'static) -> Self {
43 Self::new_boxed(Box::new(anchor))
44 }
45
46 pub fn new_boxed(anchor: Box<dyn Widget<Msg>>) -> Self {
49 Self {
50 children: vec![Box::new(anchor)],
51 placement: Placement::Below,
52 on_dismiss: None,
53 }
54 }
55
56 pub fn overlay(self, content: impl Widget<Msg> + 'static, placement: Placement) -> Self {
58 self.overlay_boxed(Box::new(content), placement)
59 }
60
61 pub fn overlay_boxed(mut self, content: Box<dyn Widget<Msg>>, placement: Placement) -> Self {
63 self.children.truncate(1);
64 self.children.push(Box::new(content));
65 self.placement = placement;
66 self
67 }
68
69 pub fn dismiss(mut self, message: impl Into<Msg>) -> Self {
71 self.on_dismiss = Some(message.into());
72 self
73 }
74}
75
76impl<Msg: Clone> Widget<Msg> for OverlayPortal<Msg> {
77 fn style(&self) -> Style {
78 Style::default()
79 }
80
81 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
82 &self.children
83 }
84
85 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
86
87 fn on_click(&self) -> Option<Msg> {
88 None
89 }
90
91 fn overlay(&self) -> Option<(&dyn Widget<Msg>, Placement)> {
92 self.children
93 .get(1)
94 .map(|content| (content.as_ref(), self.placement))
95 }
96
97 fn overlay_dismiss(&self) -> Option<Msg> {
98 self.on_dismiss.clone()
99 }
100
101 fn on_key(&self, key: &crate::Key) -> crate::KeyResponse<Msg> {
104 match (key, &self.on_dismiss) {
105 (crate::Key::Escape, Some(message)) => {
106 crate::KeyResponse::Handled(Some(message.clone()))
107 }
108 _ => crate::KeyResponse::Ignored,
109 }
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use crate::{Container, Runtime, Size, Text};
117
118 #[test]
119 fn overlay_present_only_when_set() {
120 let bare: OverlayPortal<()> = OverlayPortal::new(Container::<()>::new());
121 assert!(Widget::<()>::overlay(&bare).is_none());
122
123 let with: OverlayPortal<()> =
124 OverlayPortal::new(Container::<()>::new()).overlay(Text::new("tip"), Placement::Center);
125 assert!(Widget::<()>::overlay(&with).is_some());
126 }
127
128 #[test]
129 fn center_overlay_draws_scrim_and_content() {
130 let portal: OverlayPortal<()> =
132 OverlayPortal::new(Container::<()>::new().width(20.0).height(20.0)).overlay(
133 Container::<()>::new()
134 .width(100.0)
135 .height(60.0)
136 .color(frus_core::Color::WHITE),
137 Placement::Center,
138 );
139 let ui = crate::build_ui(
140 &portal,
141 Size::new(400.0, 300.0),
142 &Runtime::default(),
143 &crate::Theme::default(),
144 );
145 let full_screen =
147 ui.scene().primitives().iter().any(
148 |p| matches!(p, frus_core::Primitive::Rect { rect, .. } if rect.width >= 400.0),
149 );
150 assert!(full_screen, "the full-screen scrim must be present");
151 }
152
153 #[test]
160 fn an_overlay_whose_anchor_left_the_window_goes_with_it() {
161 let row: crate::Flex<()> = crate::Flex::row()
163 .child(Container::<()>::new().width(900.0).height(20.0))
164 .child(
165 OverlayPortal::new(Container::<()>::new().width(20.0).height(20.0))
166 .overlay(
167 Container::<()>::new()
168 .width(120.0)
169 .height(40.0)
170 .color(frus_core::Color::WHITE),
171 Placement::Below,
172 )
173 .dismiss(()),
174 );
175 let ui = crate::build_ui(
176 &row,
177 Size::new(400.0, 300.0),
178 &Runtime::default(),
179 &crate::Theme::default(),
180 );
181 let inside = ui.scene().primitives().iter().any(|p| {
183 matches!(p, frus_core::Primitive::Rect { rect, .. }
184 if (rect.width - 120.0).abs() < 0.5 && rect.x < 400.0)
185 });
186 assert!(!inside, "the overlay was pulled back into the window");
187 assert!(
190 ui.top_dismiss().is_none(),
191 "an invisible barrier would eat the next press"
192 );
193 }
194}