1use frust_core::{
39 BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent, LayoutCtx,
40 PaintCtx, PaintScene, SemanticsCtx, View, Widget, WindowInsets, any,
41};
42use kurbo::{Point, Size};
43
44pub struct SafeAreaView<State: 'static> {
46 left: bool,
47 top: bool,
48 right: bool,
49 bottom: bool,
50 minimum: crate::EdgeInsets,
51 child: frust_core::AnyView<State>,
52}
53
54pub fn safe_area<State: 'static, V: View<State>>(child: V) -> SafeAreaView<State> {
60 SafeAreaView {
61 left: true,
62 top: true,
63 right: true,
64 bottom: true,
65 minimum: crate::EdgeInsets::all(0.0),
66 child: any(child),
67 }
68}
69
70impl<State: 'static> SafeAreaView<State> {
71 pub fn left(mut self, enabled: bool) -> Self {
75 self.left = enabled;
76 self
77 }
78
79 pub fn top(mut self, enabled: bool) -> Self {
81 self.top = enabled;
82 self
83 }
84
85 pub fn right(mut self, enabled: bool) -> Self {
87 self.right = enabled;
88 self
89 }
90
91 pub fn bottom(mut self, enabled: bool) -> Self {
93 self.bottom = enabled;
94 self
95 }
96
97 pub fn minimum(mut self, minimum: crate::EdgeInsets) -> Self {
102 self.minimum = minimum;
103 self
104 }
105}
106
107pub struct SafeAreaWidget {
109 left: bool,
110 top: bool,
111 right: bool,
112 bottom: bool,
113 minimum: crate::EdgeInsets,
114 child: ChildPod,
115}
116
117impl<State: 'static> View<State> for SafeAreaView<State> {
118 type Element = SafeAreaWidget;
119
120 fn build(&self, ctx: &mut BuildCtx<'_>) -> SafeAreaWidget {
121 SafeAreaWidget {
122 left: self.left,
123 top: self.top,
124 right: self.right,
125 bottom: self.bottom,
126 minimum: self.minimum,
127 child: crate::authoring::build_child(&self.child, ctx),
128 }
129 }
130
131 fn rebuild(
132 &self,
133 prev: &Self,
134 element: &mut SafeAreaWidget,
135 ctx: &mut BuildCtx<'_>,
136 ) -> ChangeFlags {
137 let mut flags = ChangeFlags::NONE;
138 if prev.left != self.left
139 || prev.top != self.top
140 || prev.right != self.right
141 || prev.bottom != self.bottom
142 || prev.minimum != self.minimum
143 {
144 element.left = self.left;
145 element.top = self.top;
146 element.right = self.right;
147 element.bottom = self.bottom;
148 element.minimum = self.minimum;
149 flags |= ChangeFlags::LAYOUT;
150 }
151 flags |= crate::authoring::rebuild_child(&prev.child, &self.child, &mut element.child, ctx);
152 flags
153 }
154
155 fn teardown(&self, element: &mut SafeAreaWidget, ctx: &mut BuildCtx<'_>) {
156 crate::authoring::teardown_child(&self.child, &mut element.child, ctx);
157 }
158}
159
160impl SafeAreaWidget {
161 fn consumed_insets(&self, insets: WindowInsets) -> WindowInsets {
164 insets.consuming(self.left, self.top, self.right, self.bottom)
165 }
166}
167
168impl Widget for SafeAreaWidget {
169 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
170 let padding = ctx.window_insets().padding();
174 let left = (if self.left { padding.left } else { 0.0 }).max(self.minimum.left);
175 let top = (if self.top { padding.top } else { 0.0 }).max(self.minimum.top);
176 let right = (if self.right { padding.right } else { 0.0 }).max(self.minimum.right);
177 let bottom = (if self.bottom { padding.bottom } else { 0.0 }).max(self.minimum.bottom);
178
179 let h = left + right;
180 let v = top + bottom;
181 let child_bc = BoxConstraints::new(
184 Size::new(
185 (bc.min().width - h).max(0.0),
186 (bc.min().height - v).max(0.0),
187 ),
188 Size::new(
189 (bc.max().width - h).max(0.0),
190 (bc.max().height - v).max(0.0),
191 ),
192 );
193 let consumed = self.consumed_insets(ctx.window_insets());
198 let child = &mut self.child;
199 let child_size = ctx.with_window_insets(consumed, |ctx| child.layout_child(ctx, &child_bc));
200 self.child.set_origin(Point::new(left, top));
201 bc.constrain(Size::new(child_size.width + h, child_size.height + v))
202 }
203
204 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
205 let consumed = self.consumed_insets(ctx.window_insets());
208 let child = &mut self.child;
209 ctx.with_window_insets(consumed, |ctx| child.paint_child(ctx, scene));
210 }
211
212 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
213 crate::authoring::route_event_single(&mut self.child, ctx, event)
214 }
215
216 fn semantics(&self, ctx: &mut SemanticsCtx) {
217 self.child.semantics_child(ctx);
219 }
220
221 crate::authoring::visit_children!(child);
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227 use crate::test_support::leaf;
228 use frust_core::{
229 PointerButton, PointerEvent, PointerPhase, RenderRoot, WindowEdgeInsets, WindowInsets,
230 };
231 use std::any::Any;
232
233 fn safe_area_widget(root: &RenderRoot<(), SafeAreaView<()>>) -> &SafeAreaWidget {
234 let id = root.root_id().expect("root built");
235 (root.tree().pod(id).expect("root pod").widget() as &dyn Any)
236 .downcast_ref::<SafeAreaWidget>()
237 .expect("root is a SafeAreaWidget")
238 }
239
240 #[test]
241 fn no_insets_behaves_like_minimum_padding() {
242 fn logic(_: &mut ()) -> SafeAreaView<()> {
245 safe_area(leaf(40.0, 20.0)).minimum(crate::EdgeInsets {
246 left: 5.0,
247 top: 10.0,
248 right: 15.0,
249 bottom: 20.0,
250 })
251 }
252 let mut root: RenderRoot<(), SafeAreaView<()>> = RenderRoot::new();
253 let mut state = ();
254 root.rebuild(&mut logic, &mut state);
255 let size = root.layout(Size::new(500.0, 500.0));
256 assert_eq!(size, Size::new(60.0, 50.0));
257 let w = safe_area_widget(&root);
258 assert_eq!(w.child.origin(), Point::new(5.0, 10.0));
259 assert_eq!(w.child.size(), Size::new(40.0, 20.0));
260 }
261
262 #[test]
263 fn pushed_insets_pad_enabled_edges_only() {
264 fn logic(_: &mut ()) -> SafeAreaView<()> {
265 safe_area(leaf(40.0, 20.0)).right(false)
266 }
267 let mut root: RenderRoot<(), SafeAreaView<()>> = RenderRoot::new();
268 let mut state = ();
269 root.rebuild(&mut logic, &mut state);
270 root.set_insets(WindowInsets::new(
271 WindowEdgeInsets::new(10.0, 24.0, 30.0, 34.0),
272 WindowEdgeInsets::ZERO,
273 ));
274 let size = root.layout(Size::new(500.0, 500.0));
275 let w = safe_area_widget(&root);
276 assert_eq!(w.child.origin(), Point::new(10.0, 24.0));
279 assert_eq!(size, Size::new(40.0 + 10.0, 20.0 + 24.0 + 34.0));
280 }
281
282 #[test]
283 fn minimum_wins_when_larger() {
284 fn logic(_: &mut ()) -> SafeAreaView<()> {
285 safe_area(leaf(40.0, 20.0)).minimum(crate::EdgeInsets::all(50.0))
286 }
287 let mut root: RenderRoot<(), SafeAreaView<()>> = RenderRoot::new();
288 let mut state = ();
289 root.rebuild(&mut logic, &mut state);
290 root.set_insets(WindowInsets::new(
291 WindowEdgeInsets::new(10.0, 10.0, 10.0, 10.0),
292 WindowEdgeInsets::ZERO,
293 ));
294 root.layout(Size::new(500.0, 500.0));
295 let w = safe_area_widget(&root);
296 assert_eq!(
297 w.child.origin(),
298 Point::new(50.0, 50.0),
299 "the larger minimum wins over the smaller pushed inset on every edge"
300 );
301 }
302
303 #[test]
304 fn ime_overlap_clamps_bottom_padding_to_zero() {
305 fn logic(_: &mut ()) -> SafeAreaView<()> {
310 safe_area(leaf(40.0, 20.0))
311 }
312 let mut root: RenderRoot<(), SafeAreaView<()>> = RenderRoot::new();
313 let mut state = ();
314 root.rebuild(&mut logic, &mut state);
315 root.set_insets(WindowInsets::new(
316 WindowEdgeInsets::new(0.0, 24.0, 0.0, 34.0),
317 WindowEdgeInsets::new(0.0, 0.0, 0.0, 340.0),
318 ));
319 root.layout(Size::new(500.0, 500.0));
320 let w = safe_area_widget(&root);
321 assert_eq!(
322 w.child.origin(),
323 Point::new(0.0, 24.0),
324 "bottom padding clamps to 0 under the IME overlap; top is unaffected"
325 );
326 }
327
328 #[derive(Default)]
331 struct Counter {
332 presses: u32,
333 }
334
335 fn pointer_ev(phase: PointerPhase, x: f64, y: f64) -> InputEvent {
336 InputEvent::Pointer(PointerEvent {
337 phase,
338 position: Point::new(x, y),
339 button: PointerButton::Primary,
340 })
341 }
342
343 fn button_safe_area(minimum: crate::EdgeInsets) -> SafeAreaWidget {
347 let view: SafeAreaView<Counter> =
348 safe_area(crate::button::<Counter, _>("go", |s: &mut Counter| {
349 s.presses += 1
350 }))
351 .minimum(minimum);
352 let mut counter = 0u64;
353 let mut w = view.build(&mut BuildCtx::new(&mut counter));
354 let mut text_ctx = frust_text::TextContext::new();
355 let mut lctx = LayoutCtx::with_text_context(&mut text_ctx);
356 w.layout(&mut lctx, &BoxConstraints::loose(Size::new(500.0, 500.0)));
357 w
358 }
359
360 #[test]
361 fn event_coordinates_route_through_the_offset() {
362 let mut w = button_safe_area(crate::EdgeInsets::all(10.0));
363 let origin = w.child.origin();
364 assert_eq!(origin, Point::new(10.0, 10.0));
365
366 let mut state = Counter::default();
367 let state_any: &mut dyn Any = &mut state;
368 let mut ctx = EventCtx::new(state_any, Point::ZERO, Size::new(500.0, 500.0));
369
370 let inside = Point::new(origin.x + 2.0, origin.y + 2.0);
371 assert_eq!(
372 w.event(
373 &mut ctx,
374 &pointer_ev(PointerPhase::Down, inside.x, inside.y)
375 ),
376 EventResult::Handled
377 );
378 assert_eq!(
379 w.event(&mut ctx, &pointer_ev(PointerPhase::Up, inside.x, inside.y)),
380 EventResult::Handled
381 );
382 drop(ctx);
383 assert_eq!(
384 state.presses, 1,
385 "a synthetic tap at the child's offset location must reach it"
386 );
387 }
388}