Files
iris/src/layout_tests.rs
T

1160 lines
36 KiB
Rust

use crate::prelude::*;
use std::{cell::Cell, cell::RefCell, rc::Rc};
pub(crate) struct TestRsc {
pub(crate) ui: Ui,
}
impl UiRsc for TestRsc {
fn ui(&self) -> &Ui {
&self.ui
}
fn ui_mut(&mut self) -> &mut Ui {
&mut self.ui
}
}
struct FixedRect(f32);
impl Widget for FixedRect {
fn draw(&mut self, painter: &mut Painter) {
let size = Size::from_axis(Axis::Y, Len::abs(self.0), Len::REST);
let paint = painter.paint(&PaintId::WHITE);
painter.primitive_within(
RectPrimitive::color(paint),
size.to_uivec2(painter.density())
.align(RegionAlign::TOP_LEFT),
);
painter.set_size(size);
}
}
struct ChildOffset {
child: StrongWidget,
offset: Vec2,
}
impl Widget for ChildOffset {
fn draw(&mut self, painter: &mut Painter) {
painter.set_child_offset(self.offset);
let size = painter.widget(&self.child).size();
painter.set_size(size);
}
}
struct TracedLeaf {
height: f32,
trace: Rc<RefCell<Vec<&'static str>>>,
}
impl Widget for TracedLeaf {
fn draw(&mut self, painter: &mut Painter) {
self.trace.borrow_mut().push("child");
painter.set_size(Size::from_axis(Axis::Y, Len::abs(self.height), Len::REST));
}
}
struct TracedParent {
child: StrongWidget,
reads_child_size: bool,
trace: Rc<RefCell<Vec<&'static str>>>,
}
struct CountedLeaf {
height: f32,
draws: Rc<Cell<u32>>,
}
impl Widget for CountedLeaf {
fn draw(&mut self, painter: &mut Painter) {
self.draws.set(self.draws.get() + 1);
painter.set_size(Size::from_axis(Axis::Y, Len::abs(self.height), Len::REST));
}
}
impl Widget for TracedParent {
fn draw(&mut self, painter: &mut Painter) {
self.trace.borrow_mut().push("parent");
let child = painter.widget(&self.child);
let size = if self.reads_child_size {
child.size()
} else {
Size::REST
};
painter.set_size(size);
}
}
#[test]
fn a_widget_retains_its_entry_layer_not_its_child_cursor() {
let mut rsc = TestRsc { ui: Ui::default() };
let back = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
let front = rsc.ui.widgets.add_strong(Rect::new(PaintId::RED));
let stack = rsc.ui.widgets.add_strong(Stack {
children: vec![back.any(), front.any()],
size: StackSize::Default,
});
let stack_id = stack.id();
let outer = rsc.ui.widgets.add_strong(Sized {
inner: stack.any(),
x: None,
y: None,
});
let outer_weak = outer.weak();
let root = outer.any();
let mut render = UiRenderState::new();
render.resize((100.0, 100.0));
render.update(&root, &mut rsc);
assert_eq!(
render.active[&stack_id].layer, 0,
"Stack entered on layer 0; layers 1 and 2 belong only to its children"
);
render.take_counters();
rsc.ui.widgets.get_mut(&outer_weak).unwrap().x = None;
render.update(&root, &mut rsc);
assert_eq!(
render.take_counters().draws,
1,
"redrawing the parent should retain the unchanged Stack subtree"
);
}
#[test]
fn a_size_dependent_parent_is_invalidated_before_layout_runs_downward() {
let mut rsc = TestRsc { ui: Ui::default() };
let trace = Rc::new(RefCell::new(Vec::new()));
let child = rsc.ui.widgets.add_strong(TracedLeaf {
height: 20.0,
trace: trace.clone(),
});
let child_weak = child.weak();
let parent = rsc.ui.widgets.add_strong(TracedParent {
child: child.any(),
reads_child_size: true,
trace: trace.clone(),
});
let parent_weak = parent.weak();
let root = parent.any();
let mut render = UiRenderState::new();
render.resize((100.0, 100.0));
render.update(&root, &mut rsc);
trace.borrow_mut().clear();
rsc.ui.widgets.get_mut(&child_weak).unwrap().height = 40.0;
render.update(&root, &mut rsc);
assert_eq!(&*trace.borrow(), &["parent", "child"]);
assert_eq!(render.active[&parent_weak.id()].size.y, Len::abs(40.0));
}
#[test]
fn a_parent_that_ignores_child_size_is_not_invalidated_with_it() {
let mut rsc = TestRsc { ui: Ui::default() };
let trace = Rc::new(RefCell::new(Vec::new()));
let child = rsc.ui.widgets.add_strong(TracedLeaf {
height: 20.0,
trace: trace.clone(),
});
let child_weak = child.weak();
let parent = rsc.ui.widgets.add_strong(TracedParent {
child: child.any(),
reads_child_size: false,
trace: trace.clone(),
});
let root = parent.any();
let mut render = UiRenderState::new();
render.resize((100.0, 100.0));
render.update(&root, &mut rsc);
trace.borrow_mut().clear();
rsc.ui.widgets.get_mut(&child_weak).unwrap().height = 40.0;
render.update(&root, &mut rsc);
assert_eq!(&*trace.borrow(), &["child"]);
}
#[test]
fn a_span_reuses_unchanged_sibling_sizes_when_only_its_along_extent_changes() {
let mut rsc = TestRsc { ui: Ui::default() };
let changed_draws = Rc::new(Cell::new(0));
let sibling_draws = Rc::new(Cell::new(0));
let changed = rsc.ui.widgets.add_strong(CountedLeaf {
height: 20.0,
draws: changed_draws.clone(),
});
let changed_weak = changed.weak();
let sibling = rsc.ui.widgets.add_strong(CountedLeaf {
height: 20.0,
draws: sibling_draws.clone(),
});
let span = rsc.ui.widgets.add_strong(Span {
children: vec![changed.any(), sibling.any()],
dir: Dir::DOWN,
gap: Len::ZERO,
});
let root = rsc
.ui
.widgets
.add_strong(Sized {
inner: span.any(),
x: None,
y: None,
})
.any();
let mut render = UiRenderState::new();
render.resize((100.0, 100.0));
render.update(&root, &mut rsc);
changed_draws.set(0);
sibling_draws.set(0);
rsc.ui.widgets.get_mut(&changed_weak).unwrap().height = 40.0;
render.update(&root, &mut rsc);
assert!(changed_draws.get() > 0, "the changed child was not redrawn");
assert_eq!(
sibling_draws.get(),
0,
"a vertical size change redrew a sibling whose width stayed fixed"
);
}
#[test]
fn a_child_coordinate_offset_moves_only_the_child_subtree() {
let mut rsc = TestRsc { ui: Ui::default() };
let child = rsc.ui.widgets.add_strong(FixedRect(40.0));
let child_weak = child.weak();
let parent = rsc.ui.widgets.add_strong(ChildOffset {
child: child.any(),
offset: vec2(0.0, 15.0),
});
let parent_weak = parent.weak();
let outer = rsc.ui.widgets.add_strong(Sized {
inner: parent.any(),
x: None,
y: None,
});
let outer_weak = outer.weak();
let root = outer.any();
let mut render = UiRenderState::new();
render.resize((100.0, 100.0));
render.update(&root, &mut rsc);
render.take_counters();
let parent_before = render.window_region(&parent_weak, &rsc).unwrap();
let child_before = render.window_region(&child_weak, &rsc).unwrap();
assert!((parent_before.top_left.y - 0.0).abs() < 0.01);
assert!((child_before.top_left.y - 15.0).abs() < 0.01);
rsc.ui.widgets.get_mut(&parent_weak).unwrap().offset.y = 35.0;
rsc.ui.widgets.get_mut(&outer_weak).unwrap().x = None;
render.update(&root, &mut rsc);
let RenderCounters {
draws,
region_rewrites: rewrites,
moves,
..
} = render.take_counters();
let parent_after = render.window_region(&parent_weak, &rsc).unwrap();
let child_after = render.window_region(&child_weak, &rsc).unwrap();
assert_eq!((draws, rewrites, moves), (2, 0, 1));
assert_eq!(
parent_after, parent_before,
"the container itself stays put"
);
assert!((child_after.top_left.y - 35.0).abs() < 0.01);
}
#[test]
fn a_hinted_rest_draws_once_and_only_moves_the_fixed_child_after_it() {
let mut rsc = TestRsc { ui: Ui::default() };
let first = rsc.ui.widgets.add_strong(FixedRect(40.0));
let fill = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
let fill = rsc.ui.widgets.add_strong(Sized {
inner: fill.any(),
x: None,
y: Some(Len::REST),
});
let last = rsc.ui.widgets.add_strong(FixedRect(40.0));
let last_w = last.weak();
let mut span = Span::empty(Dir::DOWN);
span.push(first.any());
span.push(fill.any());
span.push(last.any());
let root = rsc.ui.widgets.add_strong(span).any();
let mut render = UiRenderState::new();
render.resize((200.0, 300.0));
render.update(&root, &mut rsc);
let RenderCounters { draws, moves, .. } = render.take_counters();
assert_eq!(draws, 5);
assert_eq!(moves, 1);
let last = render.window_region(&last_w, &rsc).unwrap();
assert!((last.top_left.y - 260.0).abs() < 0.01, "{last:?}");
}
fn scrolled_rects(
rsc: &mut TestRsc,
n: usize,
) -> (WeakWidget<ScrollArea>, StrongWidget, Vec<WeakWidget<Rect>>) {
let mut span = Span::empty(Dir::DOWN);
let mut rects = Vec::with_capacity(n);
for _ in 0..n {
let rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
rects.push(rect.weak());
let row = rsc.ui.widgets.add_strong(Sized {
inner: rect.any(),
x: None,
y: Some(Len::abs(10.0)),
});
span.push(row.any());
}
let span = rsc.ui.widgets.add_strong(span);
let scroll = rsc
.ui
.widgets
.add_strong(ScrollArea::new(span.any(), Axis::Y, Pin::Start));
let weak = scroll.weak();
(weak, scroll.any(), rects)
}
#[test]
fn an_unchanged_frame_draws_and_rewrites_nothing() {
let mut rsc = TestRsc { ui: Ui::default() };
let (_scroll, root, _rects) = scrolled_rects(&mut rsc, 500);
let mut render = UiRenderState::new();
render.resize((800.0, 20000.0));
render.update(&root, &mut rsc);
render.update(&root, &mut rsc);
render.take_counters(); // discard the first, real draws
render.update(&root, &mut rsc);
let RenderCounters {
draws,
region_rewrites: rewrites,
moves,
..
} = render.take_counters();
assert_eq!((draws, rewrites, moves), (0, 0, 0));
}
#[test]
fn scrolling_moves_in_o1_without_a_redraw() {
let mut rsc = TestRsc { ui: Ui::default() };
let (scroll, root, _rects) = scrolled_rects(&mut rsc, 500);
let mut render = UiRenderState::new();
render.resize((800.0, 600.0));
render.update(&root, &mut rsc);
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(0.0);
render.update(&root, &mut rsc);
render.take_counters();
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(-40.0);
render.update(&root, &mut rsc);
let RenderCounters { draws, moves, .. } = render.take_counters();
assert_eq!(draws, 1, "only Scroll itself should redraw");
assert_eq!(moves, 1, "the scrolled subtree should move in one write");
}
#[test]
fn fractional_scrolling_keeps_primitive_edges_on_physical_pixels() {
let mut rsc = TestRsc { ui: Ui::default() };
let (scroll, root, rects) = scrolled_rects(&mut rsc, 20);
let mut render = UiRenderState::new();
render.resize((800.0, 600.0));
render.update(&root, &mut rsc);
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(0.0);
render.update(&root, &mut rsc);
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(-37.25);
render.update(&root, &mut rsc);
let slot = render
.first_primitive(rects[2].id())
.expect("the scrolled rect has a primitive");
let corners = render.primitive_corners(slot, &rsc);
for edge in [
corners.top_left.x,
corners.top_left.y,
corners.bot_right.x,
corners.bot_right.y,
] {
assert_eq!(edge.fract(), 0.0, "fractional rendered edge: {corners:?}");
}
}
#[test]
fn hit_testing_follows_a_scrolled_widget() {
let mut rsc = TestRsc { ui: Ui::default() };
let (scroll, root, rects) = scrolled_rects(&mut rsc, 500);
let mut render = UiRenderState::new();
render.resize((800.0, 600.0));
render.update(&root, &mut rsc);
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(0.0);
render.update(&root, &mut rsc);
let target = &rects[2];
let before = render.resolved_region(target, &rsc).unwrap();
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(-37.0);
render.update(&root, &mut rsc);
let after = render.resolved_region(target, &rsc).unwrap();
let before_px = before.to_px((800.0, 600.0).into());
let after_px = after.to_px((800.0, 600.0).into());
assert!(
(after_px.top_left.y - (before_px.top_left.y - 37.0)).abs() < 0.01,
"before={before_px:?} after={after_px:?}"
);
}
#[test]
fn redrawing_a_masked_widget_does_not_nest_its_own_mask() {
let mut rsc = TestRsc { ui: Ui::default() };
let (_scroll, inner_root, _rects) = scrolled_rects(&mut rsc, 8);
let masked = rsc.ui.widgets.add_strong(Masked {
shape: None,
inner: inner_root,
});
let masked_id = masked.id();
let root = masked.any();
let mut render = UiRenderState::new();
render.resize((800.0, 600.0));
render.update(&root, &mut rsc);
render.redraw(masked_id, &mut rsc);
render.redraw(masked_id, &mut rsc);
assert_eq!(
render.active.get(&masked_id).unwrap().mask,
MaskIdx::NONE,
"a `Masked` at the root is drawn under no mask of its own"
);
}
#[test]
fn a_mask_stays_put_while_its_scrolled_content_moves() {
let mut rsc = TestRsc { ui: Ui::default() };
let (scroll, inner_root, _rects) = scrolled_rects(&mut rsc, 500);
let masked = rsc.ui.widgets.add_strong(Masked {
shape: None,
inner: inner_root,
});
let masked_id = masked.id();
let root = masked.any();
let mut render = UiRenderState::new();
render.resize((800.0, 600.0));
render.update(&root, &mut rsc);
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(0.0);
render.update(&root, &mut rsc);
let masked_slot_before = render.active.get(&masked_id).unwrap().move_slot;
let mask_delta_before = rsc.ui.move_offsets[masked_slot_before.idx()].delta;
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(-40.0);
render.update(&root, &mut rsc);
let masked_slot_after = render.active.get(&masked_id).unwrap().move_slot;
let mask_delta_after = rsc.ui.move_offsets[masked_slot_after.idx()].delta;
assert_eq!(mask_delta_before, [0.0, 0.0]);
assert_eq!(mask_delta_after, [0.0, 0.0]);
}
fn composer_like_tree(rsc: &mut TestRsc) -> (WeakWidget<TextEdit>, StrongWidget) {
let field = wtext("")
.editable(EditMode::MultiLine)
.text_align(Align::LEFT)
.wrap(true)
.size(18)
.color(PaintId::WHITE)
.add(rsc);
let bar = (field.pad(dp(12)).width(rest(1)),)
.span(Dir::RIGHT)
.background(rect(Srgba8::new(40, 40, 46, 255)))
.add(rsc);
let list_stand_in = rect(PaintId::BLACK).height(rest(1)).add(rsc);
let tree = (list_stand_in, bar).span(Dir::DOWN).add_strong(rsc).any();
(field, tree)
}
#[test]
fn composing_text_after_a_keyboard_resize_lands_in_the_bars_own_region() {
let mut rsc = TestRsc { ui: Ui::default() };
let (field, root) = composer_like_tree(&mut rsc);
let mut render = UiRenderState::new();
render.resize((1080.0, 2298.0));
render.update(&root, &mut rsc);
field(&mut rsc).select(vec2(40.0, 2250.0), vec2(1080.0, 2298.0), false, false);
field(&mut rsc).insert("a");
render.update(&root, &mut rsc);
let before_px = render.window_region(&field, &rsc).unwrap();
assert!(
before_px.bot_right.y - before_px.top_left.y < 200.0,
"before a resize: {before_px:?}"
);
assert!(
before_px.top_left.y > 1800.0,
"expected the bar near the bottom before a resize: {before_px:?}"
);
render.resize((1080.0, 1478.0));
render.update(&root, &mut rsc);
field(&mut rsc).insert("b");
render.update(&root, &mut rsc);
let after_px = render.window_region(&field, &rsc).unwrap();
assert!(
after_px.bot_right.y - after_px.top_left.y < 200.0,
"after a resize + keystroke: {after_px:?}"
);
assert!(
after_px.top_left.y > 1200.0,
"expected the bar near the bottom of the shorter window: {after_px:?}"
);
}
#[test]
fn a_scroll_measures_the_box_it_was_offered_not_the_window() {
let mut rsc = TestRsc { ui: Ui::default() };
let rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
let tall = rsc.ui.widgets.add_strong(Sized {
inner: rect.any(),
x: None,
y: Some(Len::abs(1000.0)),
});
let scroll = rsc
.ui
.widgets
.add_strong(ScrollArea::new(tall.any(), Axis::Y, Pin::Start));
let scroll_w = scroll.weak();
let scroll_id = scroll.id();
let capped = rsc.ui.widgets.add_strong(MaxSize {
inner: scroll.any(),
x: None,
y: Some(Len::abs(100.0)),
});
let capped_id = capped.id();
let root = capped.any();
let mut render = UiRenderState::new();
render.resize((800.0, 600.0));
render.update(&root, &mut rsc);
rsc.ui.widgets.get_mut(&scroll_w).unwrap().scroll(0.0);
render.update(&root, &mut rsc);
assert_eq!(
render.active.get(&scroll_id).unwrap().size.y,
Len::abs(1000.0)
);
assert_eq!(
render.active.get(&capped_id).unwrap().size.y,
Len::abs(100.0),
"the cap, not the content and not the window"
);
rsc.ui.widgets.get_mut(&scroll_w).unwrap().scroll(-10_000.0);
render.update(&root, &mut rsc);
assert!(
(rsc.ui.widgets.get_mut(&scroll_w).unwrap().amt() - 900.0).abs() < 0.01,
"amt={}",
rsc.ui.widgets.get_mut(&scroll_w).unwrap().amt()
);
}
#[test]
fn a_panned_widgets_own_hit_box_moves_exactly_once() {
let mut rsc = TestRsc { ui: Ui::default() };
let rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
let tall = rsc.ui.widgets.add_strong(Sized {
inner: rect.any(),
x: None,
y: Some(Len::abs(1000.0)),
});
let tall_w = tall.weak();
let scroll = rsc
.ui
.widgets
.add_strong(ScrollArea::new(tall.any(), Axis::Y, Pin::Start));
let scroll_w = scroll.weak();
let root = scroll.any();
let mut render = UiRenderState::new();
render.resize((800.0, 600.0));
render.update(&root, &mut rsc);
rsc.ui.widgets.get_mut(&scroll_w).unwrap().scroll(0.0);
render.update(&root, &mut rsc);
let before = render.window_region(&tall_w, &rsc).unwrap();
rsc.ui.widgets.get_mut(&scroll_w).unwrap().scroll(-37.0);
render.update(&root, &mut rsc);
let after = render.window_region(&tall_w, &rsc).unwrap();
assert!(
(after.top_left.y - (before.top_left.y - 37.0)).abs() < 0.01,
"the pan was applied twice: before={before:?} after={after:?}"
);
}
#[test]
fn a_masked_widget_keeps_one_mask_slot_that_is_always_its_own_region() {
let mut rsc = TestRsc { ui: Ui::default() };
let (_scroll, inner_root, _rects) = scrolled_rects(&mut rsc, 8);
let masked = rsc.ui.widgets.add_strong(Masked {
shape: None,
inner: inner_root,
});
let masked_id = masked.id();
let filler = rsc.ui.widgets.add_strong(Rect::new(PaintId::BLACK));
let filler = rsc.ui.widgets.add_strong(Sized {
inner: filler.any(),
x: None,
y: Some(rest(1)),
});
let capped = rsc.ui.widgets.add_strong(MaxSize {
inner: masked.any(),
x: None,
y: Some(Len::abs(60.0)),
});
let mut span = Span::empty(Dir::DOWN);
span.push(filler.any());
span.push(capped.any());
let root = rsc.ui.widgets.add_strong(span).any();
let mut render = UiRenderState::new();
render.resize((800.0, 600.0));
for _ in 0..3 {
render.update(&root, &mut rsc);
render.redraw(masked_id, &mut rsc);
}
assert_eq!(
rsc.ui.masks.iter().count(),
1,
"one `Masked` must own exactly one mask slot, however often it is redrawn"
);
let mask = *rsc.ui.masks.iter().next().unwrap();
assert_eq!(
render.primitives.instance(mask.primitive).region,
render.active.get(&masked_id).unwrap().region,
"the mask a descendant clips against must be this widget's current box"
);
}
#[test]
fn a_dp_cap_is_reported_in_pixels_so_a_span_can_place_it() {
let mut rsc = TestRsc { ui: Ui::default() };
let rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
let tall = rsc.ui.widgets.add_strong(Sized {
inner: rect.any(),
x: None,
y: Some(Len::abs(1000.0)),
});
let capped = rsc.ui.widgets.add_strong(MaxSize {
inner: tall.any(),
x: None,
y: Some(Len::dp(100.0)),
});
let capped_w = capped.weak();
let filler = rsc.ui.widgets.add_strong(Rect::new(PaintId::BLACK));
let filler = rsc.ui.widgets.add_strong(Sized {
inner: filler.any(),
x: None,
y: Some(rest(1)),
});
let mut span = Span::empty(Dir::DOWN);
span.push(filler.any());
span.push(capped.any());
let root = rsc.ui.widgets.add_strong(span).any();
let mut render = UiRenderState::new();
render.resize((800.0, 600.0));
render.set_density(2.5);
render.update(&root, &mut rsc);
render.update(&root, &mut rsc);
let box_px = render.window_region(&capped_w, &rsc).unwrap();
let height = box_px.bot_right.y - box_px.top_left.y;
assert!(
(height - 250.0).abs() < 0.01,
"expected the 100dp cap at density 2.5 to be a 250px slot, got {height} ({box_px:?})"
);
}
#[test]
fn a_size_independent_widget_moved_by_its_parent_has_the_hit_box_it_is_drawn_at() {
let mut rsc = TestRsc { ui: Ui::default() };
let top = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
let spacer = rsc.ui.widgets.add_strong(Sized {
inner: top.any(),
x: None,
y: Some(Len::abs(100.0)),
});
let spacer_w = spacer.weak();
let below = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
let below_w = below.weak();
let mut span = Span::empty(Dir::DOWN);
span.push(spacer.any());
span.push(below.any());
let root = rsc.ui.widgets.add_strong(span).any();
let mut render = UiRenderState::new();
render.resize((800.0, 600.0));
render.update(&root, &mut rsc);
let first = render.window_region(&below_w, &rsc).unwrap();
assert!(
(first.top_left.y - 100.0).abs() < 0.01,
"hit box at {:?}, drawn at y=100",
first.top_left
);
rsc.ui.widgets.get_mut(&spacer_w).unwrap().y = Some(Len::abs(250.0));
render.update(&root, &mut rsc);
let after = render.window_region(&below_w, &rsc).unwrap();
assert!(
(after.top_left.y - 250.0).abs() < 0.01,
"hit box at {:?}, drawn at y=250",
after.top_left
);
}
struct MoveThenPlace {
inner: StrongWidget,
offer_top: f32,
place_top: f32,
}
impl Widget for MoveThenPlace {
fn draw(&mut self, painter: &mut Painter) {
let offer = UiRegion::new(
UiSpan::FULL,
UiSpan::new(
UiScalar::abs(self.offer_top),
UiScalar::abs(self.offer_top + 40.0),
),
);
painter.widget_within(&self.inner, offer);
let place = UiRegion::new(
UiSpan::FULL,
UiSpan::new(
UiScalar::abs(self.place_top),
UiScalar::abs(self.place_top + 40.0),
),
);
painter.place(&self.inner, place);
painter.set_size(Size::default());
}
}
#[test]
fn a_widget_moved_by_its_parent_and_then_placed_inside_it_lands_at_the_placement() {
let mut rsc = TestRsc { ui: Ui::default() };
let rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
let child = rsc.ui.widgets.add_strong(Sized {
inner: rect.any(),
x: None,
y: Some(Len::abs(40.0)),
});
let child_w = child.weak();
let parent = rsc.ui.widgets.add_strong(MoveThenPlace {
inner: child.any(),
offer_top: 0.0,
place_top: 100.0,
});
let parent_w = parent.weak();
let root = parent.any();
let mut render = UiRenderState::new();
render.resize((200.0, 400.0));
render.update(&root, &mut rsc);
let before = render.window_region(&child_w, &rsc).unwrap();
assert!(
(before.top_left.y - 100.0).abs() < 0.01,
"the child should be drawn where it was placed, not where it was offered: {before:?}"
);
{
let parent = rsc.ui.widgets.get_mut(&parent_w).unwrap();
parent.offer_top = 200.0;
}
rsc.ui.widgets.needs_redraw.insert(parent_w.id());
render.update(&root, &mut rsc);
let after = render.window_region(&child_w, &rsc).unwrap();
assert!(
(after.top_left.y - 100.0).abs() < 0.01,
"the placement did not change, so neither should the child: before={before:?} \
after={after:?}"
);
}
const RADIUS: f32 = 20.0;
fn rounded_container(rsc: &mut TestRsc) -> (UiRenderState, MaskIdx, WidgetId, u32) {
let child = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
let child_id = child.id();
let shape = rsc
.ui
.widgets
.add_strong(Rect::new(PaintId::BLACK).radius(Len::abs(RADIUS)));
let shape_id = shape.id();
let root = rsc
.ui
.widgets
.add_strong(Masked {
shape: Some(shape.any()),
inner: child.any(),
})
.any();
let mut render = UiRenderState::new();
render.resize((200.0, 100.0));
render.update(&root, rsc);
let mask = render
.active
.get(&child_id)
.expect("the child is drawn")
.mask;
assert_ne!(
mask,
MaskIdx::NONE,
"the child was drawn with no clip at all"
);
let slot = render
.first_primitive(shape_id)
.expect("the shape widget drew a rect");
(render, mask, child_id, slot)
}
#[test]
fn a_masked_child_is_clipped_by_its_container_s_own_corner() {
let mut rsc = TestRsc { ui: Ui::default() };
let (render, mask, _child, slot) = rounded_container(&mut rsc);
let corners = render.primitive_corners(slot, &rsc);
let radius = render
.primitives
.primitive_data::<RectPrimitive>(slot)
.expect("a mask's shape is a rect")
.radius;
let arc_center = corners.top_left + Vec2::new(radius, radius);
let (mut outside, mut inside) = (0, 0);
for i in 0..=20 {
let angle = std::f32::consts::FRAC_PI_2 * i as f32 / 20.0;
let dir = Vec2::new(-angle.cos(), -angle.sin());
for out in [-1.5f32, 0.0, 1.5] {
let pos = arc_center + dir * (radius + out);
let container = rounded_rect_coverage(pos, corners.top_left, corners.bot_right, radius);
assert_eq!(
render.mask_coverage(mask, pos, &rsc),
container,
"at {pos:?} the child's clip and the container's own edge disagree",
);
if container < 0.5 {
outside += 1;
} else {
inside += 1;
}
}
}
assert!(
outside > 0 && inside > 0,
"the sweep stayed on one side of the curve ({outside} out, {inside} in), so it proved \
nothing about the corner"
);
}
#[test]
fn a_mask_s_shape_decides_what_can_be_pressed() {
let mut rsc = TestRsc { ui: Ui::default() };
let (render, mask, _child, slot) = rounded_container(&mut rsc);
let corners = render.primitive_corners(slot, &rsc);
let cut = corners.top_left + Vec2::new(1.0, 1.0);
assert!(
!render.mask_admits(mask, cut, &rsc),
"the corner the container rounded away is still pressable",
);
let inside = corners.top_left + Vec2::new(RADIUS, RADIUS);
assert!(
render.mask_admits(mask, inside, &rsc),
"a point well inside the curve is not pressable",
);
let edge = Vec2::new(
(corners.top_left.x + corners.bot_right.x) / 2.0,
corners.top_left.y + 1.0,
);
assert!(
render.mask_admits(mask, edge, &rsc),
"a straight edge between two corners is not pressable",
);
}
#[test]
fn nested_masks_multiply_their_coverage() {
let mut rsc = TestRsc { ui: Ui::default() };
let child = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
let child_id = child.id();
let inner_shape = rsc
.ui
.widgets
.add_strong(Rect::new(PaintId::BLACK).radius(Len::abs(RADIUS)));
let inner_shape_id = inner_shape.id();
let inner = rsc.ui.widgets.add_strong(Masked {
shape: Some(inner_shape.any()),
inner: child.any(),
});
let outer_shape = rsc
.ui
.widgets
.add_strong(Rect::new(PaintId::BLACK).radius(Len::abs(RADIUS)));
let outer_shape_id = outer_shape.id();
let root = rsc
.ui
.widgets
.add_strong(Masked {
shape: Some(outer_shape.any()),
inner: inner.any(),
})
.any();
let mut render = UiRenderState::new();
render.resize((200.0, 100.0));
render.update(&root, &mut rsc);
let mask = render.active.get(&child_id).expect("drawn").mask;
let one = |render: &UiRenderState, rsc: &TestRsc, id, pos| {
let slot = render.first_primitive(id).expect("a shape rect");
let c = render.primitive_corners(slot, rsc);
let radius = render
.primitives
.primitive_data::<RectPrimitive>(slot)
.unwrap()
.radius;
rounded_rect_coverage(pos, c.top_left, c.bot_right, radius)
};
let slot = render.first_primitive(inner_shape_id).unwrap();
let corners = render.primitive_corners(slot, &rsc);
let pos = corners.top_left + Vec2::new(RADIUS * 0.3, RADIUS * 0.3);
let inner_cov = one(&render, &rsc, inner_shape_id, pos);
let outer_cov = one(&render, &rsc, outer_shape_id, pos);
assert!(
inner_cov > 0.0 && inner_cov < 1.0,
"the sample point is not inside a feather ({inner_cov}), so this proves nothing"
);
assert_eq!(
render.mask_coverage(mask, pos, &rsc),
inner_cov * outer_cov,
"two nested masks must multiply, not intersect",
);
}
#[test]
fn a_plain_mask_still_clips_to_a_square_box() {
let mut rsc = TestRsc { ui: Ui::default() };
let (_scroll, inner_root, _rects) = scrolled_rects(&mut rsc, 8);
let root = rsc
.ui
.widgets
.add_strong(Masked {
shape: None,
inner: inner_root,
})
.any();
let mut render = UiRenderState::new();
render.resize((200.0, 100.0));
render.update(&root, &mut rsc);
let mask = *rsc.ui.masks.iter().next().expect("one mask");
let corners = render.primitive_corners(mask.primitive, &rsc);
let mask_idx = MaskIdx::preset(0);
assert!(
render.mask_admits(mask_idx, corners.top_left + Vec2::new(0.5, 0.5), &rsc),
"a square clip must admit its own corner pixel",
);
assert!(
!render.mask_admits(mask_idx, corners.top_left - Vec2::new(2.0, 2.0), &rsc),
"a square clip must reject a point outside it",
);
}
#[test]
fn a_scroll_area_opens_at_the_start_of_content_it_has_not_measured_yet() {
for (name, pin, want) in [("read", Pin::Start, 0.0), ("written", Pin::End, 4900.0)] {
let mut rsc = TestRsc { ui: Ui::default() };
let fill = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE)).any();
let tall = rsc.ui.widgets.add_strong(Sized {
inner: fill,
x: None,
y: Some(Len::abs(5000.0)),
});
let scroll = rsc
.ui
.widgets
.add_strong(ScrollArea::new(tall.any(), Axis::Y, pin));
let weak = scroll.weak();
let root = scroll.any();
let mut render = UiRenderState::new();
render.resize((800.0, 100.0));
render.update(&root, &mut rsc);
let _ = rsc.ui.widgets.get_mut(&weak);
render.update(&root, &mut rsc);
let amt = rsc.ui.widgets.get(&weak).unwrap().amt();
assert!(
(amt - want).abs() < 0.01,
"an area to be {name} should have opened at {want}, got {amt}"
);
}
}
#[test]
fn a_span_of_padded_children_inside_a_span_draws_each_where_its_box_is() {
const PAD: f32 = 4.0;
const ROW: f32 = 20.0;
const HEADER: f32 = 30.0;
let mut rsc = TestRsc { ui: Ui::default() };
let header_fill = rsc.ui.widgets.add_strong(Rect::new(PaintId::RED)).any();
let header_id = header_fill.id();
let header = rsc.ui.widgets.add_strong(Sized {
inner: header_fill,
x: None,
y: Some(Len::abs(HEADER)),
});
let mut inner = Span::empty(Dir::DOWN);
let mut rects = Vec::new();
for _ in 0..3 {
let rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
rects.push(rect.weak());
let sized = rsc.ui.widgets.add_strong(Sized {
inner: rect.any(),
x: None,
y: Some(Len::abs(ROW)),
});
let padded = rsc.ui.widgets.add_strong(Pad {
padding: Padding::uniform(PAD),
inner: sized.any(),
exact_region: false,
});
let fill = rsc.ui.widgets.add_strong(Rect::new(PaintId::BLUE)).any();
let card = rsc.ui.widgets.add_strong(Stack {
children: vec![fill, padded.any()],
size: StackSize::Child(1),
});
let wide = rsc.ui.widgets.add_strong(Sized {
inner: card.any(),
x: Some(Len::rest(1.0)),
y: None,
});
inner.push(wide.any());
}
let inner = rsc.ui.widgets.add_strong(inner);
let outer = rsc.ui.widgets.add_strong(Span {
children: vec![header.any(), inner.any()],
dir: Dir::DOWN,
gap: Len::ZERO,
});
let mut list = LazySpan::new(Dir::DOWN, Pin::End);
list.push_back(LazyItem::new(0, outer.any()));
let list = rsc.ui.widgets.add_strong(list);
let root = rsc
.ui
.widgets
.add_strong(Masked {
shape: None,
inner: list.any(),
})
.any();
let mut render = UiRenderState::new();
render.resize((200.0, 400.0));
render.update(&root, &mut rsc);
render.update(&root, &mut rsc);
let head_slot = render
.first_primitive(header_id)
.expect("the header drew a primitive");
let head_top = render.primitive_corners(head_slot, &rsc).top_left.y;
for (i, rect) in rects.iter().enumerate() {
let want = head_top + HEADER + (ROW + 2.0 * PAD) * i as f32 + PAD;
let slot = render
.first_primitive(rect.id())
.expect("each rect drew a primitive");
let drawn = render.primitive_corners(slot, &rsc);
assert!(
(drawn.top_left.y - want).abs() < 0.01,
"row {i} should be drawn at y={want}, got {drawn:?}"
);
}
}
#[test]
fn a_new_child_in_a_growing_lazy_row_uses_its_final_box_immediately() {
const FIRST: f32 = 30.0;
const SECOND: f32 = 70.0;
const GAP: f32 = 8.0;
let mut rsc = TestRsc { ui: Ui::default() };
let first = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
let first_id = first.id();
let first = rsc.ui.widgets.add_strong(Sized {
inner: first.any(),
x: None,
y: Some(Len::abs(FIRST)),
});
let mut contents = Span::empty(Dir::DOWN).gap(Len::abs(GAP));
contents.push(first.any());
let contents = rsc.ui.widgets.add_strong(contents);
let contents_w = contents.weak();
let row = rsc.ui.widgets.add_strong(Sized {
inner: contents.any(),
x: Some(Len::rest(1.0)),
y: None,
});
let mut list = LazySpan::new(Dir::DOWN, Pin::End);
list.push_back(LazyItem::new(0, row.any()));
let root = rsc.ui.widgets.add_strong(list).any();
let mut render = UiRenderState::new();
render.resize((200.0, 200.0));
render.update(&root, &mut rsc);
let second = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
let second_id = second.id();
let second = rsc.ui.widgets.add_strong(Sized {
inner: second.any(),
x: None,
y: Some(Len::abs(SECOND)),
});
rsc.ui
.widgets
.get_mut(&contents_w)
.unwrap()
.push(second.any());
render.update(&root, &mut rsc);
let first = render.primitive_corners(render.first_primitive(first_id).unwrap(), &rsc);
let second = render.primitive_corners(render.first_primitive(second_id).unwrap(), &rsc);
assert!(
(second.top_left.y - (first.bot_right.y + GAP)).abs() < 0.01,
"the new child should start after the old child and its gap: first={first:?} second={second:?}"
);
assert!(
(second.bot_right.y - second.top_left.y - SECOND).abs() < 0.01,
"the new child retained its provisional box: {second:?}"
);
}