use frust::Theme;
use frust::authoring::{
AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent,
LayoutCtx, PaintCtx, PaintScene, Role, SemanticsCtx, View, ViewSeq, Widget, build_child,
rebuild_children, route_event, teardown_child, visit_children,
};
use kurbo::{Point, Size};
use peniko::Color;
use super::{
AppBarCollapse, AppBarDensity, AppBarShapeFamily, AppBarVariant, GAP, HEADLINE_INSET,
HEADLINE_LINE_HEIGHT, HEADLINE_SIZE, PAD_X, TITLE_LINE_HEIGHT, TITLE_SIZE, TitleSlot,
fill_container, finite_or_zero, lerp, push_bar_semantics, resolve_container, resolve_radius,
title_ref,
};
#[derive(Clone, Copy, Debug, PartialEq)]
enum CollapseInput {
Fraction(f64),
Offset(f64),
}
pub struct SliverAppBarView<State: 'static> {
title: TitleSlot<State>,
leading: Option<AnyView<State>>,
actions: Vec<AnyView<State>>,
variant: AppBarVariant,
density: AppBarDensity,
shape_family: AppBarShapeFamily,
center_title: bool,
corner_shift: bool,
pinned: bool,
collapse: CollapseInput,
background: Option<Color>,
semantic_label: Option<String>,
}
pub fn sliver_app_bar<State: 'static>(title: impl Into<String>) -> SliverAppBarView<State> {
SliverAppBarView {
title: TitleSlot::Text(title.into()),
leading: None,
actions: Vec::new(),
variant: AppBarVariant::Medium,
density: AppBarDensity::Regular,
shape_family: AppBarShapeFamily::Round,
center_title: false,
corner_shift: true,
pinned: true,
collapse: CollapseInput::Fraction(0.0),
background: None,
semantic_label: None,
}
}
#[allow(non_snake_case)]
pub fn SliverAppBar<State: 'static>(title: impl Into<String>) -> SliverAppBarView<State> {
sliver_app_bar(title)
}
impl<State: 'static> SliverAppBarView<State> {
pub fn leading(mut self, leading: impl View<State>) -> Self {
self.leading = Some(AnyView::new(leading));
self
}
pub fn actions<M>(mut self, actions: impl ViewSeq<State, M>) -> Self {
self.actions.clear();
actions.extend_views(&mut self.actions);
self
}
pub fn title_view(mut self, title: impl View<State>) -> Self {
self.title = TitleSlot::View(AnyView::new(title));
self
}
pub fn variant(mut self, variant: AppBarVariant) -> Self {
self.variant = variant;
self
}
pub fn density(mut self, density: AppBarDensity) -> Self {
self.density = density;
self
}
pub fn shape_family(mut self, family: AppBarShapeFamily) -> Self {
self.shape_family = family;
self
}
pub fn center_title(mut self, center: bool) -> Self {
self.center_title = center;
self
}
#[must_use]
pub fn corner_shift(mut self, enabled: bool) -> Self {
self.corner_shift = enabled;
self
}
pub fn pinned(mut self, pinned: bool) -> Self {
self.pinned = pinned;
self
}
pub fn collapse(mut self, collapse: f64) -> Self {
self.collapse = CollapseInput::Fraction(collapse);
self
}
pub fn scroll_offset(mut self, offset: f64) -> Self {
self.collapse = CollapseInput::Offset(offset);
self
}
pub fn background(mut self, color: Color) -> Self {
self.background = Some(color);
self
}
pub fn semantic_label(mut self, label: impl Into<String>) -> Self {
self.semantic_label = Some(label.into());
self
}
fn geometry(&self) -> AppBarCollapse {
AppBarCollapse::new(self.variant, self.density, self.pinned)
}
fn collapse_fraction(&self) -> f64 {
match self.collapse {
CollapseInput::Fraction(t) => super::clamp01(t),
CollapseInput::Offset(offset) => self.geometry().fraction_for_offset(offset),
}
}
fn label(&self) -> Option<&str> {
self.semantic_label
.as_deref()
.or_else(|| self.title.label())
}
}
fn title_type_scale(headline: f64) -> (f32, f32) {
(
lerp(HEADLINE_SIZE as f64, TITLE_SIZE as f64, headline) as f32,
lerp(
HEADLINE_LINE_HEIGHT as f64,
TITLE_LINE_HEIGHT as f64,
headline,
) as f32,
)
}
fn slot_views<'a, State: 'static>(
view: &'a SliverAppBarView<State>,
title: &'a AnyView<State>,
) -> Vec<&'a AnyView<State>> {
let mut views = Vec::with_capacity(2 + view.actions.len());
if let Some(leading) = &view.leading {
views.push(leading);
}
views.push(title);
views.extend(view.actions.iter());
views
}
pub struct SliverAppBarWidget {
slots: Vec<ChildPod>,
has_leading: bool,
center_title: bool,
corner_shift: bool,
geometry: AppBarCollapse,
collapse: f64,
shape_family: AppBarShapeFamily,
background: Option<Color>,
label: Option<String>,
}
impl SliverAppBarWidget {
fn title_index(&self) -> usize {
usize::from(self.has_leading)
}
}
impl<State: 'static> View<State> for SliverAppBarView<State> {
type Element = SliverAppBarWidget;
fn build(&self, ctx: &mut BuildCtx<'_>) -> SliverAppBarWidget {
let geometry = self.geometry();
let collapse = self.collapse_fraction();
let (size, line_height) = title_type_scale(geometry.headline(collapse));
let mut storage = None;
let title = title_ref(&self.title, &mut storage, size, line_height);
let slots = slot_views(self, title)
.into_iter()
.map(|view| build_child(view, ctx))
.collect();
SliverAppBarWidget {
slots,
has_leading: self.leading.is_some(),
center_title: self.center_title,
corner_shift: self.corner_shift,
geometry,
collapse,
shape_family: self.shape_family,
background: self.background,
label: self.label().map(str::to_string),
}
}
fn rebuild(
&self,
prev: &Self,
element: &mut SliverAppBarWidget,
ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
let prev_geometry = prev.geometry();
let prev_collapse = prev.collapse_fraction();
let (prev_size, prev_line) = title_type_scale(prev_geometry.headline(prev_collapse));
let geometry = self.geometry();
let collapse = self.collapse_fraction();
let (size, line_height) = title_type_scale(geometry.headline(collapse));
let mut prev_storage = None;
let mut next_storage = None;
let prev_title = title_ref(&prev.title, &mut prev_storage, prev_size, prev_line);
let next_title = title_ref(&self.title, &mut next_storage, size, line_height);
let mut flags = rebuild_children(
&slot_views(prev, prev_title),
&slot_views(self, next_title),
&mut element.slots,
ctx,
|view: &&AnyView<State>| *view,
|_| None,
);
let has_leading = self.leading.is_some();
if element.has_leading != has_leading {
element.has_leading = has_leading;
flags |= ChangeFlags::LAYOUT;
}
if element.center_title != self.center_title {
element.center_title = self.center_title;
flags |= ChangeFlags::LAYOUT;
}
if element.corner_shift != self.corner_shift {
element.corner_shift = self.corner_shift;
flags |= ChangeFlags::LAYOUT;
}
if element.geometry != geometry || element.collapse != collapse {
element.geometry = geometry;
element.collapse = collapse;
flags |= ChangeFlags::LAYOUT;
}
if element.shape_family != self.shape_family || element.background != self.background {
element.shape_family = self.shape_family;
element.background = self.background;
flags |= ChangeFlags::PAINT;
}
element.label = self.label().map(str::to_string);
flags
}
fn teardown(&self, element: &mut SliverAppBarWidget, ctx: &mut BuildCtx<'_>) {
let geometry = self.geometry();
let (size, line_height) = title_type_scale(geometry.headline(self.collapse_fraction()));
let mut storage = None;
let title = title_ref(&self.title, &mut storage, size, line_height);
for (view, pod) in slot_views(self, title)
.into_iter()
.zip(element.slots.iter_mut())
{
teardown_child(view, pod, ctx);
}
}
}
impl Widget for SliverAppBarWidget {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
let width = finite_or_zero(bc.max().width);
let height = self.geometry.height(self.collapse);
let headline = self.geometry.headline(self.collapse);
let row = self.geometry.collapsed_height();
let row_center = row.min(height) - row / 2.0;
let slot_bc = BoxConstraints::loose(Size::new(f64::INFINITY, row));
let title_index = self.title_index();
let (shift_l, shift_r) = if self.corner_shift {
let corners = ctx.window_insets().corner_insets;
(
if corners.top_left.height > 0.0 {
corners.top_left.width
} else {
0.0
},
if corners.top_right.height > 0.0 {
corners.top_right.width
} else {
0.0
},
)
} else {
(0.0, 0.0)
};
let mut left = PAD_X + shift_l;
if self.has_leading {
let size = self.slots[0].layout_child(ctx, &slot_bc);
self.slots[0].set_origin(Point::new(left, row_center - size.height / 2.0));
left += size.width + GAP;
}
let mut right = width - PAD_X - shift_r;
for pod in self.slots[title_index + 1..].iter_mut().rev() {
let size = pod.layout_child(ctx, &slot_bc);
right -= size.width;
pod.set_origin(Point::new(right, row_center - size.height / 2.0));
right -= GAP;
}
let collapsed_available = (right - left).max(0.0);
let expanded_available = (width - 2.0 * HEADLINE_INSET).max(0.0);
let available = lerp(expanded_available, collapsed_available, headline).max(0.0);
let title_pod = &mut self.slots[title_index];
let title_size =
title_pod.layout_child(ctx, &BoxConstraints::loose(Size::new(available, height)));
let expanded_x = if self.center_title {
(width - title_size.width).max(0.0) / 2.0
} else {
HEADLINE_INSET
};
let collapsed_x = if self.center_title {
left + (collapsed_available - title_size.width).max(0.0) / 2.0
} else {
left
};
let expanded_y = height - HEADLINE_INSET - title_size.height;
let collapsed_y = row_center - title_size.height / 2.0;
title_pod.set_origin(Point::new(
lerp(expanded_x, collapsed_x, headline),
lerp(expanded_y, collapsed_y, headline),
));
bc.constrain(Size::new(width, height))
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
let theme = Theme::from_paint_ctx(ctx);
let origin = ctx.origin();
let size = ctx.size();
let fill = self.background.unwrap_or_else(|| resolve_container(theme));
let radius = resolve_radius(theme, self.shape_family, size.width, size.height);
fill_container(scene, origin, size, radius, fill);
let clipped = size.height + f64::EPSILON < self.geometry.collapsed_height();
if clipped {
scene.push_clip(origin, size);
}
for pod in &mut self.slots {
pod.paint_child(ctx, scene);
}
if clipped {
scene.pop_clip();
}
}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
route_event(&mut self.slots, ctx, event)
}
fn semantics(&self, ctx: &mut SemanticsCtx) {
push_bar_semantics(ctx, Role::TitleBar, self.label.as_deref(), &self.slots);
}
visit_children!(slots);
}
#[cfg(test)]
mod tests {
use super::*;
use frust::authoring::text::TextContext;
use frust::authoring::{CornerInset, CornerInsets, WindowInsets};
use frust_widgets::test_support::leaf_any;
use std::any::Any;
const ROW: f64 = 64.0;
fn ctx(counter: &mut u64) -> BuildCtx<'_> {
BuildCtx::new(counter)
}
fn build(view: &SliverAppBarView<()>) -> SliverAppBarWidget {
let mut counter = 0u64;
View::<()>::build(view, &mut ctx(&mut counter))
}
fn layout(w: &mut SliverAppBarWidget, width: f64) -> Size {
let mut tcx = TextContext::new();
let mut lctx = LayoutCtx::with_resources(Some(&mut tcx as &mut dyn Any), None);
w.layout(
&mut lctx,
&BoxConstraints::loose(Size::new(width, f64::INFINITY)),
)
}
fn layout_with_corners(w: &mut SliverAppBarWidget, width: f64, corners: CornerInsets) -> Size {
let mut tcx = TextContext::new();
let mut lctx = LayoutCtx::with_resources(Some(&mut tcx as &mut dyn Any), None);
let bc = BoxConstraints::loose(Size::new(width, f64::INFINITY));
lctx.with_window_insets(WindowInsets::default().with_corner_insets(corners), |ctx| {
w.layout(ctx, &bc)
})
}
fn top_corners() -> CornerInsets {
CornerInsets::new(
CornerInset::new(44.0, 30.0),
CornerInset::new(52.0, 30.0),
CornerInset::ZERO,
CornerInset::ZERO,
)
}
fn large_at(t: f64) -> (Size, Point, Size) {
let view: SliverAppBarView<()> = sliver_app_bar("Inbox")
.variant(AppBarVariant::Large)
.collapse(t);
let mut w = build(&view);
let size = layout(&mut w, 400.0);
let title = &w.slots[w.title_index()];
(size, title.origin(), title.size())
}
#[derive(Default)]
struct ClipRecorder {
rrects: Vec<(Point, Size, f64, Color)>,
clips: Vec<(Point, Size)>,
pops: u32,
}
impl PaintScene for ClipRecorder {
fn fill_rect(&mut self, _o: Point, _s: Size, _c: Color) {}
fn draw_text(&mut self, _o: Point, _t: &str) {}
fn fill_rounded_rect(&mut self, o: Point, s: Size, radius: f64, color: Color) {
self.rrects.push((o, s, radius, color));
}
fn push_clip(&mut self, origin: Point, size: Size) {
self.clips.push((origin, size));
}
fn pop_clip(&mut self) {
self.pops += 1;
}
}
#[test]
fn corner_shift_moves_the_collapsed_row_slots() {
let view: SliverAppBarView<()> = sliver_app_bar("Inbox")
.variant(AppBarVariant::Large)
.leading(leaf_any(40.0, 40.0))
.actions(vec![leaf_any(24.0, 24.0)])
.collapse(1.0);
let mut w = build(&view);
layout_with_corners(&mut w, 400.0, top_corners());
assert_eq!(w.slots[0].origin().x, PAD_X + 44.0);
let action = w.slots.last().unwrap();
assert_eq!(
action.origin().x + action.size().width,
400.0 - PAD_X - 52.0
);
}
#[test]
fn corner_shift_opt_out_leaves_the_collapsed_row_unshifted() {
let view: SliverAppBarView<()> = sliver_app_bar("Inbox")
.variant(AppBarVariant::Large)
.leading(leaf_any(40.0, 40.0))
.actions(vec![leaf_any(24.0, 24.0)])
.corner_shift(false)
.collapse(1.0);
let mut w = build(&view);
layout_with_corners(&mut w, 400.0, top_corners());
assert_eq!(w.slots[0].origin().x, PAD_X);
let action = w.slots.last().unwrap();
assert_eq!(action.origin().x + action.size().width, 400.0 - PAD_X);
}
#[test]
fn expanded_headline_is_not_shifted() {
let view: SliverAppBarView<()> = sliver_app_bar("Inbox")
.variant(AppBarVariant::Large)
.leading(leaf_any(40.0, 40.0))
.collapse(0.0);
let mut w = build(&view);
layout_with_corners(&mut w, 400.0, top_corners());
let shifted = w.slots[w.title_index()].origin();
let mut w = build(&view);
layout_with_corners(&mut w, 400.0, CornerInsets::ZERO);
let plain = w.slots[w.title_index()].origin();
assert_eq!(shifted.x, HEADLINE_INSET);
assert_eq!(shifted, plain);
}
#[test]
fn the_container_paints_its_own_band_at_both_endpoints() {
let theme = crate::baseline();
for (t, expected) in [(0.0, 152.0), (1.0, ROW)] {
let view: SliverAppBarView<()> = sliver_app_bar("Inbox")
.variant(AppBarVariant::Large)
.collapse(t);
let mut w = build(&view);
let size = layout(&mut w, 400.0);
assert_eq!(size.height, expected);
let mut scene = ClipRecorder::default();
let mut pctx = PaintCtx::new(Point::ZERO, size).with_theme(&theme);
w.paint(&mut pctx, &mut scene);
assert_eq!(scene.rrects[0].1, size, "container band at t={t}");
assert_eq!(scene.rrects[0].2, theme.shape.small, "radius at t={t}");
assert_eq!(
scene.rrects[0].3,
theme.scheme().surface,
"container role at t={t}"
);
}
}
#[test]
fn the_expanded_endpoint_seats_the_headline_at_the_bottom_inset() {
let (size, origin, title) = large_at(0.0);
assert_eq!(size, Size::new(400.0, 152.0));
assert_eq!(origin.x, HEADLINE_INSET);
assert_eq!(origin.y, 152.0 - HEADLINE_INSET - title.height);
assert!(origin.y >= ROW);
}
#[test]
fn the_collapsed_endpoint_seats_the_headline_in_the_top_row() {
let (size, origin, title) = large_at(1.0);
assert_eq!(size, Size::new(400.0, ROW));
assert_eq!(origin.x, PAD_X);
assert_eq!(origin.y, (ROW - title.height) / 2.0);
}
#[test]
fn the_midpoint_interpolates_the_band_and_the_headline_anchor() {
let (expanded_size, expanded_origin, _) = large_at(0.0);
let (mid_size, mid_origin, _) = large_at(0.5);
let (collapsed_size, collapsed_origin, _) = large_at(1.0);
assert_eq!(mid_size.height, (152.0 + ROW) / 2.0);
assert!(mid_size.height < expanded_size.height);
assert!(mid_size.height > collapsed_size.height);
assert!(mid_origin.x > expanded_origin.x.min(collapsed_origin.x));
assert!(mid_origin.x < expanded_origin.x.max(collapsed_origin.x));
assert!(mid_origin.y < expanded_origin.y);
assert!(mid_origin.y > collapsed_origin.y);
}
#[test]
fn a_quarter_and_three_quarter_collapse_stay_inside_the_endpoints() {
for t in [0.25, 0.75] {
let (size, origin, _) = large_at(t);
assert!(size.height > ROW && size.height < 152.0, "band at t={t}");
assert!(origin.x >= HEADLINE_INSET.min(PAD_X), "anchor at t={t}");
assert!(origin.y > 0.0, "anchor at t={t}");
}
let heights: Vec<f64> = [0.0, 0.25, 0.5, 0.75, 1.0]
.into_iter()
.map(|t| large_at(t).0.height)
.collect();
assert!(
heights.windows(2).all(|w| w[0] > w[1]),
"the band shrinks monotonically: {heights:?}"
);
}
#[test]
fn the_headline_type_scale_interpolates_between_the_two_tokens() {
assert_eq!(title_type_scale(0.0), (HEADLINE_SIZE, HEADLINE_LINE_HEIGHT));
assert_eq!(title_type_scale(1.0), (TITLE_SIZE, TITLE_LINE_HEIGHT));
let (mid_size, mid_line) = title_type_scale(0.5);
assert_eq!(mid_size, (HEADLINE_SIZE + TITLE_SIZE) / 2.0);
assert_eq!(mid_line, (HEADLINE_LINE_HEIGHT + TITLE_LINE_HEIGHT) / 2.0);
let (_, _, expanded) = large_at(0.0);
let (_, _, collapsed) = large_at(1.0);
assert!(
expanded.height > collapsed.height,
"expanded title {expanded:?} should be taller than collapsed {collapsed:?}"
);
}
#[test]
fn a_scroll_offset_maps_onto_the_same_geometry_whatever_the_builder_order() {
let early: SliverAppBarView<()> = sliver_app_bar("Inbox")
.scroll_offset(44.0)
.variant(AppBarVariant::Large);
let late: SliverAppBarView<()> = sliver_app_bar("Inbox")
.variant(AppBarVariant::Large)
.scroll_offset(44.0);
assert_eq!(early.collapse_fraction(), late.collapse_fraction());
assert_eq!(early.collapse_fraction(), 0.5);
let mut offset_bar = build(&early);
let offset_size = layout(&mut offset_bar, 400.0);
assert_eq!(offset_size.height, large_at(0.5).0.height);
}
#[test]
fn a_degenerate_collapse_input_never_reaches_the_layout() {
for input in [f64::NAN, -3.0, 7.0, f64::INFINITY] {
let view: SliverAppBarView<()> = sliver_app_bar("Inbox").collapse(input);
let mut w = build(&view);
let size = layout(&mut w, 400.0);
assert!(
size.height.is_finite() && size.height >= ROW,
"collapse {input} produced {size:?}"
);
}
for offset in [f64::NAN, -50.0, f64::INFINITY] {
let view: SliverAppBarView<()> = sliver_app_bar("Inbox").scroll_offset(offset);
let mut w = build(&view);
let size = layout(&mut w, 400.0);
assert!(
size.height.is_finite() && size.height >= ROW,
"offset {offset} produced {size:?}"
);
}
}
#[test]
fn an_unpinned_bar_scrolls_its_row_away_and_clips_while_it_does() {
let view: SliverAppBarView<()> = sliver_app_bar("Inbox")
.pinned(false)
.leading(leaf_any(40.0, 40.0))
.collapse(1.0);
let mut w = build(&view);
let size = layout(&mut w, 400.0);
assert_eq!(size.height, 0.0);
assert!(w.slots[0].origin().y < 0.0);
let mut scene = ClipRecorder::default();
let mut pctx = PaintCtx::new(Point::ZERO, size);
w.paint(&mut pctx, &mut scene);
assert_eq!(scene.clips.len(), 1);
assert_eq!(scene.pops, 1);
let pinned: SliverAppBarView<()> = sliver_app_bar("Inbox")
.leading(leaf_any(40.0, 40.0))
.collapse(1.0);
let mut w = build(&pinned);
let size = layout(&mut w, 400.0);
assert_eq!(size.height, ROW);
let mut scene = ClipRecorder::default();
let mut pctx = PaintCtx::new(Point::ZERO, size);
w.paint(&mut pctx, &mut scene);
assert!(scene.clips.is_empty());
}
#[test]
fn the_small_variant_keeps_its_title_in_the_row_at_every_offset() {
for t in [0.0, 0.5, 1.0] {
let view: SliverAppBarView<()> = sliver_app_bar("Inbox")
.variant(AppBarVariant::Small)
.collapse(t);
let mut w = build(&view);
let size = layout(&mut w, 400.0);
let title = &w.slots[w.title_index()];
assert_eq!(size.height, ROW, "band at t={t}");
assert_eq!(title.origin().x, PAD_X, "anchor at t={t}");
assert_eq!(
title.origin().y,
(ROW - title.size().height) / 2.0,
"anchor at t={t}"
);
}
}
#[test]
fn compact_density_shifts_both_endpoints() {
let view: SliverAppBarView<()> = sliver_app_bar("Inbox")
.variant(AppBarVariant::Medium)
.density(AppBarDensity::Compact)
.collapse(0.0);
let mut w = build(&view);
assert_eq!(layout(&mut w, 400.0).height, 112.0 - 8.0);
let view: SliverAppBarView<()> = sliver_app_bar("Inbox")
.variant(AppBarVariant::Medium)
.density(AppBarDensity::Compact)
.collapse(1.0);
let mut w = build(&view);
assert_eq!(layout(&mut w, 400.0).height, ROW - 8.0);
}
#[test]
fn leading_and_actions_stay_in_the_top_row_while_the_headline_moves() {
let view: SliverAppBarView<()> = sliver_app_bar("Inbox")
.variant(AppBarVariant::Large)
.leading(leaf_any(40.0, 40.0))
.actions(vec![leaf_any(24.0, 24.0), leaf_any(24.0, 24.0)])
.collapse(0.0);
let mut w = build(&view);
layout(&mut w, 400.0);
assert_eq!(w.slots[0].origin().x, PAD_X);
assert_eq!(w.slots[0].origin().y, (ROW - 40.0) / 2.0);
assert_eq!(
w.slots[3].origin().x + w.slots[3].size().width,
400.0 - PAD_X
);
assert_eq!(w.slots[3].origin().y, (ROW - 24.0) / 2.0);
assert!(w.slots[1].origin().y > ROW);
}
#[test]
fn rebuild_adopts_a_new_collapse_value() {
let mut counter = 0u64;
let prev: SliverAppBarView<()> = sliver_app_bar("Inbox").variant(AppBarVariant::Large);
let mut w = View::<()>::build(&prev, &mut ctx(&mut counter));
assert_eq!(w.collapse, 0.0);
let next: SliverAppBarView<()> = sliver_app_bar("Inbox")
.variant(AppBarVariant::Large)
.collapse(1.0);
let flags = View::<()>::rebuild(&next, &prev, &mut w, &mut ctx(&mut counter));
assert!(flags.needs_layout());
assert_eq!(w.collapse, 1.0);
assert_eq!(layout(&mut w, 400.0).height, ROW);
}
#[test]
fn semantics_node_is_titlebar_labelled_with_the_title() {
fn logic(_state: &mut ()) -> SliverAppBarView<()> {
sliver_app_bar("Inbox")
}
let mut root: frust_core::RenderRoot<(), SliverAppBarView<()>> =
frust_core::RenderRoot::new();
let mut state = ();
root.rebuild(&mut logic, &mut state);
let mut tcx = TextContext::new();
root.layout_with_text(Size::new(300.0, 112.0), &mut tcx as &mut dyn Any);
let update = root.semantics();
let (_, node) = update
.nodes
.iter()
.find(|(_, n)| n.role() == Role::TitleBar)
.expect("a TitleBar node is contributed");
assert_eq!(node.label(), Some("Inbox"));
}
}