use frust_core::{
BoxConstraints, BuildCtx, ChangeFlags, InspectNode, LayoutCtx, PaintCtx, PaintScene,
RenderRoot, View, Widget, WindowEdgeInsets, WindowInsets, any,
};
use frust_widgets::{Alignment, ScaffoldView, scaffold};
use kurbo::{Point, Size};
struct Body {
intrinsic: Size,
}
fn body(width: f64, height: f64) -> Body {
Body {
intrinsic: Size::new(width, height),
}
}
struct BodyWidget {
intrinsic: Size,
}
impl View<()> for Body {
type Element = BodyWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> BodyWidget {
BodyWidget {
intrinsic: self.intrinsic,
}
}
fn rebuild(&self, prev: &Self, el: &mut BodyWidget, _ctx: &mut BuildCtx<'_>) -> ChangeFlags {
if prev.intrinsic != self.intrinsic {
el.intrinsic = self.intrinsic;
ChangeFlags::LAYOUT
} else {
ChangeFlags::NONE
}
}
}
impl Widget for BodyWidget {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.constrain(self.intrinsic)
}
fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
}
struct Bar {
height: f64,
}
fn bar(height: f64) -> Bar {
Bar { height }
}
struct BarWidget {
height: f64,
}
impl View<()> for Bar {
type Element = BarWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> BarWidget {
BarWidget {
height: self.height,
}
}
fn rebuild(&self, prev: &Self, el: &mut BarWidget, _ctx: &mut BuildCtx<'_>) -> ChangeFlags {
if prev.height != self.height {
el.height = self.height;
ChangeFlags::LAYOUT
} else {
ChangeFlags::NONE
}
}
}
impl Widget for BarWidget {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
let width = if bc.max().width.is_finite() {
bc.max().width
} else {
0.0
};
bc.constrain(Size::new(width, self.height))
}
fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
}
struct Fab {
intrinsic: Size,
}
fn fab_leaf(width: f64, height: f64) -> Fab {
Fab {
intrinsic: Size::new(width, height),
}
}
struct FabWidget {
intrinsic: Size,
}
impl View<()> for Fab {
type Element = FabWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> FabWidget {
FabWidget {
intrinsic: self.intrinsic,
}
}
fn rebuild(&self, prev: &Self, el: &mut FabWidget, _ctx: &mut BuildCtx<'_>) -> ChangeFlags {
if prev.intrinsic != self.intrinsic {
el.intrinsic = self.intrinsic;
ChangeFlags::LAYOUT
} else {
ChangeFlags::NONE
}
}
}
impl Widget for FabWidget {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.constrain(self.intrinsic)
}
fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
}
#[derive(Clone, Copy, PartialEq)]
enum InsetEdge {
Top,
Bottom,
}
struct SelfInsetBar {
content_height: f64,
edge: InsetEdge,
}
fn self_inset_bar(content_height: f64, edge: InsetEdge) -> SelfInsetBar {
SelfInsetBar {
content_height,
edge,
}
}
struct SelfInsetBarWidget {
content_height: f64,
edge: InsetEdge,
}
impl View<()> for SelfInsetBar {
type Element = SelfInsetBarWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> SelfInsetBarWidget {
SelfInsetBarWidget {
content_height: self.content_height,
edge: self.edge,
}
}
fn rebuild(
&self,
prev: &Self,
el: &mut SelfInsetBarWidget,
_ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
if prev.content_height != self.content_height || prev.edge != self.edge {
el.content_height = self.content_height;
el.edge = self.edge;
ChangeFlags::LAYOUT
} else {
ChangeFlags::NONE
}
}
}
impl Widget for SelfInsetBarWidget {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
let inset = match self.edge {
InsetEdge::Top => ctx.window_insets().padding().top,
InsetEdge::Bottom => ctx.window_insets().padding().bottom,
};
let width = if bc.max().width.is_finite() {
bc.max().width
} else {
0.0
};
bc.constrain(Size::new(width, self.content_height + inset))
}
fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
}
fn inspect<V: View<()>>(
build: &mut impl FnMut(&mut ()) -> V,
window: Size,
insets: WindowInsets,
) -> Vec<InspectNode> {
let mut root: RenderRoot<(), V> = RenderRoot::new();
let mut state = ();
root.rebuild(build, &mut state);
root.set_insets(insets);
root.layout(window);
root.inspect()
}
fn two_pass<V: View<()>>(
mut first: impl FnMut(&mut ()) -> V,
mut second: impl FnMut(&mut ()) -> V,
window: Size,
insets: WindowInsets,
) -> (Vec<InspectNode>, Vec<InspectNode>, ChangeFlags) {
let mut root: RenderRoot<(), V> = RenderRoot::new();
let mut state = ();
root.rebuild(&mut first, &mut state);
root.set_insets(insets);
root.layout(window);
let before = root.inspect();
let flags = root.rebuild(&mut second, &mut state);
root.layout(window);
let after = root.inspect();
(before, after, flags)
}
fn short_names(nodes: &[InspectNode]) -> Vec<&str> {
nodes
.iter()
.map(|n| {
let bare = n.type_name.split('<').next().unwrap_or(n.type_name);
bare.rsplit("::").next().unwrap_or(bare)
})
.collect()
}
fn find<'a>(nodes: &'a [InspectNode], name: &str) -> &'a InspectNode {
nodes
.iter()
.find(|n| {
let bare = n.type_name.split('<').next().unwrap_or(n.type_name);
bare.rsplit("::").next().unwrap_or(bare) == name
})
.unwrap_or_else(|| panic!("no {name} node found; nodes were {:?}", short_names(nodes)))
}
#[test]
fn body_only_scaffold_has_no_optional_slot_nodes() {
let nodes = inspect(
&mut |_: &mut ()| -> ScaffoldView<()> { scaffold(body(50.0, 50.0)) },
Size::new(300.0, 400.0),
WindowInsets::default(),
);
assert_eq!(short_names(&nodes), vec!["ScaffoldWidget", "BodyWidget"]);
assert_eq!(nodes[0].children.len(), 1);
let body_node = find(&nodes, "BodyWidget");
assert_eq!(body_node.bounds.origin(), Point::ZERO);
assert_eq!(body_node.bounds.width(), 300.0);
assert_eq!(body_node.bounds.height(), 400.0);
}
#[test]
fn all_four_slots_are_present_sized_and_positioned_consistently() {
let nodes = inspect(
&mut |_: &mut ()| -> ScaffoldView<()> {
scaffold(body(0.0, 0.0))
.app_bar(any(bar(56.0)))
.bottom_bar(any(bar(48.0)))
.fab(any(fab_leaf(56.0, 56.0)))
},
Size::new(300.0, 400.0),
WindowInsets::default(),
);
assert_eq!(
short_names(&nodes),
vec![
"ScaffoldWidget",
"BodyWidget",
"BarWidget",
"BarWidget",
"FabWidget"
]
);
let body_node = &nodes[1];
let app_bar_node = &nodes[2];
let bottom_bar_node = &nodes[3];
let fab_node = &nodes[4];
assert_eq!(app_bar_node.bounds.origin(), Point::ZERO);
assert_eq!(app_bar_node.bounds.height(), 56.0);
assert_eq!(body_node.bounds.origin(), Point::new(0.0, 56.0));
assert_eq!(body_node.bounds.width(), 300.0);
assert_eq!(body_node.bounds.height(), 400.0 - 56.0 - 48.0);
assert_eq!(
bottom_bar_node.bounds.origin(),
Point::new(0.0, 400.0 - 48.0)
);
assert_eq!(bottom_bar_node.bounds.height(), 48.0);
assert_eq!(
fab_node.bounds.origin(),
Point::new(300.0 - 16.0 - 56.0, 400.0 - 48.0 - 16.0 - 56.0)
);
}
#[test]
fn fab_alignment_moves_the_fab_within_its_box() {
let nodes = inspect(
&mut |_: &mut ()| -> ScaffoldView<()> {
scaffold(body(0.0, 0.0))
.fab(any(fab_leaf(56.0, 56.0)))
.fab_alignment(Alignment::TOP_LEFT)
},
Size::new(300.0, 400.0),
WindowInsets::default(),
);
let fab_node = find(&nodes, "FabWidget");
assert_eq!(fab_node.bounds.origin(), Point::new(16.0, 16.0));
}
#[test]
fn r_b4_inset_a_self_insetting_app_bar_is_never_pre_inset_by_the_scaffold() {
let insets = WindowInsets::new(
WindowEdgeInsets::new(0.0, 44.0, 0.0, 0.0),
WindowEdgeInsets::ZERO,
);
let nodes = inspect(
&mut |_: &mut ()| -> ScaffoldView<()> {
scaffold(body(0.0, 0.0)).app_bar(any(self_inset_bar(56.0, InsetEdge::Top)))
},
Size::new(300.0, 400.0),
insets,
);
let app_bar_node = find(&nodes, "SelfInsetBarWidget");
let body_node = find(&nodes, "BodyWidget");
assert_eq!(app_bar_node.bounds.height(), 56.0 + 44.0);
assert_eq!(app_bar_node.bounds.origin(), Point::ZERO);
assert_eq!(body_node.bounds.origin().y, 100.0);
assert_eq!(body_node.bounds.origin().y, app_bar_node.bounds.height());
}
#[test]
fn fab_consumes_the_bottom_window_inset_when_no_bottom_bar_is_present() {
let insets = WindowInsets::new(
WindowEdgeInsets::new(0.0, 0.0, 0.0, 34.0),
WindowEdgeInsets::ZERO,
);
let nodes = inspect(
&mut |_: &mut ()| -> ScaffoldView<()> {
scaffold(body(0.0, 0.0)).fab(any(fab_leaf(56.0, 56.0)))
},
Size::new(300.0, 400.0),
insets,
);
let fab_node = find(&nodes, "FabWidget");
assert_eq!(
fab_node.bounds.origin().y,
400.0 - 16.0 - 34.0 - 56.0,
"with no bottom_bar, the fab must consume the window's bottom inset itself"
);
}
#[test]
fn bottom_bar_self_insets_and_the_fab_floats_above_it_without_double_consuming() {
let insets = WindowInsets::new(
WindowEdgeInsets::new(0.0, 0.0, 0.0, 34.0),
WindowEdgeInsets::ZERO,
);
let nodes = inspect(
&mut |_: &mut ()| -> ScaffoldView<()> {
scaffold(body(0.0, 0.0))
.bottom_bar(any(self_inset_bar(48.0, InsetEdge::Bottom)))
.fab(any(fab_leaf(56.0, 56.0)))
},
Size::new(300.0, 400.0),
insets,
);
let bottom_bar_node = find(&nodes, "SelfInsetBarWidget");
let body_node = find(&nodes, "BodyWidget");
let fab_node = find(&nodes, "FabWidget");
let bar_height = 48.0 + 34.0;
assert_eq!(bottom_bar_node.bounds.height(), bar_height);
assert_eq!(
bottom_bar_node.bounds.origin(),
Point::new(0.0, 400.0 - bar_height)
);
assert_eq!(body_node.bounds.height(), 400.0 - bar_height);
assert_eq!(
fab_node.bounds.origin().y,
400.0 - bar_height - 16.0 - 56.0,
"the fab must not double-consume the bottom inset once a bottom_bar has self-inset for it"
);
}
#[test]
fn app_bar_removed_on_rebuild_expands_the_body_to_full_height() {
let (before, after, flags) = two_pass(
|_: &mut ()| -> ScaffoldView<()> {
scaffold(body(0.0, 0.0)).app_bar_opt(Some(any(bar(56.0))))
},
|_: &mut ()| -> ScaffoldView<()> { scaffold(body(0.0, 0.0)).app_bar_opt(None) },
Size::new(300.0, 400.0),
WindowInsets::default(),
);
let body_before = find(&before, "BodyWidget");
assert_eq!(body_before.bounds.origin(), Point::new(0.0, 56.0));
assert_eq!(body_before.bounds.height(), 400.0 - 56.0);
assert_eq!(
short_names(&after),
vec!["ScaffoldWidget", "BodyWidget"],
"the app_bar node must be torn down, not merely emptied"
);
let body_after = find(&after, "BodyWidget");
assert_eq!(body_after.bounds.origin(), Point::ZERO);
assert_eq!(body_after.bounds.height(), 400.0);
assert!(flags.needs_layout());
}
#[test]
fn app_bar_added_on_rebuild_shrinks_the_body_below_it() {
let (before, after, flags) = two_pass(
|_: &mut ()| -> ScaffoldView<()> { scaffold(body(0.0, 0.0)).app_bar_opt(None) },
|_: &mut ()| -> ScaffoldView<()> {
scaffold(body(0.0, 0.0)).app_bar_opt(Some(any(bar(56.0))))
},
Size::new(300.0, 400.0),
WindowInsets::default(),
);
assert_eq!(short_names(&before), vec!["ScaffoldWidget", "BodyWidget"]);
let body_before = find(&before, "BodyWidget");
assert_eq!(body_before.bounds.origin(), Point::ZERO);
assert_eq!(body_before.bounds.height(), 400.0);
let bar_after = find(&after, "BarWidget");
assert_eq!(bar_after.bounds.origin(), Point::ZERO);
assert_eq!(bar_after.bounds.height(), 56.0);
let body_after = find(&after, "BodyWidget");
assert_eq!(body_after.bounds.origin(), Point::new(0.0, 56.0));
assert_eq!(body_after.bounds.height(), 400.0 - 56.0);
assert!(flags.needs_layout());
}
#[test]
fn bottom_bar_removed_on_rebuild_expands_the_body_to_full_height() {
let (before, after, flags) = two_pass(
|_: &mut ()| -> ScaffoldView<()> {
scaffold(body(0.0, 0.0)).bottom_bar_opt(Some(any(bar(48.0))))
},
|_: &mut ()| -> ScaffoldView<()> { scaffold(body(0.0, 0.0)).bottom_bar_opt(None) },
Size::new(300.0, 400.0),
WindowInsets::default(),
);
let body_before = find(&before, "BodyWidget");
assert_eq!(body_before.bounds.origin(), Point::ZERO);
assert_eq!(body_before.bounds.height(), 400.0 - 48.0);
assert_eq!(
short_names(&after),
vec!["ScaffoldWidget", "BodyWidget"],
"the bottom_bar node must be torn down, not merely emptied"
);
let body_after = find(&after, "BodyWidget");
assert_eq!(body_after.bounds.origin(), Point::ZERO);
assert_eq!(body_after.bounds.height(), 400.0);
assert!(flags.needs_layout());
}
#[test]
fn bottom_bar_added_on_rebuild_shrinks_the_body_above_it() {
let (before, after, flags) = two_pass(
|_: &mut ()| -> ScaffoldView<()> { scaffold(body(0.0, 0.0)).bottom_bar_opt(None) },
|_: &mut ()| -> ScaffoldView<()> {
scaffold(body(0.0, 0.0)).bottom_bar_opt(Some(any(bar(48.0))))
},
Size::new(300.0, 400.0),
WindowInsets::default(),
);
let body_before = find(&before, "BodyWidget");
assert_eq!(body_before.bounds.height(), 400.0);
let bar_after = find(&after, "BarWidget");
assert_eq!(bar_after.bounds.height(), 48.0);
assert_eq!(bar_after.bounds.origin(), Point::new(0.0, 400.0 - 48.0));
let body_after = find(&after, "BodyWidget");
assert_eq!(body_after.bounds.origin(), Point::ZERO);
assert_eq!(body_after.bounds.height(), 400.0 - 48.0);
assert!(flags.needs_layout());
}
#[test]
fn fab_appearing_on_rebuild_positions_the_fab_without_disturbing_the_body() {
let (before, after, flags) = two_pass(
|_: &mut ()| -> ScaffoldView<()> { scaffold(body(0.0, 0.0)) },
|_: &mut ()| -> ScaffoldView<()> {
scaffold(body(0.0, 0.0)).fab(any(fab_leaf(56.0, 56.0)))
},
Size::new(300.0, 400.0),
WindowInsets::default(),
);
assert_eq!(short_names(&before), vec!["ScaffoldWidget", "BodyWidget"]);
let body_before = find(&before, "BodyWidget");
assert_eq!(
short_names(&after),
vec!["ScaffoldWidget", "BodyWidget", "FabWidget"]
);
let body_after = find(&after, "BodyWidget");
let fab_after = find(&after, "FabWidget");
assert_eq!(
body_before.bounds, body_after.bounds,
"the body must not move or resize when the fab appears"
);
assert_eq!(
fab_after.bounds.origin(),
Point::new(300.0 - 16.0 - 56.0, 400.0 - 16.0 - 56.0)
);
assert!(flags.needs_layout());
}
#[test]
fn fab_disappearing_on_rebuild_leaves_the_body_undisturbed() {
let (before, after, flags) = two_pass(
|_: &mut ()| -> ScaffoldView<()> {
scaffold(body(0.0, 0.0)).fab(any(fab_leaf(56.0, 56.0)))
},
|_: &mut ()| -> ScaffoldView<()> { scaffold(body(0.0, 0.0)) },
Size::new(300.0, 400.0),
WindowInsets::default(),
);
assert_eq!(
short_names(&before),
vec!["ScaffoldWidget", "BodyWidget", "FabWidget"]
);
let body_before = find(&before, "BodyWidget");
assert_eq!(
short_names(&after),
vec!["ScaffoldWidget", "BodyWidget"],
"the fab node must be torn down, not merely emptied"
);
let body_after = find(&after, "BodyWidget");
assert_eq!(
body_before.bounds, body_after.bounds,
"the body must not move or resize when the fab disappears"
);
assert!(flags.needs_layout());
}
#[test]
fn app_bar_content_swap_with_a_different_height_relayouts_the_body() {
let (before, after, flags) = two_pass(
|_: &mut ()| -> ScaffoldView<()> { scaffold(body(0.0, 0.0)).app_bar(any(bar(56.0))) },
|_: &mut ()| -> ScaffoldView<()> { scaffold(body(0.0, 0.0)).app_bar(any(bar(80.0))) },
Size::new(300.0, 400.0),
WindowInsets::default(),
);
let bar_before = find(&before, "BarWidget");
assert_eq!(bar_before.bounds.height(), 56.0);
let body_before = find(&before, "BodyWidget");
assert_eq!(body_before.bounds.origin(), Point::new(0.0, 56.0));
let bar_after = find(&after, "BarWidget");
assert_eq!(bar_after.bounds.height(), 80.0);
let body_after = find(&after, "BodyWidget");
assert_eq!(body_after.bounds.origin(), Point::new(0.0, 80.0));
assert_eq!(body_after.bounds.height(), 400.0 - 80.0);
assert!(flags.needs_layout());
}
#[test]
fn rebuild_with_unchanged_slots_reports_no_change_flags() {
let (before, after, flags) = two_pass(
|_: &mut ()| -> ScaffoldView<()> {
scaffold(body(50.0, 50.0))
.app_bar_opt(Some(any(bar(56.0))))
.bottom_bar_opt(Some(any(bar(48.0))))
.fab(any(fab_leaf(56.0, 56.0)))
},
|_: &mut ()| -> ScaffoldView<()> {
scaffold(body(50.0, 50.0))
.app_bar_opt(Some(any(bar(56.0))))
.bottom_bar_opt(Some(any(bar(48.0))))
.fab(any(fab_leaf(56.0, 56.0)))
},
Size::new(300.0, 400.0),
WindowInsets::default(),
);
assert_eq!(
before, after,
"no node should move, resize, appear, or disappear when every slot is unchanged"
);
assert!(
flags.is_empty(),
"a rebuild with unchanged slots must report no ChangeFlags at all, got {flags:?}"
);
}