1160 lines
36 KiB
Rust
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:?}"
|
|
);
|
|
}
|