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>>, } 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>>, } struct CountedLeaf { height: f32, draws: Rc>, } 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, StrongWidget, Vec>) { 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, 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::(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::(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:?}" ); }