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 Selection,
34 Beside,
37}
38
39pub struct OverlayPortal<Msg = crate::callback::Callback> {
41 children: Vec<Box<dyn Widget<Msg>>>,
43 placement: Placement,
44 on_dismiss: Option<Msg>,
46}
47
48impl<Msg: 'static> OverlayPortal<Msg> {
49 pub fn new(anchor: impl Widget<Msg> + 'static) -> Self {
51 Self::new_boxed(Box::new(anchor))
52 }
53
54 pub fn new_boxed(anchor: Box<dyn Widget<Msg>>) -> Self {
57 Self {
58 children: vec![Box::new(anchor)],
59 placement: Placement::Below,
60 on_dismiss: None,
61 }
62 }
63
64 pub fn overlay(self, content: impl Widget<Msg> + 'static, placement: Placement) -> Self {
66 self.overlay_boxed(Box::new(content), placement)
67 }
68
69 pub fn overlay_boxed(mut self, content: Box<dyn Widget<Msg>>, placement: Placement) -> Self {
71 self.children.truncate(1);
72 self.children.push(Box::new(content));
73 self.placement = placement;
74 self
75 }
76
77 pub fn dismiss(mut self, message: impl Into<Msg>) -> Self {
79 self.on_dismiss = Some(message.into());
80 self
81 }
82}
83
84impl<Msg: Clone> Widget<Msg> for OverlayPortal<Msg> {
85 fn style(&self) -> Style {
90 Style {
91 flex_direction: frus_layout::FlexDirection::Column,
92 ..Style::default()
93 }
94 }
95
96 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
97 &self.children
98 }
99
100 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
101
102 fn on_click(&self) -> Option<Msg> {
103 None
104 }
105
106 fn overlay(&self) -> Option<(&dyn Widget<Msg>, Placement)> {
107 self.children
108 .get(1)
109 .map(|content| (content.as_ref(), self.placement))
110 }
111
112 fn overlay_dismiss(&self) -> Option<Msg> {
113 self.on_dismiss.clone()
114 }
115
116 fn on_key(&self, key: &crate::Key) -> crate::KeyResponse<Msg> {
119 match (key, &self.on_dismiss) {
120 (crate::Key::Escape, Some(message)) => {
121 crate::KeyResponse::Handled(Some(message.clone()))
122 }
123 _ => crate::KeyResponse::Ignored,
124 }
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use crate::{Container, Runtime, Size, Text};
132
133 #[test]
134 fn overlay_present_only_when_set() {
135 let bare: OverlayPortal<()> = OverlayPortal::new(Container::<()>::new());
136 assert!(Widget::<()>::overlay(&bare).is_none());
137
138 let with: OverlayPortal<()> =
139 OverlayPortal::new(Container::<()>::new()).overlay(Text::new("tip"), Placement::Center);
140 assert!(Widget::<()>::overlay(&with).is_some());
141 }
142
143 #[test]
144 fn center_overlay_draws_scrim_and_content() {
145 let portal: OverlayPortal<()> =
147 OverlayPortal::new(Container::<()>::new().width(20.0).height(20.0)).overlay(
148 Container::<()>::new()
149 .width(100.0)
150 .height(60.0)
151 .color(frus_core::Color::WHITE),
152 Placement::Center,
153 );
154 let ui = crate::build_ui(
155 &portal,
156 Size::new(400.0, 300.0),
157 &Runtime::default(),
158 &crate::Theme::default(),
159 );
160 let full_screen =
162 ui.scene().primitives().iter().any(
163 |p| matches!(p, frus_core::Primitive::Rect { rect, .. } if rect.width >= 400.0),
164 );
165 assert!(full_screen, "the full-screen scrim must be present");
166 }
167
168 #[test]
172 fn an_overlay_says_where_presses_are_its_own() {
173 let anchored = |dismiss: bool| {
174 let portal = OverlayPortal::<()>::new(Container::<()>::new().width(20.0).height(20.0))
175 .overlay(
176 Container::<()>::new().width(100.0).height(60.0),
177 Placement::Below,
178 );
179 let portal = if dismiss { portal.dismiss(()) } else { portal };
180 crate::build_ui(
181 &portal,
182 Size::new(400.0, 300.0),
183 &Runtime::default(),
184 &crate::Theme::default(),
185 )
186 };
187 let open = anchored(false);
188 assert!(
189 open.over_overlay(frus_core::Point::new(10.0, 40.0)),
190 "on the overlay's box"
191 );
192 assert!(
193 !open.over_overlay(frus_core::Point::new(300.0, 250.0)),
194 "elsewhere, the pages'"
195 );
196 let barred = anchored(true);
197 assert!(
198 barred.over_overlay(frus_core::Point::new(300.0, 250.0)),
199 "anywhere, under a barrier"
200 );
201 }
202
203 #[test]
210 fn an_overlay_whose_anchor_left_the_window_goes_with_it() {
211 let row: crate::Flex<()> = crate::Flex::row()
213 .child(Container::<()>::new().width(900.0).height(20.0))
214 .child(
215 OverlayPortal::new(Container::<()>::new().width(20.0).height(20.0))
216 .overlay(
217 Container::<()>::new()
218 .width(120.0)
219 .height(40.0)
220 .color(frus_core::Color::WHITE),
221 Placement::Below,
222 )
223 .dismiss(()),
224 );
225 let ui = crate::build_ui(
226 &row,
227 Size::new(400.0, 300.0),
228 &Runtime::default(),
229 &crate::Theme::default(),
230 );
231 let inside = ui.scene().primitives().iter().any(|p| {
233 matches!(p, frus_core::Primitive::Rect { rect, .. }
234 if (rect.width - 120.0).abs() < 0.5 && rect.x < 400.0)
235 });
236 assert!(!inside, "the overlay was pulled back into the window");
237 assert!(
240 ui.top_dismiss().is_none(),
241 "an invisible barrier would eat the next press"
242 );
243 }
244}