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(mut self, bar: AnyView<State>) -> Self {
98 self.app_bar = Some(bar);
99 self
100 }
101
102 pub fn app_bar_opt(mut self, bar: Option<AnyView<State>>) -> Self {
106 self.app_bar = bar;
107 self
108 }
109
110 pub fn bottom_bar(mut self, bar: AnyView<State>) -> Self {
113 self.bottom_bar = Some(bar);
114 self
115 }
116
117 pub fn bottom_bar_opt(mut self, bar: Option<AnyView<State>>) -> Self {
120 self.bottom_bar = bar;
121 self
122 }
123
124 pub fn fab(mut self, fab: AnyView<State>) -> Self {
126 self.fab = Some(fab);
127 self
128 }
129
130 pub fn fab_margin(mut self, margin: f64) -> Self {
133 self.fab_margin = margin;
134 self
135 }
136
137 pub fn fab_alignment(mut self, alignment: Alignment) -> Self {
140 self.fab_alignment = alignment;
141 self
142 }
143
144 pub fn body_behind_app_bar(mut self, enabled: bool) -> Self {
149 self.body_behind_app_bar = enabled;
150 self
151 }
152
153 pub fn background(mut self, color: Color) -> Self {
157 self.background = Some(color);
158 self
159 }
160}
161
162pub struct ScaffoldWidget {
164 body: ChildPod,
165 app_bar: Option<ChildPod>,
166 bottom_bar: Option<ChildPod>,
167 fab: Option<ChildPod>,
168 fab_margin: f64,
169 fab_alignment: Alignment,
170 body_behind_app_bar: bool,
171 background: Option<Color>,
172}
173
174fn rebuild_optional_slot<State: 'static>(
179 prev: &Option<AnyView<State>>,
180 next: &Option<AnyView<State>>,
181 slot: &mut Option<ChildPod>,
182 ctx: &mut BuildCtx<'_>,
183) -> ChangeFlags {
184 match (prev, next, &mut *slot) {
185 (Some(prev_view), Some(next_view), Some(pod)) => {
186 crate::authoring::rebuild_child(prev_view, next_view, pod, ctx)
187 }
188 (None, Some(next_view), _) => {
189 *slot = Some(crate::authoring::build_child(next_view, ctx));
190 ChangeFlags::LAYOUT | ChangeFlags::PAINT
191 }
192 (Some(prev_view), None, Some(pod)) => {
193 crate::authoring::teardown_child(prev_view, pod, ctx);
194 *slot = None;
195 ChangeFlags::LAYOUT | ChangeFlags::PAINT
196 }
197 _ => ChangeFlags::NONE,
198 }
199}
200
201impl<State: 'static> View<State> for ScaffoldView<State> {
202 type Element = ScaffoldWidget;
203
204 fn build(&self, ctx: &mut BuildCtx<'_>) -> ScaffoldWidget {
205 ScaffoldWidget {
206 body: crate::authoring::build_child(&self.body, ctx),
207 app_bar: self
208 .app_bar
209 .as_ref()
210 .map(|v| crate::authoring::build_child(v, ctx)),
211 bottom_bar: self
212 .bottom_bar
213 .as_ref()
214 .map(|v| crate::authoring::build_child(v, ctx)),
215 fab: self
216 .fab
217 .as_ref()
218 .map(|v| crate::authoring::build_child(v, ctx)),
219 fab_margin: self.fab_margin,
220 fab_alignment: self.fab_alignment,
221 body_behind_app_bar: self.body_behind_app_bar,
222 background: self.background,
223 }
224 }
225
226 fn rebuild(
227 &self,
228 prev: &Self,
229 element: &mut ScaffoldWidget,
230 ctx: &mut BuildCtx<'_>,
231 ) -> ChangeFlags {
232 let mut flags = ChangeFlags::NONE;
233 flags |= crate::authoring::rebuild_child(&prev.body, &self.body, &mut element.body, ctx);
234 flags |= rebuild_optional_slot(&prev.app_bar, &self.app_bar, &mut element.app_bar, ctx);
235 flags |= rebuild_optional_slot(
236 &prev.bottom_bar,
237 &self.bottom_bar,
238 &mut element.bottom_bar,
239 ctx,
240 );
241 flags |= rebuild_optional_slot(&prev.fab, &self.fab, &mut element.fab, ctx);
242
243 if prev.fab_margin != self.fab_margin
244 || prev.fab_alignment != self.fab_alignment
245 || prev.body_behind_app_bar != self.body_behind_app_bar
246 {
247 element.fab_margin = self.fab_margin;
248 element.fab_alignment = self.fab_alignment;
249 element.body_behind_app_bar = self.body_behind_app_bar;
250 flags |= ChangeFlags::LAYOUT;
251 }
252 if prev.background != self.background {
253 element.background = self.background;
254 flags |= ChangeFlags::PAINT;
255 }
256 flags
257 }
258
259 fn teardown(&self, element: &mut ScaffoldWidget, ctx: &mut BuildCtx<'_>) {
260 crate::authoring::teardown_child(&self.body, &mut element.body, ctx);
261 if let (Some(view), Some(pod)) = (&self.app_bar, &mut element.app_bar) {
262 crate::authoring::teardown_child(view, pod, ctx);
263 }
264 if let (Some(view), Some(pod)) = (&self.bottom_bar, &mut element.bottom_bar) {
265 crate::authoring::teardown_child(view, pod, ctx);
266 }
267 if let (Some(view), Some(pod)) = (&self.fab, &mut element.fab) {
268 crate::authoring::teardown_child(view, pod, ctx);
269 }
270 }
271}
272
273fn fraction(component: f64) -> f64 {
279 (component + 1.0) / 2.0
280}
281
282impl Widget for ScaffoldWidget {
283 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
284 let width = if bc.max().width.is_finite() {
285 bc.max().width
286 } else {
287 bc.min().width
288 };
289 let bounded_height = bc.max().height.is_finite();
290 let outer_height = bc.max().height;
291
292 let bar_bc = BoxConstraints::new(Size::new(width, 0.0), Size::new(width, f64::INFINITY));
295 let bar_height = match &mut self.app_bar {
296 Some(pod) => {
297 let size = pod.layout_child(ctx, &bar_bc);
298 pod.set_origin(Point::ZERO);
299 size.height
300 }
301 None => 0.0,
302 };
303
304 let bottom_bar_height = match &mut self.bottom_bar {
308 Some(pod) => pod.layout_child(ctx, &bar_bc).height,
309 None => 0.0,
310 };
311
312 let body_top = if self.body_behind_app_bar {
316 0.0
317 } else {
318 bar_height
319 };
320 let body_bc = if bounded_height {
321 let body_height = (outer_height - body_top - bottom_bar_height).max(0.0);
322 BoxConstraints::tight(Size::new(width, body_height))
323 } else {
324 BoxConstraints::new(Size::new(width, 0.0), Size::new(width, f64::INFINITY))
325 };
326 let body_size = self.body.layout_child(ctx, &body_bc);
327 self.body.set_origin(Point::new(0.0, body_top));
328
329 let total_height = if bounded_height {
330 outer_height
331 } else {
332 body_top + body_size.height + bottom_bar_height
333 };
334
335 if let Some(pod) = &mut self.bottom_bar {
336 pod.set_origin(Point::new(0.0, (total_height - bottom_bar_height).max(0.0)));
337 }
338
339 let bottom_bar_present = self.bottom_bar.is_some();
343 if let Some(pod) = &mut self.fab {
344 let fab_size =
345 pod.layout_child(ctx, &BoxConstraints::loose(Size::new(width, total_height)));
346 let extra_bottom = if bottom_bar_present {
347 0.0
348 } else {
349 ctx.window_insets().padding().bottom
350 };
351 let box_top = body_top + self.fab_margin;
352 let box_left = self.fab_margin;
353 let box_right = (width - self.fab_margin).max(box_left);
354 let box_bottom =
355 (total_height - bottom_bar_height - self.fab_margin - extra_bottom).max(box_top);
356
357 let free_w = (box_right - box_left - fab_size.width).max(0.0);
358 let free_h = (box_bottom - box_top - fab_size.height).max(0.0);
359 let x = box_left + free_w * fraction(self.fab_alignment.x);
360 let y = box_top + free_h * fraction(self.fab_alignment.y);
361 pod.set_origin(Point::new(x, y));
362 }
363
364 bc.constrain(Size::new(width, total_height))
365 }
366
367 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
368 if let Some(color) = self.background {
369 scene.fill_rect(ctx.origin(), ctx.size(), color);
370 }
371 self.body.paint_child(ctx, scene);
374 if let Some(pod) = &mut self.app_bar {
375 pod.paint_child(ctx, scene);
376 }
377 if let Some(pod) = &mut self.bottom_bar {
378 pod.paint_child(ctx, scene);
379 }
380 if let Some(pod) = &mut self.fab {
381 pod.paint_child(ctx, scene);
382 }
383 }
384
385 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
386 let mut slots: [Option<&mut ChildPod>; 4] = [
392 Some(&mut self.body),
393 self.app_bar.as_mut(),
394 self.bottom_bar.as_mut(),
395 self.fab.as_mut(),
396 ];
397 route_slots(&mut slots, ctx, event)
398 }
399
400 fn semantics(&self, ctx: &mut SemanticsCtx) {
401 self.body.semantics_child(ctx);
404 if let Some(pod) = &self.app_bar {
405 pod.semantics_child(ctx);
406 }
407 if let Some(pod) = &self.bottom_bar {
408 pod.semantics_child(ctx);
409 }
410 if let Some(pod) = &self.fab {
411 pod.semantics_child(ctx);
412 }
413 }
414
415 crate::authoring::visit_children!(body, app_bar, bottom_bar, fab);
416}
417
418fn releases_capture(event: &InputEvent) -> bool {
423 matches!(
424 event,
425 InputEvent::Pointer(p) if matches!(p.phase, PointerPhase::Up | PointerPhase::Cancel)
426 )
427}
428
429fn is_pointer_down(event: &InputEvent) -> bool {
432 matches!(
433 event,
434 InputEvent::Pointer(p) if matches!(p.phase, PointerPhase::Down)
435 )
436}
437
438fn route_slots(
445 slots: &mut [Option<&mut ChildPod>; 4],
446 ctx: &mut EventCtx<'_>,
447 event: &InputEvent,
448) -> EventResult {
449 if event.is_broadcast() {
450 for slot in slots.iter_mut() {
451 if let Some(pod) = slot.as_deref_mut() {
452 pod.event_child(ctx, event);
453 }
454 }
455 return EventResult::Ignored;
456 }
457 if event.is_focus_routed() {
458 for slot in slots.iter_mut() {
459 if let Some(pod) = slot.as_deref_mut()
460 && pod.is_focused()
461 {
462 return pod.event_child(ctx, event);
463 }
464 }
465 return EventResult::Ignored;
466 }
467 for slot in slots.iter_mut() {
468 if let Some(pod) = slot.as_deref_mut()
469 && pod.is_active()
470 {
471 let result = pod.event_child(ctx, event);
472 if releases_capture(event) {
473 pod.set_active(false);
474 }
475 return result;
476 }
477 }
478
479 let position = event.position();
480 let mut handled = EventResult::Ignored;
481 let mut kept_focus: Option<usize> = None;
482 for (i, slot) in slots.iter_mut().enumerate().rev() {
483 if let Some(pod) = slot.as_deref_mut()
484 && pod.contains(position)
485 && pod.event_child(ctx, event) == EventResult::Handled
486 {
487 if pod.is_focused() {
488 kept_focus = Some(i);
489 }
490 handled = EventResult::Handled;
491 break;
492 }
493 }
494 if is_pointer_down(event) {
495 for (i, slot) in slots.iter_mut().enumerate() {
496 if Some(i) != kept_focus
497 && let Some(pod) = slot.as_deref_mut()
498 && pod.is_focused()
499 {
500 pod.set_focused(false);
501 }
502 }
503 }
504 handled
505}
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510 use crate::test_support::leaf;
511 use frust_core::BuildCtx;
512
513 fn build<S: 'static>(view: &ScaffoldView<S>) -> ScaffoldWidget {
514 let mut counter = 0u64;
515 view.build(&mut BuildCtx::new(&mut counter))
516 }
517
518 #[test]
519 fn body_only_fills_the_whole_box() {
520 let view: ScaffoldView<()> = scaffold(leaf(50.0, 50.0));
521 let mut w = build(&view);
522 let mut lctx = LayoutCtx::new();
523 let size = w.layout(&mut lctx, &BoxConstraints::tight(Size::new(300.0, 400.0)));
524 assert_eq!(size, Size::new(300.0, 400.0));
525 assert_eq!(w.body.origin(), Point::ZERO);
526 assert_eq!(w.body.size(), Size::new(300.0, 400.0));
527 assert!(w.app_bar.is_none());
528 assert!(w.bottom_bar.is_none());
529 assert!(w.fab.is_none());
530 }
531
532 #[test]
533 fn app_bar_offsets_the_body_by_its_own_height() {
534 let view: ScaffoldView<()> = scaffold(leaf(50.0, 50.0)).app_bar(any(leaf(0.0, 56.0)));
535 let mut w = build(&view);
536 let mut lctx = LayoutCtx::new();
537 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(300.0, 400.0)));
538 assert_eq!(w.app_bar.as_ref().unwrap().origin(), Point::ZERO);
539 assert_eq!(w.app_bar.as_ref().unwrap().size().height, 56.0);
540 assert_eq!(w.body.origin(), Point::new(0.0, 56.0));
541 assert_eq!(w.body.size(), Size::new(300.0, 344.0));
542 }
543
544 #[test]
545 fn body_behind_app_bar_gives_the_body_full_height() {
546 let view: ScaffoldView<()> = scaffold(leaf(50.0, 50.0))
547 .app_bar(any(leaf(0.0, 56.0)))
548 .body_behind_app_bar(true);
549 let mut w = build(&view);
550 let mut lctx = LayoutCtx::new();
551 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(300.0, 400.0)));
552 assert_eq!(w.body.origin(), Point::ZERO);
553 assert_eq!(w.body.size(), Size::new(300.0, 400.0));
554 }
555
556 #[test]
557 fn bottom_bar_sits_at_the_bottom_edge_and_body_stops_above_it() {
558 let view: ScaffoldView<()> = scaffold(leaf(50.0, 50.0)).bottom_bar(any(leaf(0.0, 48.0)));
559 let mut w = build(&view);
560 let mut lctx = LayoutCtx::new();
561 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(300.0, 400.0)));
562 let bar = w.bottom_bar.as_ref().unwrap();
563 assert_eq!(bar.size().height, 48.0);
564 assert_eq!(bar.origin(), Point::new(0.0, 400.0 - 48.0));
565 assert_eq!(w.body.origin(), Point::ZERO);
566 assert_eq!(w.body.size(), Size::new(300.0, 400.0 - 48.0));
567 }
568
569 #[test]
570 fn fab_defaults_to_bottom_right_inset_by_margin() {
571 let view: ScaffoldView<()> = scaffold(leaf(50.0, 50.0)).fab(any(leaf(56.0, 56.0)));
572 let mut w = build(&view);
573 let mut lctx = LayoutCtx::new();
574 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(300.0, 400.0)));
575 let fab = w.fab.as_ref().unwrap();
576 assert_eq!(
579 fab.origin(),
580 Point::new(300.0 - 16.0 - 56.0, 400.0 - 16.0 - 56.0)
581 );
582 }
583
584 #[test]
585 fn fab_floats_above_a_present_bottom_bar_with_no_double_inset() {
586 let view: ScaffoldView<()> = scaffold(leaf(50.0, 50.0))
587 .bottom_bar(any(leaf(0.0, 48.0)))
588 .fab(any(leaf(56.0, 56.0)));
589 let mut w = build(&view);
590 let mut lctx = LayoutCtx::new();
591 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(300.0, 400.0)));
592 let fab = w.fab.as_ref().unwrap();
593 assert_eq!(
596 fab.origin(),
597 Point::new(300.0 - 16.0 - 56.0, 400.0 - 48.0 - 16.0 - 56.0)
598 );
599 }
600
601 #[test]
602 fn all_four_slots_together() {
603 let view: ScaffoldView<()> = scaffold(leaf(50.0, 50.0))
604 .app_bar(any(leaf(0.0, 56.0)))
605 .bottom_bar(any(leaf(0.0, 48.0)))
606 .fab(any(leaf(56.0, 56.0)));
607 let mut w = build(&view);
608 let mut lctx = LayoutCtx::new();
609 w.layout(&mut lctx, &BoxConstraints::tight(Size::new(300.0, 400.0)));
610 assert_eq!(w.body.origin(), Point::new(0.0, 56.0));
611 assert_eq!(w.body.size(), Size::new(300.0, 400.0 - 56.0 - 48.0));
612 assert_eq!(
613 w.bottom_bar.as_ref().unwrap().origin(),
614 Point::new(0.0, 400.0 - 48.0)
615 );
616 assert_eq!(
617 w.fab.as_ref().unwrap().origin(),
618 Point::new(300.0 - 16.0 - 56.0, 400.0 - 48.0 - 16.0 - 56.0)
619 );
620 }
621}