use frust_core::{
AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent,
LayoutCtx, PaintCtx, PaintScene, PointerPhase, SemanticsCtx, View, Widget, any,
};
use kurbo::{Point, Size};
use peniko::Color;
use crate::Alignment;
const DEFAULT_FAB_MARGIN: f64 = 16.0;
pub struct ScaffoldView<State: 'static> {
body: AnyView<State>,
app_bar: Option<AnyView<State>>,
bottom_bar: Option<AnyView<State>>,
fab: Option<AnyView<State>>,
fab_margin: f64,
fab_alignment: Alignment,
body_behind_app_bar: bool,
background: Option<Color>,
}
pub fn scaffold<State: 'static, V: View<State>>(body: V) -> ScaffoldView<State> {
ScaffoldView {
body: any(body),
app_bar: None,
bottom_bar: None,
fab: None,
fab_margin: DEFAULT_FAB_MARGIN,
fab_alignment: Alignment::BOTTOM_RIGHT,
body_behind_app_bar: false,
background: None,
}
}
impl<State: 'static> ScaffoldView<State> {
pub fn app_bar(mut self, bar: AnyView<State>) -> Self {
self.app_bar = Some(bar);
self
}
pub fn app_bar_opt(mut self, bar: Option<AnyView<State>>) -> Self {
self.app_bar = bar;
self
}
pub fn bottom_bar(mut self, bar: AnyView<State>) -> Self {
self.bottom_bar = Some(bar);
self
}
pub fn bottom_bar_opt(mut self, bar: Option<AnyView<State>>) -> Self {
self.bottom_bar = bar;
self
}
pub fn fab(mut self, fab: AnyView<State>) -> Self {
self.fab = Some(fab);
self
}
pub fn fab_margin(mut self, margin: f64) -> Self {
self.fab_margin = margin;
self
}
pub fn fab_alignment(mut self, alignment: Alignment) -> Self {
self.fab_alignment = alignment;
self
}
pub fn body_behind_app_bar(mut self, enabled: bool) -> Self {
self.body_behind_app_bar = enabled;
self
}
pub fn background(mut self, color: Color) -> Self {
self.background = Some(color);
self
}
}
pub struct ScaffoldWidget {
body: ChildPod,
app_bar: Option<ChildPod>,
bottom_bar: Option<ChildPod>,
fab: Option<ChildPod>,
fab_margin: f64,
fab_alignment: Alignment,
body_behind_app_bar: bool,
background: Option<Color>,
}
fn rebuild_optional_slot<State: 'static>(
prev: &Option<AnyView<State>>,
next: &Option<AnyView<State>>,
slot: &mut Option<ChildPod>,
ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
match (prev, next, &mut *slot) {
(Some(prev_view), Some(next_view), Some(pod)) => {
crate::authoring::rebuild_child(prev_view, next_view, pod, ctx)
}
(None, Some(next_view), _) => {
*slot = Some(crate::authoring::build_child(next_view, ctx));
ChangeFlags::LAYOUT | ChangeFlags::PAINT
}
(Some(prev_view), None, Some(pod)) => {
crate::authoring::teardown_child(prev_view, pod, ctx);
*slot = None;
ChangeFlags::LAYOUT | ChangeFlags::PAINT
}
_ => ChangeFlags::NONE,
}
}
impl<State: 'static> View<State> for ScaffoldView<State> {
type Element = ScaffoldWidget;
fn build(&self, ctx: &mut BuildCtx<'_>) -> ScaffoldWidget {
ScaffoldWidget {
body: crate::authoring::build_child(&self.body, ctx),
app_bar: self
.app_bar
.as_ref()
.map(|v| crate::authoring::build_child(v, ctx)),
bottom_bar: self
.bottom_bar
.as_ref()
.map(|v| crate::authoring::build_child(v, ctx)),
fab: self
.fab
.as_ref()
.map(|v| crate::authoring::build_child(v, ctx)),
fab_margin: self.fab_margin,
fab_alignment: self.fab_alignment,
body_behind_app_bar: self.body_behind_app_bar,
background: self.background,
}
}
fn rebuild(
&self,
prev: &Self,
element: &mut ScaffoldWidget,
ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
let mut flags = ChangeFlags::NONE;
flags |= crate::authoring::rebuild_child(&prev.body, &self.body, &mut element.body, ctx);
flags |= rebuild_optional_slot(&prev.app_bar, &self.app_bar, &mut element.app_bar, ctx);
flags |= rebuild_optional_slot(
&prev.bottom_bar,
&self.bottom_bar,
&mut element.bottom_bar,
ctx,
);
flags |= rebuild_optional_slot(&prev.fab, &self.fab, &mut element.fab, ctx);
if prev.fab_margin != self.fab_margin
|| prev.fab_alignment != self.fab_alignment
|| prev.body_behind_app_bar != self.body_behind_app_bar
{
element.fab_margin = self.fab_margin;
element.fab_alignment = self.fab_alignment;
element.body_behind_app_bar = self.body_behind_app_bar;
flags |= ChangeFlags::LAYOUT;
}
if prev.background != self.background {
element.background = self.background;
flags |= ChangeFlags::PAINT;
}
flags
}
fn teardown(&self, element: &mut ScaffoldWidget, ctx: &mut BuildCtx<'_>) {
crate::authoring::teardown_child(&self.body, &mut element.body, ctx);
if let (Some(view), Some(pod)) = (&self.app_bar, &mut element.app_bar) {
crate::authoring::teardown_child(view, pod, ctx);
}
if let (Some(view), Some(pod)) = (&self.bottom_bar, &mut element.bottom_bar) {
crate::authoring::teardown_child(view, pod, ctx);
}
if let (Some(view), Some(pod)) = (&self.fab, &mut element.fab) {
crate::authoring::teardown_child(view, pod, ctx);
}
}
}
fn fraction(component: f64) -> f64 {
(component + 1.0) / 2.0
}
impl Widget for ScaffoldWidget {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
let width = if bc.max().width.is_finite() {
bc.max().width
} else {
bc.min().width
};
let bounded_height = bc.max().height.is_finite();
let outer_height = bc.max().height;
let bar_bc = BoxConstraints::new(Size::new(width, 0.0), Size::new(width, f64::INFINITY));
let bar_height = match &mut self.app_bar {
Some(pod) => {
let size = pod.layout_child(ctx, &bar_bc);
pod.set_origin(Point::ZERO);
size.height
}
None => 0.0,
};
let bottom_bar_height = match &mut self.bottom_bar {
Some(pod) => pod.layout_child(ctx, &bar_bc).height,
None => 0.0,
};
let body_top = if self.body_behind_app_bar {
0.0
} else {
bar_height
};
let body_bc = if bounded_height {
let body_height = (outer_height - body_top - bottom_bar_height).max(0.0);
BoxConstraints::tight(Size::new(width, body_height))
} else {
BoxConstraints::new(Size::new(width, 0.0), Size::new(width, f64::INFINITY))
};
let body_size = self.body.layout_child(ctx, &body_bc);
self.body.set_origin(Point::new(0.0, body_top));
let total_height = if bounded_height {
outer_height
} else {
body_top + body_size.height + bottom_bar_height
};
if let Some(pod) = &mut self.bottom_bar {
pod.set_origin(Point::new(0.0, (total_height - bottom_bar_height).max(0.0)));
}
let bottom_bar_present = self.bottom_bar.is_some();
if let Some(pod) = &mut self.fab {
let fab_size =
pod.layout_child(ctx, &BoxConstraints::loose(Size::new(width, total_height)));
let extra_bottom = if bottom_bar_present {
0.0
} else {
ctx.window_insets().padding().bottom
};
let box_top = body_top + self.fab_margin;
let box_left = self.fab_margin;
let box_right = (width - self.fab_margin).max(box_left);
let box_bottom =
(total_height - bottom_bar_height - self.fab_margin - extra_bottom).max(box_top);
let free_w = (box_right - box_left - fab_size.width).max(0.0);
let free_h = (box_bottom - box_top - fab_size.height).max(0.0);
let x = box_left + free_w * fraction(self.fab_alignment.x);
let y = box_top + free_h * fraction(self.fab_alignment.y);
pod.set_origin(Point::new(x, y));
}
bc.constrain(Size::new(width, total_height))
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
if let Some(color) = self.background {
scene.fill_rect(ctx.origin(), ctx.size(), color);
}
self.body.paint_child(ctx, scene);
if let Some(pod) = &mut self.app_bar {
pod.paint_child(ctx, scene);
}
if let Some(pod) = &mut self.bottom_bar {
pod.paint_child(ctx, scene);
}
if let Some(pod) = &mut self.fab {
pod.paint_child(ctx, scene);
}
}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
let mut slots: [Option<&mut ChildPod>; 4] = [
Some(&mut self.body),
self.app_bar.as_mut(),
self.bottom_bar.as_mut(),
self.fab.as_mut(),
];
route_slots(&mut slots, ctx, event)
}
fn semantics(&self, ctx: &mut SemanticsCtx) {
self.body.semantics_child(ctx);
if let Some(pod) = &self.app_bar {
pod.semantics_child(ctx);
}
if let Some(pod) = &self.bottom_bar {
pod.semantics_child(ctx);
}
if let Some(pod) = &self.fab {
pod.semantics_child(ctx);
}
}
crate::authoring::visit_children!(body, app_bar, bottom_bar, fab);
}
fn releases_capture(event: &InputEvent) -> bool {
matches!(
event,
InputEvent::Pointer(p) if matches!(p.phase, PointerPhase::Up | PointerPhase::Cancel)
)
}
fn is_pointer_down(event: &InputEvent) -> bool {
matches!(
event,
InputEvent::Pointer(p) if matches!(p.phase, PointerPhase::Down)
)
}
fn route_slots(
slots: &mut [Option<&mut ChildPod>; 4],
ctx: &mut EventCtx<'_>,
event: &InputEvent,
) -> EventResult {
if event.is_broadcast() {
for slot in slots.iter_mut() {
if let Some(pod) = slot.as_deref_mut() {
pod.event_child(ctx, event);
}
}
return EventResult::Ignored;
}
if event.is_focus_routed() {
for slot in slots.iter_mut() {
if let Some(pod) = slot.as_deref_mut()
&& pod.is_focused()
{
return pod.event_child(ctx, event);
}
}
return EventResult::Ignored;
}
for slot in slots.iter_mut() {
if let Some(pod) = slot.as_deref_mut()
&& pod.is_active()
{
let result = pod.event_child(ctx, event);
if releases_capture(event) {
pod.set_active(false);
}
return result;
}
}
let position = event.position();
let mut handled = EventResult::Ignored;
let mut kept_focus: Option<usize> = None;
for (i, slot) in slots.iter_mut().enumerate().rev() {
if let Some(pod) = slot.as_deref_mut()
&& pod.contains(position)
&& pod.event_child(ctx, event) == EventResult::Handled
{
if pod.is_focused() {
kept_focus = Some(i);
}
handled = EventResult::Handled;
break;
}
}
if is_pointer_down(event) {
for (i, slot) in slots.iter_mut().enumerate() {
if Some(i) != kept_focus
&& let Some(pod) = slot.as_deref_mut()
&& pod.is_focused()
{
pod.set_focused(false);
}
}
}
handled
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::leaf;
use frust_core::BuildCtx;
fn build<S: 'static>(view: &ScaffoldView<S>) -> ScaffoldWidget {
let mut counter = 0u64;
view.build(&mut BuildCtx::new(&mut counter))
}
#[test]
fn body_only_fills_the_whole_box() {
let view: ScaffoldView<()> = scaffold(leaf(50.0, 50.0));
let mut w = build(&view);
let mut lctx = LayoutCtx::new();
let size = w.layout(&mut lctx, &BoxConstraints::tight(Size::new(300.0, 400.0)));
assert_eq!(size, Size::new(300.0, 400.0));
assert_eq!(w.body.origin(), Point::ZERO);
assert_eq!(w.body.size(), Size::new(300.0, 400.0));
assert!(w.app_bar.is_none());
assert!(w.bottom_bar.is_none());
assert!(w.fab.is_none());
}
#[test]
fn app_bar_offsets_the_body_by_its_own_height() {
let view: ScaffoldView<()> = scaffold(leaf(50.0, 50.0)).app_bar(any(leaf(0.0, 56.0)));
let mut w = build(&view);
let mut lctx = LayoutCtx::new();
w.layout(&mut lctx, &BoxConstraints::tight(Size::new(300.0, 400.0)));
assert_eq!(w.app_bar.as_ref().unwrap().origin(), Point::ZERO);
assert_eq!(w.app_bar.as_ref().unwrap().size().height, 56.0);
assert_eq!(w.body.origin(), Point::new(0.0, 56.0));
assert_eq!(w.body.size(), Size::new(300.0, 344.0));
}
#[test]
fn body_behind_app_bar_gives_the_body_full_height() {
let view: ScaffoldView<()> = scaffold(leaf(50.0, 50.0))
.app_bar(any(leaf(0.0, 56.0)))
.body_behind_app_bar(true);
let mut w = build(&view);
let mut lctx = LayoutCtx::new();
w.layout(&mut lctx, &BoxConstraints::tight(Size::new(300.0, 400.0)));
assert_eq!(w.body.origin(), Point::ZERO);
assert_eq!(w.body.size(), Size::new(300.0, 400.0));
}
#[test]
fn bottom_bar_sits_at_the_bottom_edge_and_body_stops_above_it() {
let view: ScaffoldView<()> = scaffold(leaf(50.0, 50.0)).bottom_bar(any(leaf(0.0, 48.0)));
let mut w = build(&view);
let mut lctx = LayoutCtx::new();
w.layout(&mut lctx, &BoxConstraints::tight(Size::new(300.0, 400.0)));
let bar = w.bottom_bar.as_ref().unwrap();
assert_eq!(bar.size().height, 48.0);
assert_eq!(bar.origin(), Point::new(0.0, 400.0 - 48.0));
assert_eq!(w.body.origin(), Point::ZERO);
assert_eq!(w.body.size(), Size::new(300.0, 400.0 - 48.0));
}
#[test]
fn fab_defaults_to_bottom_right_inset_by_margin() {
let view: ScaffoldView<()> = scaffold(leaf(50.0, 50.0)).fab(any(leaf(56.0, 56.0)));
let mut w = build(&view);
let mut lctx = LayoutCtx::new();
w.layout(&mut lctx, &BoxConstraints::tight(Size::new(300.0, 400.0)));
let fab = w.fab.as_ref().unwrap();
assert_eq!(
fab.origin(),
Point::new(300.0 - 16.0 - 56.0, 400.0 - 16.0 - 56.0)
);
}
#[test]
fn fab_floats_above_a_present_bottom_bar_with_no_double_inset() {
let view: ScaffoldView<()> = scaffold(leaf(50.0, 50.0))
.bottom_bar(any(leaf(0.0, 48.0)))
.fab(any(leaf(56.0, 56.0)));
let mut w = build(&view);
let mut lctx = LayoutCtx::new();
w.layout(&mut lctx, &BoxConstraints::tight(Size::new(300.0, 400.0)));
let fab = w.fab.as_ref().unwrap();
assert_eq!(
fab.origin(),
Point::new(300.0 - 16.0 - 56.0, 400.0 - 48.0 - 16.0 - 56.0)
);
}
#[test]
fn all_four_slots_together() {
let view: ScaffoldView<()> = scaffold(leaf(50.0, 50.0))
.app_bar(any(leaf(0.0, 56.0)))
.bottom_bar(any(leaf(0.0, 48.0)))
.fab(any(leaf(56.0, 56.0)));
let mut w = build(&view);
let mut lctx = LayoutCtx::new();
w.layout(&mut lctx, &BoxConstraints::tight(Size::new(300.0, 400.0)));
assert_eq!(w.body.origin(), Point::new(0.0, 56.0));
assert_eq!(w.body.size(), Size::new(300.0, 400.0 - 56.0 - 48.0));
assert_eq!(
w.bottom_bar.as_ref().unwrap().origin(),
Point::new(0.0, 400.0 - 48.0)
);
assert_eq!(
w.fab.as_ref().unwrap().origin(),
Point::new(300.0 - 16.0 - 56.0, 400.0 - 48.0 - 16.0 - 56.0)
);
}
}