use std::any::Any;
use std::collections::HashMap;
use frust::authoring::text::TextContext;
use frust::authoring::{AnyView, DiscardScene, PaintOutcome, PaintScene, View, WidgetId, any};
use frust::{SizedBox, Theme, column, stack};
use frust_core::{FrameTime, RenderRoot};
use frust_material::overlay::{OverlayAnchor, anchored_overlay, overlay_anchor};
use frust_material::{
dropdown, dropdown_field, dropdown_item, expandable_item, expandable_list, search_view,
};
use kurbo::{Point, Size};
const FRAME_NS: u64 = 16_666_667;
type Geometry = HashMap<WidgetId, (&'static str, Point, Size)>;
fn geometry<S: 'static>(root: &RenderRoot<S, AnyView<S>>) -> Geometry {
let nodes = root.inspect();
let origins: HashMap<WidgetId, Point> =
nodes.iter().map(|n| (n.id, n.bounds.origin())).collect();
nodes
.into_iter()
.map(|n| {
let parent = n
.parent
.and_then(|p| origins.get(&p).copied())
.unwrap_or(Point::ZERO);
let origin = Point::new(n.bounds.x0 - parent.x, n.bounds.y0 - parent.y);
(n.id, (n.type_name, origin, n.bounds.size()))
})
.collect()
}
type ViewFn<S> = Box<dyn FnMut(&mut S) -> AnyView<S>>;
struct Audit<S: 'static> {
root: RenderRoot<S, AnyView<S>>,
view: ViewFn<S>,
state: S,
text: TextContext,
window: Size,
now: u64,
offenders: Vec<&'static str>,
}
impl<S: 'static> Audit<S> {
fn new(state: S, theme: Theme, view: impl FnMut(&mut S) -> AnyView<S> + 'static) -> Self {
let mut audit = Audit {
root: RenderRoot::new(),
view: Box::new(view),
state,
text: TextContext::new(),
window: Size::new(400.0, 600.0),
now: 0,
offenders: Vec::new(),
};
audit.root.set_theme(Box::new(theme));
audit
}
fn layout(&mut self) {
self.root.set_layout_audit(false);
self.root
.layout_with_text(self.window, &mut self.text as &mut dyn Any);
let skipped = geometry(&self.root);
self.root.set_layout_audit(true);
self.root
.layout_with_text(self.window, &mut self.text as &mut dyn Any);
for (id, (name, origin, size)) in geometry(&self.root) {
if let Some((_, was_origin, was_size)) = skipped.get(&id) {
let moved = (origin - *was_origin).hypot() > 1e-3
|| (size.width - was_size.width).abs() > 1e-3
|| (size.height - was_size.height).abs() > 1e-3;
if moved {
self.offenders.push(name);
}
}
}
}
fn paint(&mut self, scene: &mut dyn PaintScene) -> PaintOutcome {
let outcome = self.root.paint(scene, FrameTime::from_nanos(self.now));
self.now += FRAME_NS;
outcome
}
fn frame(&mut self) -> PaintOutcome {
self.root.rebuild(&mut self.view, &mut self.state);
self.layout();
self.paint(&mut DiscardScene)
}
fn tick(&mut self) -> PaintOutcome {
self.layout();
self.paint(&mut DiscardScene)
}
fn settle(&mut self) {
for _ in 0..600 {
if !self.tick().needs_frame {
break;
}
}
self.tick();
self.tick();
}
fn assert_clean(&self, scenario: &str) {
assert!(
self.offenders.is_empty(),
"{scenario}: stale layout under skipping in {:?}",
self.offenders
);
assert_eq!(
self.root.layout_audit_mismatches(),
0,
"{scenario}: the layout audit reported mismatches"
);
}
}
fn light() -> Theme {
frust_material::baseline()
}
fn reduced() -> Theme {
let mut theme = frust_material::baseline();
theme.motion.reduce_motion = true;
theme
}
fn both_motions() -> [Theme; 2] {
[light(), reduced()]
}
#[derive(Default)]
struct Picker {
field_width: f64,
}
fn picker(anchor: &OverlayAnchor, state: &Picker) -> impl View<Picker> + use<> {
let items = || {
vec![
dropdown_item("Apple", "apple"),
dropdown_item("Banana", "banana"),
dropdown_item("Cherry", "cherry"),
]
};
stack()
.child(
SizedBox(Some(state.field_width), Some(56.0))
.child(dropdown_field::<Picker>(items()).anchor(anchor).open(true)),
)
.child(
dropdown(items(), |_: &mut Picker, _| {})
.anchor(anchor)
.open(true),
)
}
#[test]
fn a_dropdown_panel_follows_a_field_that_resizes() {
for theme in both_motions() {
let anchor = OverlayAnchor::new();
let mut audit = Audit::new(
Picker { field_width: 180.0 },
theme,
move |state: &mut Picker| any(picker(&anchor, state)),
);
audit.frame();
audit.settle();
for width in [300.0, 220.0, 360.0] {
audit.state.field_width = width;
audit.frame();
audit.settle();
}
audit.assert_clean("dropdown panel over a resizing field");
}
}
#[derive(Default)]
struct Docked {
bar_width: f64,
drop: f64,
}
fn docked(anchor: &OverlayAnchor, state: &Docked) -> AnyView<Docked> {
any(stack()
.child(
column()
.child(SizedBox(Some(10.0), Some(state.drop)))
.child(overlay_anchor(
anchor,
SizedBox(Some(state.bar_width), Some(56.0)),
)),
)
.child(
search_view(String::new(), |_: &mut Docked, _| {})
.docked(anchor)
.open(true),
))
}
fn hosted(anchor: &OverlayAnchor, state: &Docked) -> AnyView<Docked> {
any(stack()
.child(
column()
.child(SizedBox(Some(10.0), Some(state.drop)))
.child(overlay_anchor(
anchor,
SizedBox(Some(state.bar_width), Some(40.0)),
)),
)
.child(anchored_overlay(SizedBox(Some(160.0), Some(80.0))).anchor(anchor)))
}
type Scene = fn(&OverlayAnchor, &Docked) -> AnyView<Docked>;
#[test]
fn anchored_surfaces_follow_their_anchor() {
let scenes: [(&str, Scene); 2] = [("docked search view", docked), ("anchored host", hosted)];
for (name, scene) in scenes {
for theme in both_motions() {
let anchor = OverlayAnchor::new();
let mut audit = Audit::new(
Docked {
bar_width: 370.0,
drop: 0.0,
},
theme,
move |state: &mut Docked| scene(&anchor, state),
);
audit.frame();
audit.settle();
for (bar_width, drop) in [(395.0, 0.0), (395.0, 60.0), (380.0, 20.0)] {
audit.state.bar_width = bar_width;
audit.state.drop = drop;
audit.frame();
audit.settle();
}
audit.assert_clean(name);
}
}
}
#[derive(Default)]
struct Sections {
expanded: Vec<usize>,
}
fn sections(state: &mut Sections) -> AnyView<Sections> {
any(column()
.child(
expandable_list([
expandable_item(
SizedBox(Some(240.0), Some(48.0)),
SizedBox(Some(240.0), Some(120.0)),
),
expandable_item(
SizedBox(Some(240.0), Some(48.0)),
SizedBox(Some(240.0), Some(60.0)),
),
])
.expanded(state.expanded.clone()),
)
.child(SizedBox(Some(100.0), Some(20.0))))
}
#[test]
fn an_expandable_list_reveal_stays_laid_out() {
for theme in both_motions() {
let mut audit = Audit::new(Sections::default(), theme, sections);
audit.frame();
audit.settle();
for expanded in [vec![0], vec![0, 1], vec![1], vec![]] {
audit.state.expanded = expanded;
audit.frame();
audit.settle();
}
audit.assert_clean("expandable list");
}
}