1use frust_core::{
49 AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent,
50 LayoutCtx, PaintCtx, PaintScene, PointerPhase, SemanticsCtx, View, Widget, any,
51};
52use kurbo::{Point, Size};
53use peniko::Color;
54
55use crate::Alignment;
56
57const DEFAULT_FAB_MARGIN: f64 = 16.0;
64
65pub struct ScaffoldView<State: 'static> {
67 body: AnyView<State>,
68 app_bar: Option<AnyView<State>>,
69 bottom_bar: Option<AnyView<State>>,
70 fab: Option<AnyView<State>>,
71 fab_margin: f64,
72 fab_alignment: Alignment,
73 body_behind_app_bar: bool,
74 background: Option<Color>,
75}
76
77pub fn scaffold<State: 'static, V: View<State>>(body: V) -> ScaffoldView<State> {
80 ScaffoldView {
81 body: any(body),
82 app_bar: None,
83 bottom_bar: None,
84 fab: None,
85 fab_margin: DEFAULT_FAB_MARGIN,
86 fab_alignment: Alignment::BOTTOM_RIGHT,
90 body_behind_app_bar: false,
91 background: None,
92 }
93}
94
95impl<State: 'static> ScaffoldView<State> {
96 pub fn app_bar<V: View<State>>(mut self, bar: V) -> Self {
98 self.app_bar = Some(any(bar));
99 self
100 }
101
102 pub fn app_bar_opt<V: View<State>>(mut self, bar: Option<V>) -> Self {
115 self.app_bar = bar.map(any);
116 self
117 }
118
119 pub fn bottom_bar<V: View<State>>(mut self, bar: V) -> Self {
122 self.bottom_bar = Some(any(bar));
123 self
124 }
125
126 pub fn bottom_bar_opt<V: View<State>>(mut self, bar: Option<V>) -> Self {
129 self.bottom_bar = bar.map(any);
130 self
131 }
132
133 pub fn fab<V: View<State>>(mut self, fab: V) -> Self {
135 self.fab = Some(any(fab));
136 self
137 }
138
139 pub fn fab_margin(mut self, margin: f64) -> Self {
142 self.fab_margin = margin;
143 self
144 }
145
146 pub fn fab_alignment(mut self, alignment: Alignment) -> Self {
149 self.fab_alignment = alignment;
150 self
151 }
152
153 pub fn body_behind_app_bar(mut self, enabled: bool) -> Self {
158 self.body_behind_app_bar = enabled;
159 self
160 }
161
162 pub fn background(mut self, color: Color) -> Self {
166 self.background = Some(color);
167 self
168 }
169}
170
171pub struct ScaffoldWidget {
173 body: ChildPod,
174 app_bar: Option<ChildPod>,
175 bottom_bar: Option<ChildPod>,
176 fab: Option<ChildPod>,
177 fab_margin: f64,
178 fab_alignment: Alignment,
179 body_behind_app_bar: bool,
180 background: Option<Color>,
181}
182
183fn rebuild_optional_slot<State: 'static>(
188 prev: &Option<AnyView<State>>,
189 next: &Option<AnyView<State>>,
190 slot: &mut Option<ChildPod>,
191 ctx: &mut BuildCtx<'_>,
192) -> ChangeFlags {
193 match (prev, next, &mut *slot) {
194 (Some(prev_view), Some(next_view), Some(pod)) => {
195 crate::authoring::rebuild_child(prev_view, next_view, pod, ctx)
196 }
197 (None, Some(next_view), _) => {
198 *slot = Some(crate::authoring::build_child(next_view, ctx));
199 ChangeFlags::LAYOUT | ChangeFlags::PAINT
200 }
201 (Some(prev_view), None, Some(pod)) => {
202 crate::authoring::teardown_child(prev_view, pod, ctx);
203 *slot = None;
204 ChangeFlags::LAYOUT | ChangeFlags::PAINT
205 }
206 _ => ChangeFlags::NONE,
207 }
208}
209
210impl<State: 'static> View<State> for ScaffoldView<State> {
211 type Element = ScaffoldWidget;
212
213 fn build(&self, ctx: &mut BuildCtx<'_>) -> ScaffoldWidget {
214 ScaffoldWidget {
215 body: crate::authoring::build_child(&self.body, ctx),
216 app_bar: self
217 .app_bar
218 .as_ref()
219 .map(|v| crate::authoring::build_child(v, ctx)),
220 bottom_bar: self
221 .bottom_bar
222 .as_ref()
223 .map(|v| crate::authoring::build_child(v, ctx)),
224 fab: self
225 .fab
226 .as_ref()
227 .map(|v| crate::authoring::build_child(v, ctx)),
228 fab_margin: self.fab_margin,
229 fab_alignment: self.fab_alignment,
230 body_behind_app_bar: self.body_behind_app_bar,
231 background: self.background,
232 }
233 }
234
235 fn rebuild(
236 &self,
237 prev: &Self,
238 element: &mut ScaffoldWidget,
239 ctx: &mut BuildCtx<'_>,
240 ) -> ChangeFlags {
241 let mut flags = ChangeFlags::NONE;
242 flags |= crate::authoring::rebuild_child(&prev.body, &self.body, &mut element.body, ctx);
243 flags |= rebuild_optional_slot(&prev.app_bar, &self.app_bar, &mut element.app_bar, ctx);
244 flags |= rebuild_optional_slot(
245 &prev.bottom_bar,
246 &self.bottom_bar,
247 &mut element.bottom_bar,
248 ctx,
249 );
250 flags |= rebuild_optional_slot(&prev.fab, &self.fab, &mut element.fab, ctx);
251
252 if prev.fab_margin != self.fab_margin
253 || prev.fab_alignment != self.fab_alignment
254 || prev.body_behind_app_bar != self.body_behind_app_bar
255 {
256 element.fab_margin = self.fab_margin;
257 element.fab_alignment = self.fab_alignment;
258 element.body_behind_app_bar = self.body_behind_app_bar;
259 flags |= ChangeFlags::LAYOUT;
260 }
261 if prev.background != self.background {
262 element.background = self.background;
263 flags |= ChangeFlags::PAINT;
264 }
265 flags
266 }
267
268 fn teardown(&self, element: &mut ScaffoldWidget, ctx: &mut BuildCtx<'_>) {
269 crate::authoring::teardown_child(&self.body, &mut element.body, ctx);
270 if let (Some(view), Some(pod)) = (&self.app_bar, &mut element.app_bar) {
271 crate::authoring::teardown_child(view, pod, ctx);
272 }
273 if let (Some(view), Some(pod)) = (&self.bottom_bar, &mut element.bottom_bar) {
274 crate::authoring::teardown_child(view, pod, ctx);
275 }
276 if let (Some(view), Some(pod)) = (&self.fab, &mut element.fab) {
277 crate::authoring::teardown_child(view, pod, ctx);
278 }
279 }
280}
281
282fn fraction(component: f64) -> f64 {
288 (component + 1.0) / 2.0
289}
290
291impl Widget for ScaffoldWidget {
292 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
293 let width = if bc.max().width.is_finite() {
294 bc.max().width
295 } else {
296 bc.min().width
297 };
298 let bounded_height = bc.max().height.is_finite();
299 let outer_height = bc.max().height;
300
301 let bar_bc = BoxConstraints::new(Size::new(width, 0.0), Size::new(width, f64::INFINITY));
304 let bar_height = match &mut self.app_bar {
305 Some(pod) => {
306 let size = pod.layout_child(ctx, &bar_bc);
307 pod.set_origin(Point::ZERO);
308 size.height
309 }
310 None => 0.0,
311 };
312
313 let bottom_bar_height = match &mut self.bottom_bar {
317 Some(pod) => pod.layout_child(ctx, &bar_bc).height,
318 None => 0.0,
319 };
320
321 let body_top = if self.body_behind_app_bar {
325 0.0
326 } else {
327 bar_height
328 };
329 let body_bc = if bounded_height {
330 let body_height = (outer_height - body_top - bottom_bar_height).max(0.0);
331 BoxConstraints::tight(Size::new(width, body_height))
332 } else {
333 BoxConstraints::new(Size::new(width, 0.0), Size::new(width, f64::INFINITY))
334 };
335 let body_size = self.body.layout_child(ctx, &body_bc);
336 self.body.set_origin(Point::new(0.0, body_top));
337
338 let total_height = if bounded_height {
339 outer_height
340 } else {
341 body_top + body_size.height + bottom_bar_height
342 };
343
344 if let Some(pod) = &mut self.bottom_bar {
345 pod.set_origin(Point::new(0.0, (total_height - bottom_bar_height).max(0.0)));
346 }
347
348 let bottom_bar_present = self.bottom_bar.is_some();
352 if let Some(pod) = &mut self.fab {
353 let fab_size =
354 pod.layout_child(ctx, &BoxConstraints::loose(Size::new(width, total_height)));
355 let extra_bottom = if bottom_bar_present {
356 0.0
357 } else {
358 ctx.window_insets().padding().bottom
359 };
360 let box_top = body_top + self.fab_margin;
361 let box_left = self.fab_margin;
362 let box_right = (width - self.fab_margin).max(box_left);
363 let box_bottom =
364 (total_height - bottom_bar_height - self.fab_margin - extra_bottom).max(box_top);
365
366 let free_w = (box_right - box_left - fab_size.width).max(0.0);
367 let free_h = (box_bottom - box_top - fab_size.height).max(0.0);
368 let x = box_left + free_w * fraction(self.fab_alignment.x);
369 let y = box_top + free_h * fraction(self.fab_alignment.y);
370 pod.set_origin(Point::new(x, y));
371 }
372
373 bc.constrain(Size::new(width, total_height))
374 }
375
376 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
377 if let Some(color) = self.background {
378 scene.fill_rect(ctx.origin(), ctx.size(), color);
379 }
380 self.body.paint_child(ctx, scene);
383 if let Some(pod) = &mut self.app_bar {
384 pod.paint_child(ctx, scene);
385 }
386 if let Some(pod) = &mut self.bottom_bar {
387 pod.paint_child(ctx, scene);
388 }
389 if let Some(pod) = &mut self.fab {
390 pod.paint_child(ctx, scene);
391 }
392 }
393
394 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
395 let mut slots: [Option<&mut ChildPod>; 4] = [
401 Some(&mut self.body),
402 self.app_bar.as_mut(),
403 self.bottom_bar.as_mut(),
404 self.fab.as_mut(),
405 ];
406 route_slots(&mut slots, ctx, event)
407 }
408
409 fn semantics(&self, ctx: &mut SemanticsCtx) {
410 self.body.semantics_child(ctx);
413 if let Some(pod) = &self.app_bar {
414 pod.semantics_child(ctx);
415 }
416 if let Some(pod) = &self.bottom_bar {
417 pod.semantics_child(ctx);
418 }
419 if let Some(pod) = &self.fab {
420 pod.semantics_child(ctx);
421 }
422 }
423
424 crate::authoring::visit_children!(body, app_bar, bottom_bar, fab);
425}
426
427fn releases_capture(event: &InputEvent) -> bool {
432 matches!(
433 event,
434 InputEvent::Pointer(p) if matches!(p.phase, PointerPhase::Up | PointerPhase::Cancel)
435 )
436}
437
438fn is_pointer_down(event: &InputEvent) -> bool {
441 matches!(
442 event,
443 InputEvent::Pointer(p) if matches!(p.phase, PointerPhase::Down)
444 )
445}
446
447fn route_slots(
454 slots: &mut [Option<&mut ChildPod>; 4],
455 ctx: &mut EventCtx<'_>,
456 event: &InputEvent,
457) -> EventResult {
458 if event.is_broadcast() {
459 for slot in slots.iter_mut() {
460 if let Some(pod) = slot.as_deref_mut() {
461 pod.event_child(ctx, event);
462 }
463 }
464 return EventResult::Ignored;
465 }
466 if event.is_focus_routed() {
467 for slot in slots.iter_mut() {
468 if let Some(pod) = slot.as_deref_mut()
469 && pod.is_focused()
470 {
471 return pod.event_child(ctx, event);
472 }
473 }
474 return EventResult::Ignored;
475 }
476 for slot in slots.iter_mut() {
477 if let Some(pod) = slot.as_deref_mut()
478 && pod.is_active()
479 {
480 let result = pod.event_child(ctx, event);
481 if releases_capture(event) {
482 pod.set_active(false);
483 }
484 return result;
485 }
486 }
487
488 let position = event.position();
489 let mut handled = EventResult::Ignored;
490 let mut kept_focus: Option<usize> = None;
491 for (i, slot) in slots.iter_mut().enumerate().rev() {
492 if let Some(pod) = slot.as_deref_mut()
493 && pod.contains(position)
494 && pod.event_child(ctx, event) == EventResult::Handled
495 {
496 if pod.is_focused() {
497 kept_focus = Some(i);
498 }
499 handled = EventResult::Handled;
500 break;
501 }
502 }
503 if is_pointer_down(event) {
504 for (i, slot) in slots.iter_mut().enumerate() {
505 if Some(i) != kept_focus
506 && let Some(pod) = slot.as_deref_mut()
507 && pod.is_focused()
508 {
509 pod.set_focused(false);
510 }
511 }
512 }
513 handled
514}
515
516#[cfg(test)]
517mod tests {
518 use super::*;
519 use crate::test_support::leaf;
520 use frust_core::BuildCtx;
521
522 fn build<S: 'static>(view: &ScaffoldView<S>) -> ScaffoldWidget {
523 let mut counter = 0u64;
524 view.build(&mut BuildCtx::new(&mut counter))
525 }
526
527 #[test]
528 fn body_only_fills_the_whole_box() {
529 let view: ScaffoldView<()> = scaffold(leaf(50.0, 50.0));
530 let mut w = build(&view);
531 let mut lctx = LayoutCtx::new();
532 let size = w.layout(&mut lctx, &BoxConstraints::tight(Size::new(300.0, 400.0)));
533 assert_eq!(size, Size::new(300.0, 400.0));
534 assert_eq!(w.body.origin(), Point::ZERO);
535 assert_eq!(w.body.size(), Size::new(300.0, 400.0));
536 assert!(w.app_bar.is_none());
537 assert!(w.bottom_bar.is_none());
538 assert!(w.fab.is_none());
539 }
540
541 #[test]
542 fn app_bar_offsets_the_body_by_its_own_height() {
543 let view: ScaffoldView<()> = scaffold(leaf(50.0, 50.0)).app_bar(leaf(0.0, 56.0));
544 let mut w = build(&view);
545 let mut lctx = LayoutCtx::new();
546 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(300.0, 400.0)));
547 assert_eq!(w.app_bar.as_ref().unwrap().origin(), Point::ZERO);
548 assert_eq!(w.app_bar.as_ref().unwrap().size().height, 56.0);
549 assert_eq!(w.body.origin(), Point::new(0.0, 56.0));
550 assert_eq!(w.body.size(), Size::new(300.0, 344.0));
551 }
552
553 #[test]
554 fn body_behind_app_bar_gives_the_body_full_height() {
555 let view: ScaffoldView<()> = scaffold(leaf(50.0, 50.0))
556 .app_bar(leaf(0.0, 56.0))
557 .body_behind_app_bar(true);
558 let mut w = build(&view);
559 let mut lctx = LayoutCtx::new();
560 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(300.0, 400.0)));
561 assert_eq!(w.body.origin(), Point::ZERO);
562 assert_eq!(w.body.size(), Size::new(300.0, 400.0));
563 }
564
565 #[test]
566 fn bottom_bar_sits_at_the_bottom_edge_and_body_stops_above_it() {
567 let view: ScaffoldView<()> = scaffold(leaf(50.0, 50.0)).bottom_bar(leaf(0.0, 48.0));
568 let mut w = build(&view);
569 let mut lctx = LayoutCtx::new();
570 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(300.0, 400.0)));
571 let bar = w.bottom_bar.as_ref().unwrap();
572 assert_eq!(bar.size().height, 48.0);
573 assert_eq!(bar.origin(), Point::new(0.0, 400.0 - 48.0));
574 assert_eq!(w.body.origin(), Point::ZERO);
575 assert_eq!(w.body.size(), Size::new(300.0, 400.0 - 48.0));
576 }
577
578 #[test]
579 fn fab_defaults_to_bottom_right_inset_by_margin() {
580 let view: ScaffoldView<()> = scaffold(leaf(50.0, 50.0)).fab(leaf(56.0, 56.0));
581 let mut w = build(&view);
582 let mut lctx = LayoutCtx::new();
583 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(300.0, 400.0)));
584 let fab = w.fab.as_ref().unwrap();
585 assert_eq!(
588 fab.origin(),
589 Point::new(300.0 - 16.0 - 56.0, 400.0 - 16.0 - 56.0)
590 );
591 }
592
593 #[test]
594 fn fab_floats_above_a_present_bottom_bar_with_no_double_inset() {
595 let view: ScaffoldView<()> = scaffold(leaf(50.0, 50.0))
596 .bottom_bar(leaf(0.0, 48.0))
597 .fab(leaf(56.0, 56.0));
598 let mut w = build(&view);
599 let mut lctx = LayoutCtx::new();
600 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(300.0, 400.0)));
601 let fab = w.fab.as_ref().unwrap();
602 assert_eq!(
605 fab.origin(),
606 Point::new(300.0 - 16.0 - 56.0, 400.0 - 48.0 - 16.0 - 56.0)
607 );
608 }
609
610 #[test]
611 fn all_four_slots_together() {
612 let view: ScaffoldView<()> = scaffold(leaf(50.0, 50.0))
613 .app_bar(leaf(0.0, 56.0))
614 .bottom_bar(leaf(0.0, 48.0))
615 .fab(leaf(56.0, 56.0));
616 let mut w = build(&view);
617 let mut lctx = LayoutCtx::new();
618 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(300.0, 400.0)));
619 assert_eq!(w.body.origin(), Point::new(0.0, 56.0));
620 assert_eq!(w.body.size(), Size::new(300.0, 400.0 - 56.0 - 48.0));
621 assert_eq!(
622 w.bottom_bar.as_ref().unwrap().origin(),
623 Point::new(0.0, 400.0 - 48.0)
624 );
625 assert_eq!(
626 w.fab.as_ref().unwrap().origin(),
627 Point::new(300.0 - 16.0 - 56.0, 400.0 - 48.0 - 16.0 - 56.0)
628 );
629 }
630}