Recompose retained frames exactly and preserve text width validity
Keep each widget's original local frame and replay the same composition order on reuse. Remove inverse region remapping, including its fixed-frame fallback that forced otherwise valid subtrees to draw again. Require exact pixel-region equality in the shared generated oracle. Check primitive and mask geometry as well as draw reuse when fixed frames resize. Publish text's retained line-break range, with no upper bound when there are no soft breaks, and cover widening, explicit newlines, and empty text. Compared withefb416b, the depth-8 diagnostic rig performs 7-9% fewer widget evaluations in the affected phases. Uninstrumented release runs use 3.5% fewer instructions for size changes and 5.0% fewer for resize. Repaint and scroll use 0.7% and 0.6% more instructions. Container updates remain substantially more expensive than thee44dea3baseline; this is still an experimental continuation, not a production replacement.
This commit is contained in:
1 parent
efb416bbc3
commit
2ed5503717
7 files changed
+162
-203
No files matched your search
@@ -808,3 +808,109 @@ fn changing_an_inherited_extent_keeps_the_original_measurement_offer() {
|
||||
primitive_bounds(&cold, other.id())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widening_text_without_soft_breaks_reuses_its_drawing() {
|
||||
struct CountedText {
|
||||
text: Text,
|
||||
draws: Rc<Cell<usize>>,
|
||||
}
|
||||
impl Widget for CountedText {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
self.draws.set(self.draws.get() + 1);
|
||||
self.text.draw(painter)
|
||||
}
|
||||
}
|
||||
for content in ["Short text", "Two hard\nline breaks\nhere", ""] {
|
||||
let plant = |h: &mut Harness| {
|
||||
let mut text = Text::new(content);
|
||||
text.wrap = true;
|
||||
let draws = Rc::new(Cell::new(0));
|
||||
let root = CountedText {
|
||||
text,
|
||||
draws: draws.clone(),
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
h.set_root(root);
|
||||
(root, draws)
|
||||
};
|
||||
let mut warm = Harness::new((300, 200));
|
||||
let (root, draws) = plant(&mut warm);
|
||||
let before = draws.get();
|
||||
warm.resize((500, 200));
|
||||
warm.frame();
|
||||
assert_eq!(draws.get(), before, "{content:?}");
|
||||
let mut cold = Harness::new((500, 200));
|
||||
let (other, _) = plant(&mut cold);
|
||||
assert_eq!(warm.region(&root), cold.region(&other));
|
||||
assert_eq!(
|
||||
primitive_bounds(&warm, root.id()),
|
||||
primitive_bounds(&cold, other.id())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resizing_a_fixed_frame_recomposes_its_contents_without_drawing_them() {
|
||||
struct Frame {
|
||||
child: StrongWidget,
|
||||
region: UiRegion,
|
||||
}
|
||||
impl Widget for Frame {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
painter.widget_within(&self.child, self.region);
|
||||
Size::LEFTOVER
|
||||
}
|
||||
}
|
||||
struct Painted(Rc<Cell<usize>>);
|
||||
impl Widget for Painted {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
self.0.set(self.0.get() + 1);
|
||||
painter.set_mask(DrawRegion::Extent(UiRegion::FULL));
|
||||
painter.primitive(RectPrimitive::color(Color::BLUE));
|
||||
Size::LEFTOVER
|
||||
}
|
||||
}
|
||||
let fixed = |start, end| UiRegion::new(UiSpan::new(Len::px(start), Len::px(end)), UiSpan::FULL);
|
||||
for node in [false, true] {
|
||||
let plant = |h: &mut Harness, region| {
|
||||
let draws = Rc::new(Cell::new(0));
|
||||
let leaf = Painted(draws.clone()).add(&mut h.rsc);
|
||||
h.rsc.widgets_mut().set_region_node(leaf, node);
|
||||
let inner = Frame {
|
||||
child: leaf.add_strong(&mut h.rsc),
|
||||
region: UiRegion::new(UiSpan::new(Len::rel(0.23), Len::rel(0.83)), UiSpan::FULL),
|
||||
}
|
||||
.add_strong(&mut h.rsc);
|
||||
let root = Frame {
|
||||
child: inner,
|
||||
region,
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
h.set_root(root);
|
||||
(root, leaf, draws)
|
||||
};
|
||||
let mut warm = Harness::new((400, 200));
|
||||
let (root, leaf, draws) = plant(&mut warm, fixed(7.0, 104.0));
|
||||
let before = draws.get();
|
||||
warm.rsc[root].region = fixed(19.0, 180.0);
|
||||
warm.frame();
|
||||
assert_eq!(draws.get(), before);
|
||||
let mut cold = Harness::new((400, 200));
|
||||
let (_, other, _) = plant(&mut cold, fixed(19.0, 180.0));
|
||||
assert_eq!(warm.region(&leaf), cold.region(&other));
|
||||
assert_eq!(
|
||||
primitive_bounds(&warm, leaf.id()),
|
||||
primitive_bounds(&cold, other.id())
|
||||
);
|
||||
let mask = |h: &Harness, id: WidgetId| {
|
||||
let active = &h.render.active[&id];
|
||||
let mask = &h.rsc.ui().masks[active.mask.idx()];
|
||||
h.render
|
||||
.moves
|
||||
.resolve(mask.move_idx, mask.region)
|
||||
.to_px(h.render.output_size())
|
||||
};
|
||||
assert_eq!(mask(&warm, leaf.id()), mask(&cold, other.id()));
|
||||
}
|
||||
}
|
||||
+1
-30
@@ -42,21 +42,6 @@ const OUTER: (f32, f32) = (1920.0, 1200.0);
|
||||
const INNER: (f32, f32) = (640.0, 900.0);
|
||||
const STILL: (f32, f32) = (900.0, 1200.0);
|
||||
|
||||
/// The same box, to two steps of the grid between the two ways of reaching
|
||||
/// it. A move, a repaint, a row of shares and every length in pixels land on
|
||||
/// the same number. What needs the slack is a position: a box centred in a
|
||||
/// fraction of its parent against the same box centred in its own pixels,
|
||||
/// and a box re-expressed as a fraction of a parent that changed length.
|
||||
/// A step is a thousandth of a pixel, where this was a twentieth of one
|
||||
/// before any of it was on a grid.
|
||||
///
|
||||
/// **One step is not enough**, tried 2026-09-17 once a length in pixels
|
||||
/// stopped being composed: it passes the 100-seed oracle and fails the
|
||||
/// 400-seed shrinker on `resize-size`, seeds 384 and 162, by 0.002 px. So
|
||||
/// what is left here is the resize path's own rounding rather than a length
|
||||
/// reached two ways.
|
||||
const AGREE_STEPS: i32 = 2;
|
||||
|
||||
/// A way of changing what a span holds. Each is a shape worth its own case:
|
||||
/// taking a child out of the middle is not the same as emptying a span, and
|
||||
/// adding one is not the same as adding three.
|
||||
@@ -399,20 +384,6 @@ fn describe_widget(id: WidgetId, h: &Harness) -> String {
|
||||
label
|
||||
}
|
||||
|
||||
fn same_region(got: Option<PixelRegion>, want: Option<PixelRegion>) -> bool {
|
||||
match (got, want) {
|
||||
(Some(got), Some(want)) => {
|
||||
let same = |a: Px, b: Px| (a - b).abs() <= Px::STEP.mul_int(AGREE_STEPS);
|
||||
same(got.top_left.x, want.top_left.x)
|
||||
&& same(got.top_left.y, want.top_left.y)
|
||||
&& same(got.bot_right.x, want.bot_right.x)
|
||||
&& same(got.bot_right.y, want.bot_right.y)
|
||||
}
|
||||
(None, None) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs `case` on the tree `plan` describes, warm and cold, and says where
|
||||
/// the two disagree. `seed` chooses only the values a case picks at random,
|
||||
/// so one plan under one case is one comparison however it was reached.
|
||||
@@ -439,7 +410,7 @@ pub fn diverges(plan: &Plan, case: Case, seed: u64) -> Option<String> {
|
||||
for (i, (&w, &c)) in tree.ids.iter().zip(&cold_tree.ids).enumerate() {
|
||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||
drawn += got.is_some() as usize;
|
||||
if same_region(got, want) {
|
||||
if got == want {
|
||||
continue;
|
||||
}
|
||||
// Where two trees disagree is rarely where the cause is, so the
|
||||
|
||||
Reference in new issue
Block a user