use super::{test_environment, test_renderer};
use crate::renderer::{HydrolysisRenderer, tree::RenderNode};
use std::cell::{Cell, RefCell};
use std::rc::Rc;
use waterui::{AnyView, Color, Environment, View, ViewExt};
use waterui_graphics::{
EffectRenderer, ViewEffect, ViewEffectContext, ViewEffectInput, ViewEffectOutput,
};
use waterui_layout::container::FixedContainer;
use waterui_layout::{
Layout, Point, ProposalSize, Rect, Size, Spacer, SubView, SubviewPlacement, scroll,
scroll_horizontal, spacer,
};
#[derive(Debug)]
struct Observation {
proposal: ProposalSize,
frames: Vec<Rect>,
}
type Trace = Rc<RefCell<Vec<Observation>>>;
#[derive(Debug)]
struct ProbeLayout {
vertical: bool,
trace: Trace,
}
impl Layout for ProbeLayout {
fn size_that_fits(&self, _proposal: ProposalSize, _children: &[&dyn SubView]) -> Size {
axis_size(self.vertical, 160.0, 20.0)
}
fn place(
&self,
bounds: Rect,
proposal: ProposalSize,
children: &[&dyn SubView],
) -> Vec<SubviewPlacement> {
assert_eq!(children.len(), 2);
let main = if self.vertical {
proposal.height
} else {
proposal.width
};
let extents = if main.is_none() {
[40.0, 120.0]
} else {
[80.0, 80.0]
};
let mut cursor = 0.0;
let placements: Vec<_> = extents
.into_iter()
.map(|extent| {
let origin = if self.vertical {
Point::new(bounds.x(), bounds.y() + cursor)
} else {
Point::new(bounds.x() + cursor, bounds.y())
};
cursor += extent;
let size = axis_size(self.vertical, extent, 20.0);
SubviewPlacement::new(
Rect::new(origin, size),
ProposalSize::new(Some(size.width), Some(size.height)),
)
})
.collect();
self.trace.borrow_mut().push(Observation {
proposal,
frames: placements.iter().map(|placement| placement.frame).collect(),
});
placements
}
}
struct ProbeContent {
vertical: bool,
trace: Trace,
builds: Rc<Cell<usize>>,
}
impl View for ProbeContent {
fn body(self, _env: &Environment) -> impl View {
self.builds.set(self.builds.get() + 1);
FixedContainer::new(
ProbeLayout {
vertical: self.vertical,
trace: self.trace,
},
(Color::srgb_hex("#2563EB"), Color::srgb_hex("#DC2626")),
)
}
}
struct Fixture {
node: RenderNode,
trace: Trace,
builds: Rc<Cell<usize>>,
}
impl Fixture {
fn new<V: View>(
vertical: bool,
wrap: impl FnOnce(ProbeContent) -> V,
renderer: &mut HydrolysisRenderer,
env: &Environment,
) -> Self {
let trace = Rc::new(RefCell::new(Vec::new()));
let builds = Rc::new(Cell::new(0));
let content = ProbeContent {
vertical,
trace: Rc::clone(&trace),
builds: Rc::clone(&builds),
};
Self {
node: RenderNode::build(AnyView::new(wrap(content)), env, renderer),
trace,
builds,
}
}
fn assert_placement(&self, vertical: bool, proposal: ProposalSize, extents: [f32; 2]) {
let trace = self.trace.borrow();
let observation = trace.last().expect("the retained subtree must be placed");
assert_eq!(observation.proposal, proposal);
assert_eq!(
observation
.frames
.iter()
.map(|frame| *frame.size())
.collect::<Vec<_>>(),
extents
.map(|extent| axis_size(vertical, extent, 20.0))
.to_vec(),
);
assert_eq!(self.builds.get(), 1);
}
}
const fn axis_size(vertical: bool, main: f32, cross: f32) -> Size {
if vertical {
Size::new(cross, main)
} else {
Size::new(main, cross)
}
}
fn axis_proposal(vertical: bool, main: Option<f32>, cross: Option<f32>) -> ProposalSize {
if vertical {
ProposalSize::new(cross, main)
} else {
ProposalSize::new(main, cross)
}
}
#[test]
fn equal_bounds_keep_the_selected_proposal_after_other_probes() {
let env = test_environment();
let mut renderer = test_renderer();
let theme = renderer.theme();
for vertical in [false, true] {
let mut fixture = Fixture::new(vertical, |content| content, &mut renderer, &env);
for main in [None, Some(160.0), None] {
for probe in [Some(0.0), Some(80.0), Some(f32::INFINITY)] {
let dimensions = fixture.node.measure(
&mut renderer.state,
&env,
&theme,
axis_proposal(vertical, probe, Some(20.0)),
);
assert_eq!(dimensions.size, axis_size(vertical, 160.0, 20.0));
}
fixture.trace.borrow_mut().clear();
let proposal = axis_proposal(vertical, main, Some(20.0));
fixture.node.layout(
&mut renderer,
&env,
proposal,
axis_size(vertical, 160.0, 20.0),
);
fixture.assert_placement(
vertical,
proposal,
if main.is_none() {
[40.0, 120.0]
} else {
[80.0, 80.0]
},
);
}
}
}
#[test]
fn retained_scene_capture_preserves_proposal_and_viewport_boundaries() {
use crate::renderer::{LazyViewport, RenderContext, tree::RetainedSubview};
use vello::kurbo::{Affine, Rect as SceneRect};
let env = test_environment();
let mut renderer = test_renderer();
let trace = Rc::new(RefCell::new(Vec::new()));
let mut retained = RetainedSubview::new(AnyView::new(ProbeContent {
vertical: false,
trace: trace.clone(),
builds: Rc::new(Cell::new(0)),
}));
let size = Size::new(160.0, 20.0);
let rect = SceneRect::new(0.0, 0.0, 160.0, 20.0);
let ctx = RenderContext::with_transforms(rect, Affine::IDENTITY, Affine::IDENTITY);
let ideal = ProposalSize::new(None, Some(20.0));
retained.flush_in_rect(&mut renderer, ctx, &env, ideal, rect);
let outer = LazyViewport {
bounds: SceneRect::new(0.0, 800.0, 160.0, 820.0),
transform: Affine::translate((0.0, -800.0)),
};
renderer.push_lazy_viewport(outer);
let _ = retained.render_built_scene(&mut renderer, &env, size);
assert_eq!(renderer.lazy.lazy_viewport_stack.len(), 1);
assert_eq!(renderer.lazy.lazy_viewport_stack[0].bounds, outer.bounds);
trace.borrow_mut().clear();
retained.flush_in_rect(&mut renderer, ctx, &env, ideal, rect);
assert_eq!(
trace.borrow().last().expect("offer changed").proposal,
ideal
);
renderer.pop_lazy_viewport("test outer viewport");
}
#[test]
fn scroll_preserves_its_unconstrained_content_axis() {
let env = test_environment();
let mut renderer = test_renderer();
for vertical in [false, true] {
let mut fixture = Fixture::new(
vertical,
|content| {
if vertical {
scroll(content)
} else {
scroll_horizontal(content)
}
},
&mut renderer,
&env,
);
let size = axis_size(vertical, 160.0, 40.0);
fixture.node.layout(
&mut renderer,
&env,
ProposalSize::new(Some(size.width), Some(size.height)),
size,
);
fixture.assert_placement(
vertical,
axis_proposal(vertical, None, Some(40.0)),
[40.0, 120.0],
);
}
}
#[test]
fn transparent_metadata_preserves_the_selected_proposal() {
let env = test_environment();
let mut renderer = test_renderer();
let mut fixture = Fixture::new(
false,
|content| content.opacity(0.5).layout_priority(7),
&mut renderer,
&env,
);
let proposal = axis_proposal(false, None, Some(20.0));
fixture
.node
.layout(&mut renderer, &env, proposal, axis_size(false, 160.0, 20.0));
fixture.assert_placement(false, proposal, [40.0, 120.0]);
}
#[derive(Debug)]
struct PriorityProbe(Rc<Cell<Option<i32>>>);
impl Layout for PriorityProbe {
fn size_that_fits(&self, proposal: ProposalSize, children: &[&dyn SubView]) -> Size {
children[0].measure(proposal).size
}
fn place(
&self,
bounds: Rect,
proposal: ProposalSize,
children: &[&dyn SubView],
) -> Vec<SubviewPlacement> {
self.0.set(Some(children[0].priority()));
vec![SubviewPlacement::new(
Rect::new(
Point::new(bounds.x(), bounds.y()),
children[0].measure(proposal).size,
),
proposal,
)]
}
}
#[test]
fn spacer_default_priority_survives_wrappers_and_explicit_overrides() {
let env = test_environment();
let mut renderer = test_renderer();
for explicit in [None, Some(0), Some(7)] {
let priority = Rc::new(Cell::new(None));
let gap = spacer().opacity(0.5);
let content = match explicit {
Some(value) => AnyView::new(gap.layout_priority(value)),
None => AnyView::new(gap),
};
let mut node = RenderNode::build(
AnyView::new(FixedContainer::new(
PriorityProbe(Rc::clone(&priority)),
(content,),
)),
&env,
&mut renderer,
);
node.layout(
&mut renderer,
&env,
ProposalSize::new(Some(160.0), Some(20.0)),
Size::new(160.0, 20.0),
);
assert_eq!(
priority.get(),
Some(explicit.unwrap_or(Spacer::DEFAULT_LAYOUT_PRIORITY))
);
}
}
struct LayoutOnlyEffect;
impl EffectRenderer for LayoutOnlyEffect {
fn setup(&mut self, _ctx: &ViewEffectContext) -> impl std::future::Future<Output = ()> {
std::future::ready(())
}
fn render(&mut self, _input: &ViewEffectInput, _output: &ViewEffectOutput) {
panic!("layout-only verification must not invoke effect rendering");
}
}
#[test]
fn view_effect_relayouts_equal_bounds_with_a_new_proposal() {
let env = test_environment();
let mut renderer = test_renderer();
let mut fixture = Fixture::new(
false,
|content| ViewEffect::new(content, LayoutOnlyEffect),
&mut renderer,
&env,
);
for main in [None, Some(160.0), None] {
fixture.trace.borrow_mut().clear();
let proposal = axis_proposal(false, main, Some(20.0));
fixture
.node
.layout(&mut renderer, &env, proposal, Size::new(160.0, 20.0));
fixture.assert_placement(
false,
proposal,
if main.is_none() {
[40.0, 120.0]
} else {
[80.0, 80.0]
},
);
}
}
#[test]
fn nested_collections_preserve_intrinsic_cross_axes() {
use waterui_core::id::SelfId;
use waterui_layout::stack::{HStack, VStack};
let env = test_environment();
let mut renderer = test_renderer();
let grid = VStack::for_each((0..16).map(SelfId::new).collect::<Vec<_>>(), |_| {
HStack::for_each((0..10).map(SelfId::new).collect::<Vec<_>>(), |_| {
Color::srgb_hex("#2563EB").size(28.0, 18.0)
})
.spacing(4.0)
})
.spacing(4.0);
let node = RenderNode::build(AnyView::new(grid), &env, &mut renderer);
let theme = renderer.theme();
for width in [None, Some(0.0), Some(400.0), None] {
let size = node
.measure(
&mut renderer.state,
&env,
&theme,
ProposalSize::new(width, None),
)
.size;
assert_eq!(size, Size::new(316.0, 348.0));
}
}