Compare commits
27
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
950960cccd | ||
|
|
1c80051d57 | ||
|
|
a92c6acdbf | ||
|
|
c8beca5753 | ||
|
|
4bd8607968 | ||
|
|
ffd79f32d3 | ||
|
|
0e0d4af326 | ||
|
|
ea6dbae0dc | ||
|
|
32542d0c0b | ||
|
|
5b7800264d | ||
|
|
5f16617511 | ||
|
|
45a717695b | ||
|
|
d21a21524f | ||
|
|
e166e005dc | ||
|
|
38eba543f6 | ||
|
|
2bc6bdfc77 | ||
|
|
d8ae9c3bdd | ||
|
|
08c9d5aa32 | ||
|
|
60367d806e | ||
|
|
aea878d141 | ||
|
|
98d4e98a29 | ||
|
|
4febabfd2e | ||
|
|
394d5149a5 | ||
|
|
4cbb242a5d | ||
|
|
d75a1e2129 | ||
|
|
1940e85c70 | ||
|
|
cb1bba4682 |
No files matched your search
+98
-36
@@ -10,23 +10,30 @@ use std::{
|
||||
/// chain, and the same box summed from what its children asked for -- and has
|
||||
/// to decide whether the two are the same place. In floats they land a few
|
||||
/// bits apart, which is a defect wherever the answer changes what is drawn
|
||||
/// rather than where. Here adding and subtracting are exact and only a
|
||||
/// multiply or a conversion rounds, back onto the same steps, so two routes
|
||||
/// that come within half a step land on one number and everything downstream
|
||||
/// compares for equality instead of for nearness.
|
||||
/// rather than where. Here adding and subtracting are exact, a multiply
|
||||
/// drops to the step below, and a conversion between grids takes the nearest
|
||||
/// one, so two routes to one place land on one number and everything
|
||||
/// downstream compares for equality instead of for nearness.
|
||||
///
|
||||
/// `SHIFT` is the number of fractional bits, which is what makes the steps
|
||||
/// divide a whole number: a power of two also converts to `f32` without
|
||||
/// rounding while the value fits in its mantissa.
|
||||
///
|
||||
/// Arithmetic wraps at the ends of the range, the way the `i32` underneath
|
||||
/// does. Saturating instead was measured at a twelfth of layout's
|
||||
/// instructions -- five per add against one -- to keep the ordering of
|
||||
/// coordinates two million pixels out, where nothing draws anyway. A value
|
||||
/// off the end is a defect either way; wrapping makes it an obvious one.
|
||||
/// Only [`Self::from_f32`] clamps, since a float has further to come from.
|
||||
#[repr(transparent)]
|
||||
#[derive(
|
||||
Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, bytemuck::Pod, bytemuck::Zeroable,
|
||||
)]
|
||||
pub struct Fixed<const SHIFT: u32>(i32);
|
||||
|
||||
/// A length or a coordinate in pixels, to a sixty-fourth. Finer than anything
|
||||
/// a display can show, and exact in `f32` up to 262,144 px, which is what lets
|
||||
/// the same number reach the GPU.
|
||||
/// A length or a coordinate in pixels, in steps of `1/1024`. Finer than
|
||||
/// anything a display can show, and exact in `f32` up to 16,384 px, which is
|
||||
/// what lets the same number reach the GPU.
|
||||
pub type Px = Fixed<PX_SHIFT>;
|
||||
|
||||
/// How many bits of a pixel a [`Px`] keeps. One place, because [`PxVec2`]
|
||||
@@ -56,8 +63,8 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
|
||||
/// The gap between neighbouring values, which is also how far apart two
|
||||
/// numbers can be and still mean the same place.
|
||||
pub const STEP: Self = Self(1);
|
||||
/// Also what stands in for an unbounded end, since arithmetic saturates
|
||||
/// here rather than wrapping past it.
|
||||
/// Also what stands in for an unbounded end: compared against, never
|
||||
/// added to, since arithmetic wraps past it.
|
||||
pub const MIN: Self = Self(i32::MIN);
|
||||
pub const MAX: Self = Self(i32::MAX);
|
||||
|
||||
@@ -77,12 +84,13 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
|
||||
}
|
||||
|
||||
pub const fn from_int(v: i32) -> Self {
|
||||
Self(v.saturating_mul(Self::one().0))
|
||||
Self(v.wrapping_mul(Self::one().0))
|
||||
}
|
||||
|
||||
/// Rounds to the nearest step, and saturates rather than wrapping. A NaN
|
||||
/// has no nearest step and becomes zero, which is a caller's mistake
|
||||
/// rather than a value worth carrying.
|
||||
/// Rounds to the nearest step, and clamps to the ends of the grid rather
|
||||
/// than wrapping: this is where a number from outside arrives, and a float
|
||||
/// has the range to be anywhere. A NaN has no nearest step and becomes
|
||||
/// zero, which is a caller's mistake rather than a value worth carrying.
|
||||
///
|
||||
/// Half-away is written out rather than called through `f32::round`,
|
||||
/// which is not `const`: a layout constant has to stay a constant.
|
||||
@@ -101,6 +109,18 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
|
||||
})
|
||||
}
|
||||
|
||||
/// The first step at or above `v`, where [`Self::from_f32`] takes the
|
||||
/// nearest one and is below it half the time. For a bound that has to
|
||||
/// admit the value it came from: a measurement rounded down is a bound
|
||||
/// that leaves out the thing it was measured from.
|
||||
pub const fn ceil_from_f32(v: f32) -> Self {
|
||||
let nearest = Self::from_f32(v);
|
||||
match nearest.to_f32() < v {
|
||||
true => nearest.next_up(),
|
||||
false => nearest,
|
||||
}
|
||||
}
|
||||
|
||||
/// From a number as it is written in source -- `16`, `1.5` -- which is
|
||||
/// the other place a value enters the grid.
|
||||
pub fn from_num(v: impl UiNum) -> Self {
|
||||
@@ -114,33 +134,41 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
|
||||
/// The same value on another grid, rounded where the new one is coarser.
|
||||
pub const fn to_scale<const TO: u32>(self) -> Fixed<TO> {
|
||||
Fixed(match TO >= SHIFT {
|
||||
true => narrow((self.0 as i64) << (TO - SHIFT)),
|
||||
false => narrow(shift_round(self.0 as i64, SHIFT - TO)),
|
||||
true => self.0 << (TO - SHIFT),
|
||||
false => shift_round(self.0 as i64, SHIFT - TO) as i32,
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn add(self, rhs: Self) -> Self {
|
||||
Self(self.0.saturating_add(rhs.0))
|
||||
Self(self.0.wrapping_add(rhs.0))
|
||||
}
|
||||
|
||||
pub const fn sub(self, rhs: Self) -> Self {
|
||||
Self(self.0.saturating_sub(rhs.0))
|
||||
Self(self.0.wrapping_sub(rhs.0))
|
||||
}
|
||||
|
||||
pub const fn neg(self) -> Self {
|
||||
Self(self.0.saturating_neg())
|
||||
Self(self.0.wrapping_neg())
|
||||
}
|
||||
|
||||
/// Scaled by a number on any grid, which is how a length takes a fraction
|
||||
/// of itself and keeps being a length: the product is measured in the
|
||||
/// receiver's steps.
|
||||
///
|
||||
/// Dropped to the step below rather than taken to the nearest one
|
||||
/// (Bryan, 2026-09-16), which costs a share a thousandth of a pixel of
|
||||
/// its row -- less than an even number of pixels draws. Toward negative
|
||||
/// infinity on both sides of zero, since that is a shift and nothing
|
||||
/// else: a value and its negation therefore land different distances
|
||||
/// from where they came, so a flipped span can sit a step from its
|
||||
/// mirror image.
|
||||
pub const fn mul<const BY: u32>(self, by: Fixed<BY>) -> Self {
|
||||
Self(narrow(shift_round(self.0 as i64 * by.0 as i64, BY)))
|
||||
Self(((self.0 as i64 * by.0 as i64) >> BY) as i32)
|
||||
}
|
||||
|
||||
/// Repeated a whole number of times, which no grid rounds.
|
||||
pub const fn mul_int(self, by: i32) -> Self {
|
||||
Self(narrow(self.0 as i64 * by as i64))
|
||||
Self(self.0.wrapping_mul(by))
|
||||
}
|
||||
|
||||
/// Divided into a whole number of parts, rounded to the nearest step.
|
||||
@@ -149,12 +177,13 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
|
||||
if by == 0 {
|
||||
return Self::ZERO;
|
||||
}
|
||||
Self(narrow(div_round(self.0 as i64, by as i64)))
|
||||
Self(div_round(self.0 as i64, by as i64) as i32)
|
||||
}
|
||||
|
||||
/// Divided by a number on any grid. A zero divisor is a caller bug -- a
|
||||
/// box of no length has no fraction of itself -- and saturates so that a
|
||||
/// release build lays out something absurd rather than dying.
|
||||
/// box of no length has no fraction of itself -- and answers with the end
|
||||
/// of the range so that a release build lays out something absurd rather
|
||||
/// than dying.
|
||||
pub const fn div<const BY: u32>(self, by: Fixed<BY>) -> Self {
|
||||
debug_assert!(by.0 != 0, "dividing by a length of zero");
|
||||
if by.0 == 0 {
|
||||
@@ -163,7 +192,7 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
|
||||
false => Self::MAX,
|
||||
};
|
||||
}
|
||||
Self(narrow(div_round((self.0 as i64) << BY, by.0 as i64)))
|
||||
Self(div_round((self.0 as i64) << BY, by.0 as i64) as i32)
|
||||
}
|
||||
|
||||
/// `num / den` on *this* grid rather than on theirs, for weights coarser
|
||||
@@ -173,7 +202,7 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
|
||||
if den.0 == 0 {
|
||||
return Self::ZERO;
|
||||
}
|
||||
Self(narrow(div_round((num.0 as i64) << SHIFT, den.0 as i64)))
|
||||
Self(div_round((num.0 as i64) << SHIFT, den.0 as i64) as i32)
|
||||
}
|
||||
|
||||
/// `from` and `to` a fraction of the way apart, the fraction being the
|
||||
@@ -197,7 +226,7 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
|
||||
}
|
||||
|
||||
pub const fn abs(self) -> Self {
|
||||
Self(self.0.saturating_abs())
|
||||
Self(self.0.wrapping_abs())
|
||||
}
|
||||
|
||||
pub const fn clamp(self, lo: Self, hi: Self) -> Self {
|
||||
@@ -209,11 +238,11 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
|
||||
/// boundary. The step is the whole gap, so there is nothing to exclude
|
||||
/// between this and the boundary itself.
|
||||
pub const fn next_up(self) -> Self {
|
||||
Self(self.0.saturating_add(1))
|
||||
Self(self.0.wrapping_add(1))
|
||||
}
|
||||
|
||||
pub const fn next_down(self) -> Self {
|
||||
Self(self.0.saturating_sub(1))
|
||||
Self(self.0.wrapping_sub(1))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,6 +279,8 @@ pub(crate) const fn div_toward(num: i64, den: i64, up: bool) -> i64 {
|
||||
}
|
||||
}
|
||||
|
||||
/// Clamped to the ends, unlike a [`Fixed`]'s own arithmetic: a range of box
|
||||
/// lengths that runs past `i32` really is unbounded.
|
||||
pub(crate) const fn narrow(v: i64) -> i32 {
|
||||
if v > i32::MAX as i64 {
|
||||
return i32::MAX;
|
||||
@@ -353,6 +384,12 @@ impl<const SHIFT: u32> FixedVec2<SHIFT> {
|
||||
Self::new(Fixed::from_f32(v.x), Fixed::from_f32(v.y))
|
||||
}
|
||||
|
||||
/// The first step at or above each part, for a measurement reported as a
|
||||
/// box: what it occupies is not less than what was measured.
|
||||
pub fn ceil_from_f32(v: Vec2) -> Self {
|
||||
Self::new(Fixed::ceil_from_f32(v.x), Fixed::ceil_from_f32(v.y))
|
||||
}
|
||||
|
||||
pub fn to_f32(self) -> Vec2 {
|
||||
Vec2::new(self.x.to_f32(), self.y.to_f32())
|
||||
}
|
||||
@@ -444,29 +481,54 @@ mod tests {
|
||||
assert_eq!(Px::from_int(100) * Rel::ZERO, Px::ZERO);
|
||||
}
|
||||
|
||||
/// Toward negative infinity on both sides of zero, which is what makes
|
||||
/// it a shift rather than a shift and a sign branch -- and what makes a
|
||||
/// value and its negation land different distances from where they came,
|
||||
/// so a flipped span can sit a step from its mirror image.
|
||||
#[test]
|
||||
fn halves_round_away_from_zero_either_side() {
|
||||
fn a_multiply_drops_to_the_step_below_on_both_sides_of_zero() {
|
||||
// A step and a half of one, which has no step of its own.
|
||||
let step_and_a_half = Rel::from_f32(1.5).div_int(Px::ONE.raw());
|
||||
assert_eq!(Px::ONE * step_and_a_half, Px::from_raw(2));
|
||||
assert_eq!(Px::ONE * step_and_a_half, Px::from_raw(1));
|
||||
assert_eq!(Px::ONE.neg() * step_and_a_half, Px::from_raw(-2));
|
||||
}
|
||||
|
||||
/// A division rounds to the nearest step, so it cannot put back the
|
||||
/// steps a truncating multiply dropped: a round trip comes back short,
|
||||
/// never long, and by the few steps the two operations gave up.
|
||||
#[test]
|
||||
fn dividing_by_a_fraction_undoes_multiplying_by_it() {
|
||||
fn dividing_by_a_fraction_cannot_undo_a_truncating_multiply() {
|
||||
let third = Rel::ONE / Rel::from_int(3);
|
||||
let len = Px::from_int(300);
|
||||
assert_eq!(len * third / third, len);
|
||||
let back = len * third / third;
|
||||
assert!(back <= len, "{back:?} is longer than {len:?}");
|
||||
assert!(len - back <= Px::from_raw(3), "{back:?} against {len:?}");
|
||||
assert_eq!(Px::from_int(100) / Rel::from_f32(0.5), Px::from_int(200));
|
||||
}
|
||||
|
||||
/// The bound a greedy line break needs: the width it was measured at is
|
||||
/// not on the grid, and the narrowest box the break still holds for is
|
||||
/// the step at or above it, never the one below.
|
||||
#[test]
|
||||
fn arithmetic_saturates_rather_than_wrapping() {
|
||||
assert_eq!(Px::MAX + Px::ONE, Px::MAX);
|
||||
assert_eq!(Px::MIN - Px::ONE, Px::MIN);
|
||||
fn a_ceiling_never_lands_below_the_number_it_came_from() {
|
||||
let step = 1.0 / (1 << PX_SHIFT) as f32;
|
||||
for n in 0..64 {
|
||||
let v = 189.0 + n as f32 * step / 3.0;
|
||||
let up = Px::ceil_from_f32(v);
|
||||
assert!(up.to_f32() >= v, "{up:?} is below {v}");
|
||||
assert!(
|
||||
up.to_f32() - v < step,
|
||||
"{up:?} is more than a step above {v}"
|
||||
);
|
||||
}
|
||||
// An exact step is its own ceiling.
|
||||
assert_eq!(Px::ceil_from_f32(189.5), Px::from_f32(189.5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_number_from_outside_is_clamped_to_the_grid() {
|
||||
assert_eq!(Px::from_f32(1e12), Px::MAX);
|
||||
assert_eq!(Px::from_f32(-1e12), Px::MIN);
|
||||
assert_eq!(Px::from_int(i32::MAX), Px::MAX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -84,8 +84,7 @@ pub struct RegionAlign {
|
||||
}
|
||||
|
||||
impl RegionAlign {
|
||||
/// Both axes at the near edge. What a container passes as an override for
|
||||
/// a child it is going to position itself.
|
||||
/// Both axes at the near edge: the start of a box in its own orientation.
|
||||
pub const NEAR: Self = Self {
|
||||
x: AxisAlign::NEG,
|
||||
y: AxisAlign::NEG,
|
||||
|
||||
@@ -125,6 +125,13 @@ impl Size {
|
||||
Axis::Y => self.y,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn axis_mut(&mut self, axis: Axis) -> &mut LayoutLen {
|
||||
match axis {
|
||||
Axis::X => &mut self.x,
|
||||
Axis::Y => &mut self.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutLen {
|
||||
@@ -151,6 +158,18 @@ impl LayoutLen {
|
||||
Len::from_parts(self.rel.add(share), self.px)
|
||||
}
|
||||
|
||||
/// This length, given as a part of a box `len` long, as a part of the
|
||||
/// box `len` is itself a part of. The share is untouched: it is a claim
|
||||
/// on whoever divides the room, not a fraction of anything.
|
||||
pub const fn within_len(self, len: Len) -> Self {
|
||||
let part = Len::from_parts(self.rel, self.px).within_len(len);
|
||||
Self {
|
||||
px: part.px,
|
||||
rel: part.rel,
|
||||
leftover: self.leftover,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn px(px: impl UiNum) -> Self {
|
||||
Self {
|
||||
px: Px::from_num(px),
|
||||
|
||||
@@ -219,7 +219,7 @@ impl Len {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn within_len(&self, len: Len) -> Self {
|
||||
pub const fn within_len(&self, len: Len) -> Self {
|
||||
self.within(&UiSpan {
|
||||
start: Len::ZERO,
|
||||
end: len,
|
||||
@@ -282,6 +282,11 @@ impl UiSpan {
|
||||
self.end += offset;
|
||||
}
|
||||
|
||||
/// Composing a box through the one it sits in, and the hottest line in
|
||||
/// layout. It used to skip the multiplies where a span was the whole of
|
||||
/// its parent or the parent the whole of its own; both come out of the
|
||||
/// multiply unchanged anyway, and the body those comparisons cost was
|
||||
/// what kept the inliner from taking this at all.
|
||||
pub const fn within(&self, parent: &Self) -> Self {
|
||||
Self {
|
||||
start: self.start.within(parent),
|
||||
@@ -292,6 +297,15 @@ impl UiSpan {
|
||||
pub const fn len(&self) -> Len {
|
||||
self.end - self.start
|
||||
}
|
||||
|
||||
/// Both ends by the same amount, which is what moving a box without
|
||||
/// changing its length does to every part of it.
|
||||
pub const fn translated(self, by: Len) -> Self {
|
||||
Self {
|
||||
start: self.start + by,
|
||||
end: self.end + by,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
@@ -302,6 +316,17 @@ pub struct UiRegion {
|
||||
}
|
||||
|
||||
impl UiRegion {
|
||||
/// Every part of the box by the same amount on each axis. Done to the
|
||||
/// whole region rather than an end at a time, because that is what it is
|
||||
/// -- and because four adds in a row are four adds, where four asked for
|
||||
/// separately are four sequences.
|
||||
pub const fn translated(self, x: Len, y: Len) -> Self {
|
||||
Self {
|
||||
x: self.x.translated(x),
|
||||
y: self.y.translated(y),
|
||||
}
|
||||
}
|
||||
|
||||
pub const FULL: Self = Self {
|
||||
x: UiSpan::FULL,
|
||||
y: UiSpan::FULL,
|
||||
|
||||
@@ -107,13 +107,6 @@ impl Default for TextAttrs {
|
||||
}
|
||||
}
|
||||
|
||||
/// How far below the longest line a width may fall and still be answered by
|
||||
/// the break in hand. A parent that offers a child the length it reported
|
||||
/// composes that length back through the box chain, so the two differ in the
|
||||
/// last bits -- and at exactly the longest line, that decides whether a line
|
||||
/// fits. Sub-pixel, so no break it admits is one a reader could see.
|
||||
const BREAK_EPSILON_PX: f32 = 0.05;
|
||||
|
||||
/// Keeps text and its corresponding layout from getting out of sync.
|
||||
pub struct TextBuffer {
|
||||
text: String,
|
||||
@@ -200,15 +193,19 @@ impl TextBuffer {
|
||||
// A greedy break at one width is the same break at every width down
|
||||
// to the longest line it produced: each line still fits, and none can
|
||||
// take a word that would not fit in the wider box. So the layout in
|
||||
// hand already answers, and re-breaking would only be a chance to
|
||||
// disagree with itself -- which is what happens when a parent offers
|
||||
// a child the length that child just reported, and the two land
|
||||
// either side of a float.
|
||||
// hand already answers, and re-breaking would only be work.
|
||||
//
|
||||
// At the longest line exactly, with no margin below it. A narrower
|
||||
// width really does break differently, so answering one from the
|
||||
// break in hand is how a warm tree keeps lines a cold tree would
|
||||
// never produce. The margin was here because a text reports the
|
||||
// width it used and a parent hands that back; the report is the step
|
||||
// at or above its longest line now, so what comes back fits.
|
||||
if let Some(key) = &self.layout_key
|
||||
&& key.attrs == *attrs
|
||||
&& let (Some(broke_at), Some(want)) = (key.max_width, width)
|
||||
&& want <= broke_at
|
||||
&& want + BREAK_EPSILON_PX >= self.layout.width()
|
||||
&& want >= self.layout.width()
|
||||
{
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::TextShapeHits);
|
||||
|
||||
@@ -35,6 +35,10 @@ struct MoveOffset {
|
||||
// belongs to the pixel above it. Flooring the product instead drops a pixel
|
||||
// wherever a fraction divides a window exactly: a fifth of 1920 comes out of
|
||||
// `REL_STEP` as 383.99998, and five tabs each lose their last column.
|
||||
//
|
||||
// Taken over the whole coordinate, fraction and pixels summed, since a floor
|
||||
// does not distribute over a sum: floored apart, a half of one and a half of
|
||||
// the other lose the pixel the two together make.
|
||||
fn snap_floor(v: vec2<f32>) -> vec2<f32> {
|
||||
return floor(v + PX_STEP * 0.5);
|
||||
}
|
||||
@@ -147,8 +151,8 @@ fn vs_main(
|
||||
let bot_right_rel = vec2(r.x.end.rel, r.y.end.rel);
|
||||
let bot_right_px = vec2(r.x.end.px, r.y.end.px);
|
||||
|
||||
let top_left = snap_floor(top_left_rel * window.dim) + snap_floor(top_left_px);
|
||||
let bot_right = snap_floor(bot_right_rel * window.dim) + snap_floor(bot_right_px);
|
||||
let top_left = snap_floor(top_left_rel * window.dim + top_left_px);
|
||||
let bot_right = snap_floor(bot_right_rel * window.dim + bot_right_px);
|
||||
let size = bot_right - top_left;
|
||||
|
||||
let uv = vec2<f32>(
|
||||
@@ -179,8 +183,8 @@ fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> {
|
||||
let br = vec2(m.x.end.rel, m.y.end.rel);
|
||||
let br_px = vec2(m.x.end.px, m.y.end.px);
|
||||
|
||||
let top_left = snap_floor(tl * window.dim) + snap_floor(tl_px);
|
||||
let bot_right = snap_floor(br * window.dim) + snap_floor(br_px);
|
||||
let top_left = snap_floor(tl * window.dim + tl_px);
|
||||
let bot_right = snap_floor(br * window.dim + br_px);
|
||||
let pos = in.clip_position.xy;
|
||||
if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y {
|
||||
return color * 0.0;
|
||||
|
||||
+27
-14
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
Holds, LayerId, LayoutLen, MaskIdx, MoveIdx, PrimitiveHandle, RegionAlign, Size, TextureHandle,
|
||||
UiRegion, WidgetId,
|
||||
UiRegion, UiVec2, WidgetId,
|
||||
};
|
||||
|
||||
/// What is kept of a widget its parent has asked about. `drawn` says whether
|
||||
@@ -11,11 +11,20 @@ pub struct ActiveData {
|
||||
pub id: WidgetId,
|
||||
/// The box its drawing is in, in `parent_move`'s coordinates.
|
||||
pub region: UiRegion,
|
||||
/// The box its parent first asked about it in, as a part of the box the
|
||||
/// parent was itself asked in. Any later box it was given was decided
|
||||
/// knowing its answer, so this is where a question about it is asked
|
||||
/// again -- and it is kept relative so that it follows the parent's.
|
||||
pub offer: UiRegion,
|
||||
/// The box its parent gave it, in the same coordinates: what it was
|
||||
/// asked about, before its own answer placed its drawing inside it.
|
||||
/// `region` is that placement, and a local redraw asks here.
|
||||
pub given: UiRegion,
|
||||
/// The same box as lengths of its parent's box, which is the one route
|
||||
/// to a box in pixels: a draw threads these down a level at a time, and
|
||||
/// [`crate::UiRenderState::redraw`] takes the same steps back up.
|
||||
pub given_len: UiVec2,
|
||||
/// The lengths of the box its parent first asked about it in, as
|
||||
/// lengths of the box the parent was itself offered. Any later box it
|
||||
/// was given was decided knowing its answer, so this is the question
|
||||
/// asked again -- and a chain of fractions has no frame in it, which is
|
||||
/// why a region node between two widgets cannot break it.
|
||||
pub offer_len: UiVec2,
|
||||
/// What it answered there: the size and what that held for.
|
||||
pub answer: (Size, [Holds; 2]),
|
||||
/// What the widget said it used of its box, the last time it drew.
|
||||
@@ -41,18 +50,22 @@ pub struct ActiveData {
|
||||
/// A change to one moves a box this widget cannot fix by drawing again,
|
||||
/// and comparing them is what says so.
|
||||
pub declared: [Option<LayoutLen>; 2],
|
||||
/// The alignment its parent asked it with. A local redraw repeats that
|
||||
/// question, including an override chosen by a container.
|
||||
pub align: RegionAlign,
|
||||
/// Whether that alignment was the parent's override rather than the
|
||||
/// widget's own property.
|
||||
pub align_override: bool,
|
||||
/// Its own alignment when it was last drawn. A change to the property is
|
||||
/// found against this even when its parent overrode the alignment.
|
||||
/// The axes along which its parent chose its box from its own answer,
|
||||
/// so a local redraw asks the question its parent asked.
|
||||
pub decided: [bool; 2],
|
||||
/// Its alignment when it was last drawn, which a change to the property
|
||||
/// is found against.
|
||||
pub own_align: RegionAlign,
|
||||
/// The movable region whose coordinates `region` uses.
|
||||
pub parent_move: MoveIdx,
|
||||
/// The mask its drawing is clipped to: one it set itself, or the one it
|
||||
/// inherited from whoever drew it.
|
||||
pub mask: MaskIdx,
|
||||
/// That inherited one. The two differ exactly where the widget set a
|
||||
/// mask of its own, which is the one it owns and the one a move rewrites
|
||||
/// -- and the one a redraw of it must not be handed back, since setting
|
||||
/// a mask asserts there is none.
|
||||
pub parent_mask: MaskIdx,
|
||||
pub layer: LayerId,
|
||||
}
|
||||
|
||||
|
||||
+61
-22
@@ -10,9 +10,9 @@ use std::ops::RangeInclusive;
|
||||
///
|
||||
/// The ends are lengths on the grid rather than floats with a tolerance
|
||||
/// around them: a box offered back at the length a widget reported comes back
|
||||
/// as the same number, so a range means what it says. What widening there is
|
||||
/// belongs to [`Self::through`], which has a rounding to undo, and is derived
|
||||
/// from that rounding rather than chosen.
|
||||
/// as the same number, so a range means what it says. The one place a range
|
||||
/// is wider than the length it came from is [`Self::through`], and what it is
|
||||
/// wider by is the floor that inverting a fraction undoes.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Holds {
|
||||
pub lo: Px,
|
||||
@@ -41,32 +41,29 @@ impl Holds {
|
||||
}
|
||||
|
||||
/// What a box has to be for a part of it, `len` of the box long, to stay
|
||||
/// in this range. A part with no relative extent is a fixed length: it
|
||||
/// was drawn at that length and any box keeps it there.
|
||||
/// in this range: the exact preimage of `px + floor(rel * box)`, which is
|
||||
/// the one way a box in pixels is reached. A part with no relative extent
|
||||
/// is a fixed length -- it was drawn at that length and any box keeps it
|
||||
/// there.
|
||||
///
|
||||
/// The way in is `px + rel * box` taken to the nearest step, so a part
|
||||
/// of exactly `lo` came from anything within half a step of it and the
|
||||
/// answer is an interval even where this range is one length. Inverting
|
||||
/// the length alone instead gives a point that need not even contain the
|
||||
/// box the part was drawn in, which is a range excluding the drawing it
|
||||
/// was made for.
|
||||
/// The answer is an interval even where this range is a single length,
|
||||
/// because the multiply on the way in drops to the step below and many
|
||||
/// boxes therefore give one length. That is a floor rather than an
|
||||
/// allowance: inverting it is two divisions and nothing else, and the
|
||||
/// whole of a box maps back to itself.
|
||||
pub const fn through(self, len: Len) -> Self {
|
||||
let rel = len.rel.raw() as i64;
|
||||
if rel == 0 {
|
||||
return Self::ANY;
|
||||
}
|
||||
// Three half steps either side -- one for the rounding on the way
|
||||
// in, two for the difference between a length composed down the
|
||||
// chain and the same length measured against the window -- and half
|
||||
// of what a `Rel` counts in, to divide by the fraction. Exact until
|
||||
// the division takes it back to the grid.
|
||||
let px = len.px.raw() as i64;
|
||||
let half_rel = REL_SHIFT - 1;
|
||||
let lo = ((self.lo.raw() as i64 - px) * 2 - 3) << half_rel;
|
||||
let hi = ((self.hi.raw() as i64 - px) * 2 + 3) << half_rel;
|
||||
// Dividing by a negative turns the ends around, so which end each
|
||||
// bound comes from is decided before dividing rather than by taking
|
||||
// the min and max of four divisions.
|
||||
// `floor(rel * box) >= lo - px` is `rel * box >= (lo - px) << REL`, and
|
||||
// `floor(rel * box) <= hi - px` is `rel * box < (hi - px + 1) << REL`.
|
||||
let lo = (self.lo.raw() as i64 - px) << REL_SHIFT;
|
||||
let hi = (((self.hi.raw() as i64 - px) + 1) << REL_SHIFT) - 1;
|
||||
// Dividing by a negative fraction turns the ends around, so which
|
||||
// bound each comes from is decided before dividing rather than by
|
||||
// taking the min and max of four divisions.
|
||||
match rel > 0 {
|
||||
true => Self::raws(div_toward(lo, rel, true), div_toward(hi, rel, false)),
|
||||
false => Self::raws(div_toward(hi, rel, true), div_toward(lo, rel, false)),
|
||||
@@ -116,6 +113,48 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget handed the whole of its parent's box, with or without pixels
|
||||
/// taken off it, has no fraction to invert: multiplying by one is exact
|
||||
/// and taking the pixels off again is too, so the box maps back to
|
||||
/// itself. Allowing for anything here compounded a step a level down a
|
||||
/// chain of widgets each taking the whole of its parent.
|
||||
#[test]
|
||||
fn the_whole_of_a_box_maps_back_to_itself() {
|
||||
let at = Px::from_int(956);
|
||||
assert_eq!(Holds::at(at).through(Len::FULL), Holds::at(at));
|
||||
let less_eight = Len::from_parts(Rel::ONE, Px::from_int(-8));
|
||||
assert_eq!(
|
||||
Holds::at(at).through(less_eight),
|
||||
Holds::at(at + Px::from_int(8))
|
||||
);
|
||||
}
|
||||
|
||||
/// The range is the exact preimage at both ends, so a box one step
|
||||
/// outside it really does give a length outside this range. What a wider
|
||||
/// range costs is a drawing reused where it does not hold.
|
||||
#[test]
|
||||
fn a_box_one_step_outside_the_range_is_outside_it() {
|
||||
let part = Len::from_parts(Rel::from_f32(1.0 / 3.0), Px::from_int(-146));
|
||||
let at = Px::from_int(300);
|
||||
let holds = Holds::at(at).through(part);
|
||||
for inside in [holds.lo, holds.hi] {
|
||||
assert_eq!(part.to_px(inside), at, "{inside:?} left out of {holds:?}");
|
||||
}
|
||||
for outside in [holds.lo.next_down(), holds.hi.next_up()] {
|
||||
assert_ne!(part.to_px(outside), at, "{outside:?} admitted by {holds:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A truncating multiply only ever drops, so the step it needs allowing
|
||||
/// for on the way in belongs at the top of the range and not the bottom.
|
||||
#[test]
|
||||
fn a_fraction_widens_further_up_than_down() {
|
||||
let half = Len::from_parts(Rel::from_f32(0.5), Px::ZERO);
|
||||
let holds = Holds::at(Px::from_int(100)).through(half);
|
||||
let box_len = Px::from_int(200);
|
||||
assert!(holds.hi - box_len > box_len - holds.lo, "{holds:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_boundary_the_next_step_along_does_not_admit_it() {
|
||||
let boundary = Px::from_int(10);
|
||||
|
||||
+12
-6
@@ -68,17 +68,24 @@ impl Moves {
|
||||
}
|
||||
}
|
||||
|
||||
/// Composes a region held in `idx`'s coordinates down the chain, which is
|
||||
/// the same walk the vertex shader does.
|
||||
/// The same walk the vertex shader does, in the same `Len` the shader is
|
||||
/// handed, for asking where a drawing will actually land -- hit testing,
|
||||
/// and nothing layout decides on. Layout threads its lengths down the
|
||||
/// draw instead, so no box it compares is composed back up this chain.
|
||||
pub fn resolve(&self, idx: MoveIdx, local: UiRegion) -> UiRegion {
|
||||
let mut region = local;
|
||||
self.walk(idx, |entry| region = region.within(entry));
|
||||
region
|
||||
}
|
||||
|
||||
fn walk(&self, idx: MoveIdx, mut step: impl FnMut(&UiRegion)) {
|
||||
let mut at = idx;
|
||||
for _ in 0..CHAIN_LIMIT {
|
||||
if at == MoveIdx::NONE {
|
||||
return region;
|
||||
return;
|
||||
}
|
||||
let entry = self.arena[at.idx()];
|
||||
region = region.within(&entry.region);
|
||||
let entry = &self.arena[at.idx()];
|
||||
step(&entry.region);
|
||||
at = entry.parent;
|
||||
}
|
||||
debug_assert!(
|
||||
@@ -86,7 +93,6 @@ impl Moves {
|
||||
"a move chain longer than {CHAIN_LIMIT} resolves to the wrong place, \
|
||||
and the shader stops at the same depth"
|
||||
);
|
||||
region
|
||||
}
|
||||
|
||||
/// How many slots a region in `idx` is composed through, which is what
|
||||
|
||||
+93
-60
@@ -1,7 +1,7 @@
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
use crate::layout_diagnostics::{self as diag, Counter};
|
||||
use crate::{
|
||||
Axis, Holds, LayoutLen, Len, Px, PxVec2, RegionAlign, Rel, RenderedText, Size, StrongWidget,
|
||||
Axis, Holds, LayoutLen, Len, Px, PxVec2, RegionAlign, RenderedText, Size, StrongWidget,
|
||||
TextAttrs, TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiVec2, Weight,
|
||||
WidgetId, Widgets,
|
||||
render::{
|
||||
@@ -19,6 +19,11 @@ pub struct Painter<'a> {
|
||||
|
||||
/// This widget's box, in the coordinates of `move_idx`.
|
||||
pub(super) region: UiRegion,
|
||||
/// That box in pixels, which its children's are a length of: threaded
|
||||
/// down from the box this widget was given rather than composed back up
|
||||
/// the chain, so every length in layout is one multiply from its
|
||||
/// parent's and [`Holds::through`] inverts exactly that.
|
||||
pub(super) px: PxVec2,
|
||||
pub(super) mask: MaskIdx,
|
||||
pub(super) textures: Vec<TextureHandle>,
|
||||
pub(super) primitives: Vec<PrimitiveHandle>,
|
||||
@@ -26,10 +31,12 @@ pub struct Painter<'a> {
|
||||
/// The children asked about so far, so the first box each was asked in
|
||||
/// is the one recorded as its offer.
|
||||
pub(super) offered: Vec<WidgetId>,
|
||||
/// The box this widget was first asked about in, in pixels.
|
||||
/// The lengths of the box this widget was first asked about in, in
|
||||
/// pixels. Its children's offers are a fraction of it.
|
||||
pub(super) offered_px: PxVec2,
|
||||
/// Whether this draw is in that box, which makes the questions it asks
|
||||
/// the ones a cold layout asks and their answers the ones to keep.
|
||||
/// Whether this draw is in a box of those lengths, which makes the
|
||||
/// questions it asks the ones a cold layout asks and their answers the
|
||||
/// ones to keep.
|
||||
pub(super) at_offer: bool,
|
||||
/// The children whose size this widget read while drawing.
|
||||
pub(super) size_deps: Vec<WidgetId>,
|
||||
@@ -132,30 +139,25 @@ impl<'a> Painter<'a> {
|
||||
id: &'s StrongWidget<W>,
|
||||
region: UiRegion,
|
||||
) -> DrawResult<'s, 'a, W> {
|
||||
self.widget_at(id, region, None)
|
||||
self.widget_at(id, region, [false; 2])
|
||||
}
|
||||
|
||||
/// Draws a widget with an alignment chosen by its container rather than
|
||||
/// the widget's property. Containers use this when the box they hand down
|
||||
/// already expresses the size they report around the child.
|
||||
pub fn widget_aligned<'s, W: ?Sized>(
|
||||
/// Draws a widget in a box this widget chose from the widget's own
|
||||
/// answer along the `decided` axes. On those the answer is not placed
|
||||
/// inside the box again: it already is the box, and a fraction the
|
||||
/// widget reported, taken of this box a second time, would shrink it
|
||||
/// twice. A container uses this where it hands back exactly what a child
|
||||
/// asked for -- a span placing a child at the length it reported, a
|
||||
/// scroll giving its content the content's own length.
|
||||
pub fn widget_at<'s, W: ?Sized>(
|
||||
&'s mut self,
|
||||
id: &'s StrongWidget<W>,
|
||||
region: UiRegion,
|
||||
align: RegionAlign,
|
||||
) -> DrawResult<'s, 'a, W> {
|
||||
self.widget_at(id, region, Some(align))
|
||||
}
|
||||
|
||||
fn widget_at<'s, W: ?Sized>(
|
||||
&'s mut self,
|
||||
id: &'s StrongWidget<W>,
|
||||
region: UiRegion,
|
||||
align_override: Option<RegionAlign>,
|
||||
decided: [bool; 2],
|
||||
) -> DrawResult<'s, 'a, W> {
|
||||
let region_node = self.rsc.widgets().is_region_node(id.id());
|
||||
let declared = self.declared_lens(id);
|
||||
let align = align_override.unwrap_or_else(|| self.rsc.widgets().alignment(id.id()));
|
||||
let align = self.rsc.widgets().alignment(id.id());
|
||||
// Composing `FULL` through a box is not quite the identity in f32,
|
||||
// so a child with nothing declared keeps the box it would have had.
|
||||
let local = match declared.iter().any(Option::is_some) {
|
||||
@@ -176,11 +178,20 @@ impl<'a> Painter<'a> {
|
||||
self.children.push(id.id());
|
||||
}
|
||||
let first_ask = self.offer(id.id());
|
||||
let offer = match first_ask {
|
||||
true => local,
|
||||
false => self.state.active.get(&id.id()).map_or(local, |a| a.offer),
|
||||
let given_len = local.size();
|
||||
let offer_len = match first_ask {
|
||||
true => given_len,
|
||||
false => self
|
||||
.state
|
||||
.active
|
||||
.get(&id.id())
|
||||
.map_or(given_len, |a| a.offer_len),
|
||||
};
|
||||
let answers_offer = self.at_offer && local == offer;
|
||||
let px = given_len.to_px(self.px);
|
||||
let offered_px = offer_len.to_px(self.offered_px);
|
||||
// Whether this ask is the child's offer question, which is a question
|
||||
// about lengths: the same lengths somewhere else is the same question.
|
||||
let answers_offer = self.at_offer && px == offered_px;
|
||||
// The answer and what it holds for, both about the box asked in. The
|
||||
// child's record may say something else once its drawing has been
|
||||
// placed: a drawing made again in its placed box holds for that box.
|
||||
@@ -194,9 +205,11 @@ impl<'a> Painter<'a> {
|
||||
parent_move: self.move_idx,
|
||||
region_node,
|
||||
mask: self.mask,
|
||||
offer,
|
||||
offered_px: self.px_within_offer(offer),
|
||||
align: align_override,
|
||||
given_len,
|
||||
offer_len,
|
||||
px,
|
||||
offered_px,
|
||||
decided,
|
||||
},
|
||||
None,
|
||||
self.rsc,
|
||||
@@ -209,6 +222,14 @@ impl<'a> Painter<'a> {
|
||||
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) {
|
||||
*under = under.and(holds[axis as usize].through(local.axis(axis).len()));
|
||||
}
|
||||
// The answer as it was given. A fraction in it is a fraction of this
|
||||
// widget's box, which is the same thing a rule beside the child
|
||||
// means and the same thing for every box this widget hands out: a
|
||||
// span offers each child the room left from its cursor, because a
|
||||
// text has to wrap at the width actually there, and `rel(0.5)` is
|
||||
// still half the span. Padding is outside what it pads for the same
|
||||
// reason -- inset the fraction and a child's `rel` would mean the
|
||||
// inner box while its `px` meant the outer one.
|
||||
DrawResult {
|
||||
child: id,
|
||||
painter: self,
|
||||
@@ -256,15 +277,14 @@ impl<'a> Painter<'a> {
|
||||
let declared = self.declared_lens(child);
|
||||
let align = self.rsc.widgets().alignment(child.id());
|
||||
let local = declared_box(region, declared, align);
|
||||
let within = local.within(&self.region);
|
||||
let first_ask = self.offer(child.id());
|
||||
if first_ask && let Some(active) = self.state.active.get_mut(&child.id()) {
|
||||
active.offer = local;
|
||||
active.offer_len = local.size();
|
||||
}
|
||||
if let Some(hint) = self.size_hint(child, axis) {
|
||||
return Some(hint);
|
||||
}
|
||||
let px = self.state.px_of(self.move_idx, within);
|
||||
let px = local.size().to_px(self.px);
|
||||
let (size, holds) =
|
||||
self.state
|
||||
.retained_size(child.id(), px, self.move_idx, self.rsc.widgets())?;
|
||||
@@ -292,15 +312,6 @@ impl<'a> Painter<'a> {
|
||||
true
|
||||
}
|
||||
|
||||
/// The pixel size of a part of the box this widget was asked in.
|
||||
fn px_within_offer(&self, local: UiRegion) -> PxVec2 {
|
||||
let size = local.size();
|
||||
PxVec2::new(
|
||||
size.x.to_px(self.offered_px.x),
|
||||
size.y.to_px(self.offered_px.y),
|
||||
)
|
||||
}
|
||||
|
||||
fn depend_on<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
|
||||
if !self.size_deps.contains(&child.id()) {
|
||||
self.size_deps.push(child.id());
|
||||
@@ -382,25 +393,25 @@ impl<'a> Painter<'a> {
|
||||
/// near edge. A container that reports one child's size gives every child
|
||||
/// this, so what it draws is inside what it says it occupies.
|
||||
pub fn box_of(&self, size: Size) -> UiRegion {
|
||||
placed_box(UiRegion::FULL, size, RegionAlign::NEAR, [None; 2])
|
||||
let lens = placed_lens(size, [None; 2], [false; 2]);
|
||||
placed_box(UiRegion::FULL, lens, RegionAlign::NEAR)
|
||||
}
|
||||
|
||||
/// This widget's box in pixels. Reading it makes the drawing one that
|
||||
/// holds for this box only, until `holds` says how far it goes.
|
||||
pub fn px_size(&mut self) -> PxVec2 {
|
||||
let px = self.state.px_of(self.move_idx, self.region);
|
||||
for (own, len) in self.own.iter_mut().zip([px.x, px.y]) {
|
||||
for (own, len) in self.own.iter_mut().zip([self.px.x, self.px.y]) {
|
||||
if *own == Holds::ANY {
|
||||
*own = Holds::at(len);
|
||||
}
|
||||
}
|
||||
px
|
||||
self.px
|
||||
}
|
||||
|
||||
/// One axis of this widget's box in pixels. Prefer this to
|
||||
/// [`Self::px_size`] when the other axis cannot affect the drawing.
|
||||
pub fn px_len(&mut self, axis: Axis) -> Px {
|
||||
let len = self.state.px_of(self.move_idx, self.region).axis(axis);
|
||||
let len = self.px.axis(axis);
|
||||
let own = &mut self.own[axis as usize];
|
||||
if *own == Holds::ANY {
|
||||
*own = Holds::at(len);
|
||||
@@ -415,7 +426,7 @@ impl<'a> Painter<'a> {
|
||||
pub fn holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
|
||||
let holds = holds.into();
|
||||
debug_assert!(
|
||||
holds.contains(self.state.px_of(self.move_idx, self.region).axis(axis)),
|
||||
holds.contains(self.px.axis(axis)),
|
||||
"'{}' ({:?}) says its drawing holds for lengths that leave out its own box",
|
||||
self.label(),
|
||||
self.id
|
||||
@@ -526,31 +537,53 @@ pub(crate) fn declared_lens(widgets: &Widgets, id: WidgetId) -> [Option<LayoutLe
|
||||
})
|
||||
}
|
||||
|
||||
/// The box a drawing occupies: the size the widget reported, on the side of
|
||||
/// the box it was asked in that its alignment says. An axis reported as a
|
||||
/// share fills, because a share is a length only to whoever divides one, and
|
||||
/// whoever did is the one that handed down this box. A declared axis is
|
||||
/// left alone too: `declared_box` already placed it, in the parent's box,
|
||||
/// and the rule's length is what the widget reports there.
|
||||
/// Whether what a widget reported along an axis is the whole of the box it
|
||||
/// is in rather than a part to be placed inside it. A share fills, because a
|
||||
/// share is a length only to whoever divides one, and whoever did is the one
|
||||
/// that handed down this box. A declared axis does too: `declared_box`
|
||||
/// already placed it, in the parent's box, and the rule's length is what the
|
||||
/// widget reports there. And an axis the parent decided from the answer is
|
||||
/// the answer already.
|
||||
pub(crate) fn fills(reported: LayoutLen, declared: Option<LayoutLen>, decided: bool) -> bool {
|
||||
reported.leftover != Weight::ZERO || declared.is_some() || decided
|
||||
}
|
||||
|
||||
/// What of the box it was given a widget's drawing occupies, as lengths of
|
||||
/// that box: the size it reported wherever that is a part to be placed, and
|
||||
/// the whole of the box wherever the answer fills it.
|
||||
///
|
||||
/// A reported fraction is a fraction of the box the widget drew in, where a
|
||||
/// declared one is a fraction of the box its parent handed down -- a span
|
||||
/// reporting `rel(1.0)` means all of what it was given, whatever that was a
|
||||
/// fraction of. So this scales by the box rather than composing into it.
|
||||
pub(crate) fn placed_box(
|
||||
region: UiRegion,
|
||||
/// fraction of. So this is a length of the box rather than a length composed
|
||||
/// into it, and a box in pixels is this step from the given box's pixels.
|
||||
pub(crate) fn placed_lens(
|
||||
size: Size,
|
||||
align: RegionAlign,
|
||||
declared: [Option<LayoutLen>; 2],
|
||||
) -> UiRegion {
|
||||
let mut placed = region;
|
||||
for (axis, declared) in AXES.into_iter().zip(declared) {
|
||||
decided: [bool; 2],
|
||||
) -> UiVec2 {
|
||||
let mut lens = UiVec2::FULL_SIZE;
|
||||
for (axis, (declared, decided)) in AXES.into_iter().zip(declared.into_iter().zip(decided)) {
|
||||
let reported = size.axis(axis);
|
||||
if reported.leftover != Weight::ZERO || declared.is_some() {
|
||||
if !fills(reported, declared, decided) {
|
||||
*lens.axis_mut(axis) = Len::from_parts(reported.rel, reported.px);
|
||||
}
|
||||
}
|
||||
lens
|
||||
}
|
||||
|
||||
/// Where that drawing sits: those lengths taken of the box the widget was
|
||||
/// asked in, on the side of it that the widget's alignment says.
|
||||
pub(crate) fn placed_box(region: UiRegion, lens: UiVec2, align: RegionAlign) -> UiRegion {
|
||||
let mut placed = region;
|
||||
for axis in AXES {
|
||||
// The whole of the box is already where it sits, and the arithmetic
|
||||
// below is the identity for it.
|
||||
if lens.axis(axis) == Len::FULL {
|
||||
continue;
|
||||
}
|
||||
let span = placed.axis_mut(axis);
|
||||
let len = span.len().scale(reported.rel) + Len::from_parts(Rel::ZERO, reported.px);
|
||||
let len = lens.axis(axis).within_len(span.len());
|
||||
span.start += (span.len() - len).scale(align.axis(axis).rel());
|
||||
span.end = span.start + len;
|
||||
}
|
||||
|
||||
+334
-272
@@ -1,9 +1,9 @@
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind};
|
||||
use crate::ui::painter::{declared_box, declared_lens, placed_box};
|
||||
use crate::ui::painter::{declared_box, declared_lens, placed_box, placed_lens};
|
||||
use crate::{
|
||||
ActiveData, Axis, DrawLayers, Holds, IdLike, LayoutLen, Len, MaskIdx, MoveIdx, Moves, Painter,
|
||||
PixelRegion, PxVec2, RegionAlign, Rel, Size, StrongWidget, UiRegion, UiRsc, UiSpan, Weight,
|
||||
PixelRegion, Px, PxVec2, Rel, Size, StrongWidget, UiRegion, UiRsc, UiSpan, UiVec2, Weight,
|
||||
WidgetId, Widgets,
|
||||
util::{HashMap, Vec2},
|
||||
};
|
||||
@@ -20,13 +20,20 @@ pub(super) struct DrawInfo {
|
||||
pub parent_move: MoveIdx,
|
||||
pub region_node: bool,
|
||||
pub mask: MaskIdx,
|
||||
/// The box it was first asked about in, as a part of its parent's, and
|
||||
/// that box in pixels.
|
||||
pub offer: UiRegion,
|
||||
/// The box its parent gave it, as lengths of the parent's own box, and
|
||||
/// the lengths of the box it was first asked about in the same form.
|
||||
/// Both describe the box the *parent* stated, so the second, placing ask
|
||||
/// carries them unchanged while its own region is the placement inside.
|
||||
pub given_len: UiVec2,
|
||||
pub offer_len: UiVec2,
|
||||
/// This ask's box in pixels, and the offer's: one multiply from the
|
||||
/// parent's own, which is where every pixel length in layout comes from.
|
||||
pub px: PxVec2,
|
||||
pub offered_px: PxVec2,
|
||||
/// A container's answer for where the widget sits. `None` uses the
|
||||
/// widget's own property.
|
||||
pub align: Option<RegionAlign>,
|
||||
/// The axes along which the parent chose this box from the widget's own
|
||||
/// answer, so the answer is not placed inside it again. See
|
||||
/// [`Painter::widget_at`].
|
||||
pub decided: [bool; 2],
|
||||
}
|
||||
|
||||
pub struct UiRenderState {
|
||||
@@ -35,8 +42,6 @@ pub struct UiRenderState {
|
||||
pub(super) output_size: PxVec2,
|
||||
|
||||
old_root: Option<WidgetId>,
|
||||
/// The slot every chain bottoms out in, holding the output as a box.
|
||||
root_move: MoveIdx,
|
||||
/// Whether the output has changed since the last update. A frame is
|
||||
/// owed for that whether or not anything has to be drawn again.
|
||||
resized: bool,
|
||||
@@ -50,6 +55,9 @@ pub struct UiRenderState {
|
||||
/// Whether this frame contains a declared-length change, so any dirty
|
||||
/// dependent replaces its answer too.
|
||||
replace_answers: bool,
|
||||
/// Widgets waiting for an ancestor to draw them, so the walk down the
|
||||
/// depths does not pick one up again at its own depth.
|
||||
deferred: crate::util::HashSet<WidgetId>,
|
||||
pub moves: Moves,
|
||||
}
|
||||
|
||||
@@ -63,50 +71,46 @@ impl UiRenderState {
|
||||
slots: Default::default(),
|
||||
answer_invalid: Default::default(),
|
||||
replace_answers: false,
|
||||
deferred: Default::default(),
|
||||
moves: Default::default(),
|
||||
root_move: MoveIdx::NONE,
|
||||
resized: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// The window as a box, so a chain bottoms out in one rather than in a
|
||||
/// multiplication applied after it. Composing through a box held in
|
||||
/// pixels leaves everything below it in pixels, which is why nothing
|
||||
/// downstream has to know the output's size to resolve a position.
|
||||
fn write_root(&mut self) {
|
||||
let region = UiRegion::new(
|
||||
UiSpan::new(Len::ZERO, Len::from_parts(Rel::ZERO, self.output_size.x)),
|
||||
UiSpan::new(Len::ZERO, Len::from_parts(Rel::ZERO, self.output_size.y)),
|
||||
);
|
||||
match self.root_move == MoveIdx::NONE {
|
||||
true => self.root_move = self.moves.push(MoveIdx::NONE, region),
|
||||
false => self.moves.set(self.root_move, region),
|
||||
}
|
||||
}
|
||||
|
||||
/// The window, in whatever the platform measures it in, onto the grid
|
||||
/// everything below it is decided on.
|
||||
/// everything below it is decided on. No move entry holds it: a chain
|
||||
/// bottoms out in `MoveIdx::NONE`, which is the window, and the window's
|
||||
/// size is applied where a fraction becomes pixels -- here in `to_px`,
|
||||
/// and in the shader by its uniform. A resize therefore rewrites no
|
||||
/// retained entry at all.
|
||||
pub fn resize(&mut self, size: impl Into<Vec2>) {
|
||||
let size = PxVec2::from_f32(size.into());
|
||||
if size == self.output_size {
|
||||
return;
|
||||
}
|
||||
self.output_size = size;
|
||||
self.write_root();
|
||||
self.resized = true;
|
||||
}
|
||||
|
||||
fn root_info(&self) -> DrawInfo {
|
||||
/// The root is asked about in the output: the window is where a fraction
|
||||
/// becomes pixels rather than a box of its own, so the root's box is the
|
||||
/// first length threaded down. Its own rules narrow that box, and where
|
||||
/// they do the narrowed box is also the offer -- nothing above it chose
|
||||
/// anything else.
|
||||
fn root_info(&self, region: UiRegion) -> DrawInfo {
|
||||
let px = region.size().to_px(self.output_size);
|
||||
DrawInfo {
|
||||
layer: 0,
|
||||
parent: None,
|
||||
depth: 1,
|
||||
parent_move: self.root_move,
|
||||
parent_move: MoveIdx::NONE,
|
||||
region_node: false,
|
||||
mask: MaskIdx::NONE,
|
||||
offer: UiRegion::FULL,
|
||||
offered_px: self.output_size,
|
||||
align: None,
|
||||
given_len: region.size(),
|
||||
offer_len: UiVec2::FULL_SIZE,
|
||||
px,
|
||||
offered_px: px,
|
||||
decided: [false; 2],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,8 +150,8 @@ impl UiRenderState {
|
||||
// length, found the way every other box change is found. Before
|
||||
// anything dirty settles, so that whatever a new output draws
|
||||
// again is drawn once, in the box it will have.
|
||||
let info = self.root_info();
|
||||
let region = Self::root_region(root.id(), rsc.widgets());
|
||||
let info = self.root_info(region);
|
||||
let answer = self.draw_inner(root.id(), region, info, None, rsc);
|
||||
self.active.get_mut(&root.id()).unwrap().answer = answer;
|
||||
}
|
||||
@@ -163,11 +167,9 @@ impl UiRenderState {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
let _layout = diag::timer(TimerKind::FullLayout);
|
||||
self.clear(rsc);
|
||||
// free all resources & cache
|
||||
self.write_root();
|
||||
if let Some(id) = root {
|
||||
let info = self.root_info();
|
||||
let region = Self::root_region(id.id(), rsc.widgets());
|
||||
let info = self.root_info(region);
|
||||
self.draw_inner(id.id(), region, info, None, rsc);
|
||||
}
|
||||
}
|
||||
@@ -191,88 +193,87 @@ impl UiRenderState {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
{
|
||||
diag::bump(Counter::DrawRequests);
|
||||
diag::draw_request(
|
||||
id,
|
||||
info.parent,
|
||||
region,
|
||||
self.px_of(info.parent_move, region),
|
||||
info.region_node,
|
||||
);
|
||||
diag::draw_request(id, info.parent, region, info.px, info.region_node);
|
||||
}
|
||||
let own_align = rsc.widgets().alignment(id);
|
||||
let align = info.align.unwrap_or(own_align);
|
||||
let replace_answer = self.answer_invalid.remove(&id)
|
||||
|| (self.replace_answers
|
||||
&& (rsc.widgets().needs_redraw.contains(&id)
|
||||
|| self.dirty_size_under(id, rsc.widgets())));
|
||||
let retained = match replace_answer {
|
||||
let align = rsc.widgets().alignment(id);
|
||||
// Nothing this widget has is an answer while something it measured
|
||||
// is dirty: settling that changes what it would report, and a widget
|
||||
// settled inside its parent's draw tells nobody -- the comparison
|
||||
// that marks a reader is in `redraw`, which is not what asked here.
|
||||
// Both retained routes are an answer, so the question is asked once
|
||||
// rather than by each of them.
|
||||
let stale =
|
||||
rsc.widgets().needs_redraw.contains(&id) || self.dirty_size_under(id, rsc.widgets());
|
||||
let replace_answer = self.answer_invalid.remove(&id) || (self.replace_answers && stale);
|
||||
let retained = match replace_answer || stale {
|
||||
true => None,
|
||||
false => self
|
||||
.retained_answer(id, region, info, rsc.widgets())
|
||||
.retained_answer(id, info)
|
||||
.or_else(|| self.try_reuse(id, region, info, rsc)),
|
||||
};
|
||||
let answer = retained.unwrap_or_else(|| {
|
||||
if old.is_none() {
|
||||
old = self.remove(id, false, rsc);
|
||||
}
|
||||
self.draw_at(id, region, info, align, old.take(), rsc)
|
||||
self.draw_at(id, region, info, old.take(), rsc)
|
||||
});
|
||||
|
||||
let declared = declared_lens(rsc.widgets(), id);
|
||||
// A near-edge override means the caller already chose this box from
|
||||
// the child's answer. Applying the answer again would compound the
|
||||
// placement; it is also how the second, final ask terminates.
|
||||
let placed = match info.align == Some(RegionAlign::NEAR) {
|
||||
true => region,
|
||||
false => placed_box(region, answer.0, align, declared),
|
||||
};
|
||||
// The second, final ask is in a box chosen from the answer on both
|
||||
// axes, which is also what makes it terminate.
|
||||
let lens = placed_lens(answer.0, declared, info.decided);
|
||||
let placed = placed_box(region, lens, align);
|
||||
let placed_info = DrawInfo {
|
||||
align: Some(RegionAlign::NEAR),
|
||||
px: lens.to_px(info.px),
|
||||
decided: [true; 2],
|
||||
..info
|
||||
};
|
||||
// The symbolic box can be unchanged while its parent slot changed
|
||||
// pixel size. Reuse checks the resolved box even in that case.
|
||||
if self.try_reuse(id, placed, placed_info, rsc).is_none() {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::PlaceRedraws);
|
||||
let old = self.remove(id, false, rsc);
|
||||
self.draw_at(id, placed, placed_info, RegionAlign::NEAR, old, rsc);
|
||||
}
|
||||
self.place(id, placed, placed_info, rsc);
|
||||
|
||||
// The answer is only reusable while both parts of the operation are:
|
||||
// what the widget reported in the offered box, and what it drew in
|
||||
// the box its report selected. Express the latter's contract back in
|
||||
// terms of the offered box before handing it to the parent.
|
||||
// what the widget reported in the box it was asked in, and what it
|
||||
// drew in the box its report selected. Express the latter's contract
|
||||
// back in terms of the box asked in before handing it to the parent.
|
||||
let drawing_holds = self.active[&id].holds;
|
||||
let mut settled = answer;
|
||||
for axis in AXES {
|
||||
let reported = answer.0.axis(axis);
|
||||
let placed_len =
|
||||
match reported.leftover != Weight::ZERO || declared[axis as usize].is_some() {
|
||||
true => Len::FULL,
|
||||
false => Len::from_parts(reported.rel, reported.px),
|
||||
};
|
||||
settled.1[axis as usize] =
|
||||
settled.1[axis as usize].and(drawing_holds[axis as usize].through(placed_len));
|
||||
settled.1[axis as usize].and(drawing_holds[axis as usize].through(lens.axis(axis)));
|
||||
}
|
||||
|
||||
let active = self.active.get_mut(&id).unwrap();
|
||||
active.offer = info.offer;
|
||||
// Whoever asked owns how the box was reached: the box it stated, and
|
||||
// what of that box the answer then took. A local redraw asks the
|
||||
// same question again from these.
|
||||
active.given = region;
|
||||
active.given_len = info.given_len;
|
||||
active.offer_len = info.offer_len;
|
||||
active.answer = settled;
|
||||
active.align = align;
|
||||
active.align_override = info.align.is_some();
|
||||
active.own_align = own_align;
|
||||
active.decided = info.decided;
|
||||
active.own_align = align;
|
||||
active.depth = info.depth;
|
||||
settled
|
||||
}
|
||||
|
||||
/// Draws a widget in the final box its answer chose, reusing the drawing
|
||||
/// already there where its retained contract holds for that box. The
|
||||
/// symbolic box can be unchanged while the box it sits in changed pixel
|
||||
/// length, so what reuse checks is the box in pixels.
|
||||
fn place(&mut self, id: WidgetId, placed: UiRegion, info: DrawInfo, rsc: &mut dyn UiRsc) {
|
||||
if self.try_reuse(id, placed, info, rsc).is_none() {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::PlaceRedraws);
|
||||
let old = self.remove(id, false, rsc);
|
||||
self.draw_at(id, placed, info, old, rsc);
|
||||
}
|
||||
}
|
||||
|
||||
/// Calls a widget's `draw` and keeps what it drew in `region`.
|
||||
fn draw_at(
|
||||
&mut self,
|
||||
id: WidgetId,
|
||||
region: UiRegion,
|
||||
info: DrawInfo,
|
||||
align: RegionAlign,
|
||||
old: Option<ActiveData>,
|
||||
rsc: &mut dyn UiRsc,
|
||||
) -> (Size, [Holds; 2]) {
|
||||
@@ -293,12 +294,17 @@ impl UiRenderState {
|
||||
None => (Vec::new(), None),
|
||||
};
|
||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
||||
let px = self.px_of(move_idx, local);
|
||||
let at_offer = same_px(px, info.offered_px);
|
||||
// A box of the offered lengths asks the offer's question wherever it
|
||||
// sits, since what a drawing depends on is its lengths -- and
|
||||
// equality is the comparison, these being counts of a step rather
|
||||
// than floats to be compared for nearness.
|
||||
let px = info.px;
|
||||
let at_offer = px == info.offered_px;
|
||||
|
||||
let mut painter = Painter {
|
||||
state: self,
|
||||
region: local,
|
||||
px,
|
||||
mask: info.mask,
|
||||
layer: info.layer,
|
||||
own_layer: info.layer,
|
||||
@@ -332,6 +338,7 @@ impl UiRenderState {
|
||||
state: _,
|
||||
rsc: _,
|
||||
region: _,
|
||||
px: _,
|
||||
mask,
|
||||
textures,
|
||||
primitives,
|
||||
@@ -401,9 +408,11 @@ impl UiRenderState {
|
||||
parent_move: move_idx,
|
||||
region_node: false,
|
||||
mask,
|
||||
offer: UiRegion::FULL,
|
||||
given_len: UiVec2::FULL_SIZE,
|
||||
offer_len: UiVec2::FULL_SIZE,
|
||||
px,
|
||||
offered_px: px,
|
||||
align: None,
|
||||
decided: [false; 2],
|
||||
},
|
||||
rsc,
|
||||
);
|
||||
@@ -414,7 +423,12 @@ impl UiRenderState {
|
||||
let active = ActiveData {
|
||||
id,
|
||||
region,
|
||||
offer: info.offer,
|
||||
// The box a placing ask draws in is a part of the one its parent
|
||||
// gave, which `draw_inner` writes back over these once the
|
||||
// placement is done.
|
||||
given: region,
|
||||
given_len: info.given_len,
|
||||
offer_len: info.offer_len,
|
||||
// Whoever asked writes the answer, if this was the asking.
|
||||
answer: old_answer.unwrap_or((size, holds)),
|
||||
size,
|
||||
@@ -427,12 +441,12 @@ impl UiRenderState {
|
||||
children,
|
||||
size_deps,
|
||||
declared: declared_lens(rsc.widgets(), id),
|
||||
align,
|
||||
align_override: info.align.is_some(),
|
||||
decided: info.decided,
|
||||
own_align: rsc.widgets().alignment(id),
|
||||
move_idx,
|
||||
parent_move: info.parent_move,
|
||||
mask,
|
||||
parent_mask: info.mask,
|
||||
layer: info.layer,
|
||||
};
|
||||
rsc.on_draw(&active);
|
||||
@@ -460,14 +474,6 @@ impl UiRenderState {
|
||||
}
|
||||
}
|
||||
|
||||
/// The pixel size of a region held in `slot`'s coordinates.
|
||||
pub(super) fn px_of(&self, slot: MoveIdx, region: UiRegion) -> PxVec2 {
|
||||
self.moves
|
||||
.resolve(slot, region)
|
||||
.size()
|
||||
.to_px(self.output_size)
|
||||
}
|
||||
|
||||
/// A clean widget's retained answer, if that answer holds for a box of
|
||||
/// `px`. This does not move its drawing, which may already be in the box
|
||||
/// that answer placed it in.
|
||||
@@ -492,17 +498,9 @@ impl UiRenderState {
|
||||
|
||||
/// The answer to an ask can be retained independently of where its
|
||||
/// drawing ended up. Alignment is exactly that case: the first box is the
|
||||
/// question and the smaller placed box holds the drawing.
|
||||
fn retained_answer(
|
||||
&self,
|
||||
id: WidgetId,
|
||||
region: UiRegion,
|
||||
info: DrawInfo,
|
||||
widgets: &Widgets,
|
||||
) -> Option<(Size, [Holds; 2])> {
|
||||
if widgets.needs_redraw.contains(&id) || self.dirty_size_under(id, widgets) {
|
||||
return None;
|
||||
}
|
||||
/// question and the smaller placed box holds the drawing. Whether the
|
||||
/// answer is stale at all is its caller's question, asked once there.
|
||||
fn retained_answer(&self, id: WidgetId, info: DrawInfo) -> Option<(Size, [Holds; 2])> {
|
||||
let active = self.active.get(&id)?;
|
||||
let has_region_node = active.move_idx != active.parent_move;
|
||||
if !active.drawn
|
||||
@@ -511,15 +509,16 @@ impl UiRenderState {
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let px = self.px_of(info.parent_move, region);
|
||||
let (size, holds) = active.answer;
|
||||
(holds[0].contains(px.x) && holds[1].contains(px.y)).then_some((size, holds))
|
||||
(holds[0].contains(info.px.x) && holds[1].contains(info.px.y)).then_some((size, holds))
|
||||
}
|
||||
|
||||
/// Whether anything whose size this widget's own size was read from is
|
||||
/// dirty. Not needed for the answer to come right -- a changed size
|
||||
/// reaches its reader in any order -- but a reader that asks first
|
||||
/// lays out once rather than twice.
|
||||
/// dirty, which makes what it would answer not yet known. It also keeps
|
||||
/// a reader that asks first from laying out twice, which is all it was
|
||||
/// here for while a changed size was thought to reach its reader in any
|
||||
/// order; it does not, where the change settles inside the reader's own
|
||||
/// draw.
|
||||
fn dirty_size_under(&self, id: WidgetId, widgets: &Widgets) -> bool {
|
||||
self.active.get(&id).is_some_and(|active| {
|
||||
active.size_deps.iter().any(|child| {
|
||||
@@ -528,31 +527,39 @@ impl UiRenderState {
|
||||
})
|
||||
}
|
||||
|
||||
/// The first box a widget was asked about, re-expressed in the coordinate
|
||||
/// space its drawing uses. Keeping the relative box and composing it
|
||||
/// again avoids rebuilding a shifted box from rounded pixel lengths.
|
||||
fn offered_region(&self, id: WidgetId) -> UiRegion {
|
||||
/// The pixel lengths of the box a widget was given and of the box it was
|
||||
/// first asked about, which is what a local redraw needs to ask the
|
||||
/// question its parent asked.
|
||||
///
|
||||
/// Both are threaded down from the window a length of a box at a time,
|
||||
/// and this takes the same steps back up: a widget's box is a length of
|
||||
/// the box its parent drew in, and its offer a length of the box its
|
||||
/// parent was itself offered. Neither chain has a coordinate frame in it,
|
||||
/// so neither breaks at a region node -- and both land on the numbers a
|
||||
/// cold layout computes, rather than near them.
|
||||
fn asked_px(&self, id: WidgetId) -> (PxVec2, PxVec2) {
|
||||
let active = &self.active[&id];
|
||||
let parent_region = match active.parent.and_then(|id| self.active.get(&id)) {
|
||||
Some(parent) if parent.move_idx == active.parent_move => {
|
||||
if parent.move_idx == parent.parent_move {
|
||||
self.offered_region(parent.id)
|
||||
} else {
|
||||
UiRegion::FULL
|
||||
// Nothing above the root: the window is where a fraction becomes
|
||||
// pixels, which is also the whole of the box the root is given.
|
||||
let (parent_px, parent_offer) = match active.parent.and_then(|p| self.active.get(&p)) {
|
||||
Some(parent) => {
|
||||
let (given, offer) = self.asked_px(parent.id);
|
||||
let lens = placed_lens(parent.answer.0, parent.declared, parent.decided);
|
||||
(lens.to_px(given), offer)
|
||||
}
|
||||
}
|
||||
_ => UiRegion::FULL,
|
||||
};
|
||||
let mut offered = match active.offer == UiRegion::FULL {
|
||||
true => parent_region,
|
||||
false => active.offer.within(&parent_region),
|
||||
None => (self.output_size, self.output_size),
|
||||
};
|
||||
let px = active.given_len.to_px(parent_px);
|
||||
let mut offered = active.offer_len.to_px(parent_offer);
|
||||
for axis in AXES {
|
||||
// A declared length is resolved by whoever drew the widget, in
|
||||
// the box that widget drew in, so the box it has is the box it
|
||||
// was asked about however the offer above it moved.
|
||||
if active.declared[axis as usize].is_some() {
|
||||
*offered.axis_mut(axis) = *active.region.axis(axis);
|
||||
*offered.axis_mut(axis) = px.axis(axis);
|
||||
}
|
||||
}
|
||||
offered
|
||||
(px, offered)
|
||||
}
|
||||
|
||||
/// Reuses the actual drawing in a new box if its retained contract holds
|
||||
@@ -609,10 +616,10 @@ impl UiRenderState {
|
||||
}
|
||||
return None;
|
||||
}
|
||||
// In pixels, because `region` is a fraction of a slot's box and that
|
||||
// box may be what changed -- an unchanged fraction of a box half the
|
||||
// size is half the widget.
|
||||
if !active.holds_at(self.px_of(info.parent_move, region)) {
|
||||
// In pixels, because `region` is a fraction of the box its parent
|
||||
// drew in and that box may be what changed -- an unchanged fraction
|
||||
// of a box half the size is half the widget.
|
||||
if !active.holds_at(info.px) {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
{
|
||||
diag::bump(Counter::ReuseOutside);
|
||||
@@ -621,23 +628,21 @@ impl UiRenderState {
|
||||
return None;
|
||||
}
|
||||
let moved = active.region != region;
|
||||
let (answer, old_region, slot, mask) = (
|
||||
(active.size, active.holds),
|
||||
active.region,
|
||||
active.move_idx,
|
||||
info.mask,
|
||||
);
|
||||
let (answer, old_region, slot) =
|
||||
((active.size, active.holds), active.region, active.move_idx);
|
||||
if moved {
|
||||
if has_region_node {
|
||||
self.moves.set(slot, region);
|
||||
} else {
|
||||
let remap = RegionRemap::new(old_region, region)?;
|
||||
self.remap_subtree(id, remap, info.parent_move, mask, rsc);
|
||||
self.remap_subtree(id, &remap, info.parent_move, rsc);
|
||||
}
|
||||
}
|
||||
let active = self.active.get_mut(&id).unwrap();
|
||||
active.region = region;
|
||||
active.offer = info.offer;
|
||||
active.given = region;
|
||||
active.given_len = info.given_len;
|
||||
active.offer_len = info.offer_len;
|
||||
active.depth = info.depth;
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
{
|
||||
@@ -668,12 +673,12 @@ impl UiRenderState {
|
||||
fn remap_subtree(
|
||||
&mut self,
|
||||
id: WidgetId,
|
||||
remap: RegionRemap,
|
||||
remap: &RegionRemap,
|
||||
parent_move: MoveIdx,
|
||||
inherited_mask: MaskIdx,
|
||||
rsc: &mut dyn UiRsc,
|
||||
) {
|
||||
let active = self.active.get_mut(&id).unwrap();
|
||||
active.given = remap.apply(active.given);
|
||||
if active.move_idx != parent_move {
|
||||
let region = remap.apply(active.region);
|
||||
active.region = region;
|
||||
@@ -685,16 +690,18 @@ impl UiRenderState {
|
||||
*region = remap.apply(*region);
|
||||
}
|
||||
active.region = remap.apply(active.region);
|
||||
let mask = active.mask;
|
||||
let own_mask = (active.mask != active.parent_mask).then_some(active.mask);
|
||||
let children = active.children.len();
|
||||
if mask != inherited_mask && mask != MaskIdx::NONE {
|
||||
let mask = rsc.ui_mut().masks.get_mut(mask);
|
||||
// A mask the widget set itself moves with it; one it inherited
|
||||
// belongs to the widget that set it, and moves there or not at all.
|
||||
if let Some(idx) = own_mask {
|
||||
let mask = rsc.ui_mut().masks.get_mut(idx);
|
||||
debug_assert_eq!(mask.move_idx, parent_move);
|
||||
mask.region = remap.apply(mask.region);
|
||||
}
|
||||
for index in 0..children {
|
||||
let child = self.active[&id].children[index];
|
||||
self.remap_subtree(child, remap, parent_move, mask, rsc);
|
||||
self.remap_subtree(child, remap, parent_move, rsc);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -769,7 +776,9 @@ impl UiRenderState {
|
||||
ActiveData {
|
||||
id,
|
||||
region: UiRegion::FULL,
|
||||
offer: UiRegion::FULL,
|
||||
given: UiRegion::FULL,
|
||||
given_len: UiVec2::FULL_SIZE,
|
||||
offer_len: UiVec2::FULL_SIZE,
|
||||
answer: (size, [Holds::ANY; 2]),
|
||||
size,
|
||||
holds: [Holds::ANY; 2],
|
||||
@@ -782,11 +791,11 @@ impl UiRenderState {
|
||||
size_deps: Vec::new(),
|
||||
move_idx: info.parent_move,
|
||||
declared: [None; 2],
|
||||
align: RegionAlign::default(),
|
||||
align_override: false,
|
||||
decided: [false; 2],
|
||||
own_align: rsc.widgets().alignment(id),
|
||||
parent_move: info.parent_move,
|
||||
mask: info.mask,
|
||||
parent_mask: info.mask,
|
||||
layer: info.layer,
|
||||
},
|
||||
);
|
||||
@@ -802,7 +811,6 @@ impl UiRenderState {
|
||||
self.answer_invalid.clear();
|
||||
self.replace_answers = false;
|
||||
self.moves.clear();
|
||||
self.root_move = MoveIdx::NONE;
|
||||
self.layers.clear();
|
||||
rsc.widgets_mut().needs_redraw.clear();
|
||||
self.free(rsc);
|
||||
@@ -823,19 +831,34 @@ impl UiRenderState {
|
||||
pub fn redraw_updates(&mut self, rsc: &mut dyn UiRsc) {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
let _layout = diag::timer(TimerKind::IncrementalLayout);
|
||||
// Deepest first: a reader whose children have all settled asks each
|
||||
// once, where any other order has it lay out again for whatever
|
||||
// settles under it afterwards. Equal-depth widgets are independent,
|
||||
// so their order does not matter.
|
||||
while let Some(id) = {
|
||||
let dirty = rsc.widgets().needs_redraw.iter().copied();
|
||||
dirty.max_by_key(|&id| self.depth(id))
|
||||
} {
|
||||
// Deepest first, and strictly: a widget that cannot settle where it
|
||||
// is defers to its parent rather than drawing the parent from
|
||||
// inside itself. It marks the parent, stays marked, and waits here
|
||||
// until the walk reaches its parent's depth.
|
||||
//
|
||||
// What that buys is that nothing shallower is ever drawn while
|
||||
// anything deeper is still dirty. A parent drawing can therefore
|
||||
// trust every answer it reads without descending to check whether
|
||||
// something below is about to change it -- which is the whole class
|
||||
// of defect where a widget settles inside its parent's draw, clears
|
||||
// its mark there, and tells nobody its answer moved.
|
||||
loop {
|
||||
let next = rsc
|
||||
.widgets()
|
||||
.needs_redraw
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|id| !self.deferred.contains(id))
|
||||
.max_by_key(|&id| self.depth(id));
|
||||
let Some(id) = next else { break };
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::QueuePops);
|
||||
self.redraw(id, rsc);
|
||||
if !self.redraw(id, rsc) {
|
||||
self.deferred.insert(id);
|
||||
}
|
||||
}
|
||||
self.deferred.clear();
|
||||
}
|
||||
|
||||
fn depth(&self, id: WidgetId) -> usize {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
@@ -903,23 +926,29 @@ impl UiRenderState {
|
||||
}
|
||||
|
||||
/// Where a widget is on screen: its box composed through the boxes it
|
||||
/// sits within, which is the walk the vertex shader does. `None` for one
|
||||
/// that is not drawn.
|
||||
/// sits within, the same walk the vertex shader does. `None` for one that
|
||||
/// is not drawn.
|
||||
///
|
||||
/// This is for asking where a drawing landed: hit testing, and a test
|
||||
/// reading a box back. Layout decides on the lengths threaded down the
|
||||
/// draw instead, and a position is not one of its inputs.
|
||||
pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> {
|
||||
let active = self.active.get(&id.id())?;
|
||||
if !active.drawn {
|
||||
return None;
|
||||
}
|
||||
let region = self.moves.resolve(active.parent_move, active.region);
|
||||
Some(region.to_px(self.output_size))
|
||||
active.drawn.then(|| {
|
||||
self.moves
|
||||
.resolve(active.parent_move, active.region)
|
||||
.to_px(self.output_size)
|
||||
})
|
||||
}
|
||||
|
||||
/// Settles a dirty widget: asks it again where its parent asked, and
|
||||
/// tells the parent if the answer changed.
|
||||
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) {
|
||||
/// tells the parent if the answer changed. `false` where the question is
|
||||
/// its parent's rather than its own, which leaves it marked for the
|
||||
/// parent to draw when the walk reaches that depth.
|
||||
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) -> bool {
|
||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
||||
let Some(active) = self.active.get(&id) else {
|
||||
return;
|
||||
return true;
|
||||
};
|
||||
// Its parent resolved its declared lengths into its box and decided
|
||||
// whether to draw it at all, so a change to either is the parent's
|
||||
@@ -939,86 +968,74 @@ impl UiRenderState {
|
||||
at = self.active[&next].parent;
|
||||
}
|
||||
}
|
||||
// Both stay marked: the parent because it has this to draw, and
|
||||
// this because the parent must draw it rather than keep what it
|
||||
// has. The mark comes off in `draw_at`, where the parent draws.
|
||||
rsc.widgets_mut().needs_redraw.insert(id);
|
||||
self.redraw(parent, rsc);
|
||||
// Whatever the parent did not draw again is nothing it holds now.
|
||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
||||
return;
|
||||
rsc.widgets_mut().needs_redraw.insert(parent);
|
||||
return false;
|
||||
}
|
||||
if !active.drawn {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
let region = active.region;
|
||||
let region_node = active.parent.is_some() && rsc.widgets().is_region_node(id);
|
||||
let asked_in = match active.parent {
|
||||
Some(_) => self.offered_region(id),
|
||||
None => Self::root_region(id, rsc.widgets()),
|
||||
// Nothing above the root resolved its rules or its alignment, so its
|
||||
// box is its own to work out again against the output. Every other
|
||||
// widget was given one.
|
||||
let Some(parent) = active.parent else {
|
||||
let region = Self::root_region(id, rsc.widgets());
|
||||
let info = DrawInfo {
|
||||
mask: active.parent_mask,
|
||||
..self.root_info(region)
|
||||
};
|
||||
let offered_px = self.px_of(active.parent_move, asked_in);
|
||||
let at_offer = same_px(self.px_of(active.parent_move, region), offered_px);
|
||||
let parent_must_place = active.parent.is_some()
|
||||
&& (!region_node || active.align_override)
|
||||
&& !same_pixel_region(
|
||||
self.moves
|
||||
.resolve(active.parent_move, region)
|
||||
.to_px(self.output_size),
|
||||
self.moves
|
||||
.resolve(active.parent_move, asked_in)
|
||||
.to_px(self.output_size),
|
||||
);
|
||||
// An independently positioned region node can redraw at its offer
|
||||
// and move its slot to its own placement. Every other widget needs
|
||||
// its parent to reproduce a different final position.
|
||||
if let Some(parent) = active.parent
|
||||
&& parent_must_place
|
||||
{
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::LocalRedraws);
|
||||
let old = self.remove(id, false, rsc);
|
||||
self.draw_inner(id, region, info, old, rsc);
|
||||
return true;
|
||||
};
|
||||
let (given_px, offered_px) = self.asked_px(id);
|
||||
// Asked again in the box its parent gave it, which is the question
|
||||
// its parent asked only while that box is as long as the offer. Any
|
||||
// other box is a different question, so the parent asks it, with the
|
||||
// mark left on. Lengths and not whole boxes: what a drawing depends
|
||||
// on is its lengths, so the same lengths elsewhere is one question.
|
||||
if given_px != offered_px {
|
||||
rsc.widgets_mut().needs_redraw.insert(id);
|
||||
self.redraw(parent, rsc);
|
||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
||||
return;
|
||||
rsc.widgets_mut().needs_redraw.insert(parent);
|
||||
return false;
|
||||
}
|
||||
let info = DrawInfo {
|
||||
layer: active.layer,
|
||||
parent: active.parent,
|
||||
depth: active.depth,
|
||||
parent_move: active.parent_move,
|
||||
region_node,
|
||||
mask: active.mask,
|
||||
offer: active.offer,
|
||||
region_node: rsc.widgets().is_region_node(id),
|
||||
mask: active.parent_mask,
|
||||
given_len: active.given_len,
|
||||
offer_len: active.offer_len,
|
||||
px: given_px,
|
||||
offered_px,
|
||||
align: active.align_override.then_some(active.align),
|
||||
decided: active.decided,
|
||||
};
|
||||
let (was_answer, was) = (active.answer, (active.size, active.holds));
|
||||
let (given, was_answer) = (active.given, active.answer);
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::LocalRedraws);
|
||||
|
||||
let old = self.remove(id, false, rsc);
|
||||
let answer = self.draw_inner(id, asked_in, info, old, rsc);
|
||||
self.active.get_mut(&id).unwrap().answer = answer;
|
||||
let Some(parent) = info.parent else {
|
||||
return;
|
||||
};
|
||||
// `draw_inner` places the answer inside that box itself, which is the
|
||||
// ask that leaves the widget where its parent put it.
|
||||
let answer = self.draw_inner(id, given, info, old, rsc);
|
||||
if answer != was_answer {
|
||||
// Left where it was asked: the parent lays out again and chooses
|
||||
// its final box.
|
||||
// Its parent chose its box knowing the old answer, so it lays out
|
||||
// again and chooses the box the new one asks for.
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
{
|
||||
diag::bump(Counter::SizeChanges);
|
||||
diag::bump(Counter::ReaderEdges);
|
||||
}
|
||||
rsc.widgets_mut().needs_redraw.insert(parent);
|
||||
return;
|
||||
}
|
||||
if at_offer {
|
||||
return;
|
||||
}
|
||||
// Then in the final box its parent chose from that answer. It is kept
|
||||
// if it holds there; otherwise its result is the parent's business.
|
||||
self.draw_inner(id, region, info, None, rsc);
|
||||
let active = &self.active[&id];
|
||||
if (active.size, active.holds) != was {
|
||||
rsc.widgets_mut().needs_redraw.insert(parent);
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1031,69 +1048,114 @@ fn within_box(size: Size, px: PxVec2, axis: Axis) -> bool {
|
||||
len.leftover != Weight::ZERO || box_len.mul(len.rel) + len.px <= box_len
|
||||
}
|
||||
|
||||
/// The same box is the same number of steps, both of these being lengths on
|
||||
/// the grid rather than floats to be compared for nearness.
|
||||
fn same_px(a: PxVec2, b: PxVec2) -> bool {
|
||||
a == b
|
||||
}
|
||||
|
||||
fn same_pixel_region(a: PixelRegion, b: PixelRegion) -> bool {
|
||||
same_px(a.top_left, b.top_left) && same_px(a.bot_right, b.bot_right)
|
||||
}
|
||||
|
||||
/// A retained region rewritten from one parent box into another. A fixed
|
||||
/// source extent can be translated but cannot recover fractions for a resize.
|
||||
#[derive(Clone, Copy)]
|
||||
struct RegionRemap {
|
||||
from: UiRegion,
|
||||
to: UiRegion,
|
||||
axes: [AxisRemap; 2],
|
||||
}
|
||||
|
||||
/// Moving one axis of a box into another, worked out once for the whole
|
||||
/// subtree that moves with it. Every part of that subtree is divided by the
|
||||
/// same extent and placed between the same two ends, so the ends and the
|
||||
/// divisor belong here rather than in each part's arithmetic.
|
||||
#[derive(Clone, Copy)]
|
||||
enum AxisRemap {
|
||||
/// A box that kept its length carries its parts by moving them, which is
|
||||
/// exact. Dividing to find the fraction each sits at and multiplying to
|
||||
/// place it again are two roundings, and they land a step from where
|
||||
/// growing the tree that way does.
|
||||
Translate(Len),
|
||||
/// A box that changed length has to re-express each part as a fraction of
|
||||
/// the new one, which is what a part of a box means.
|
||||
Scale(AxisScale),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct AxisScale {
|
||||
/// What the fraction is measured from, and what divides it. `whole` is
|
||||
/// the common case of a box spanning the whole of its parent's, where
|
||||
/// dividing by one is the expensive way to write a subtraction.
|
||||
start_rel: Rel,
|
||||
extent: Rel,
|
||||
whole: bool,
|
||||
/// `lerp` is `a + (b - a) * fraction`, and both ends are the same for
|
||||
/// every part, so each is kept as its near end and its span.
|
||||
from_px: Px,
|
||||
from_px_span: Px,
|
||||
to_rel: Rel,
|
||||
to_rel_span: Rel,
|
||||
to_px: Px,
|
||||
to_px_span: Px,
|
||||
}
|
||||
|
||||
impl RegionRemap {
|
||||
fn new(from: UiRegion, to: UiRegion) -> Option<Self> {
|
||||
AXES.into_iter()
|
||||
.all(|axis| {
|
||||
let from = from.axis(axis);
|
||||
from.start.rel != from.end.rel || from.len() == to.axis(axis).len()
|
||||
Some(Self {
|
||||
axes: [AxisRemap::new(from.x, to.x)?, AxisRemap::new(from.y, to.y)?],
|
||||
})
|
||||
.then_some(Self { from, to })
|
||||
}
|
||||
|
||||
fn apply(self, region: UiRegion) -> UiRegion {
|
||||
fn apply(&self, region: UiRegion) -> UiRegion {
|
||||
// A box that only moved carries every part of itself by the same two
|
||||
// amounts, and that is the common move. Asking it once for the whole
|
||||
// region is what lets it be eight adds in a row rather than four
|
||||
// sequences with a branch each -- measured, it is where the time in a
|
||||
// move goes.
|
||||
if let [AxisRemap::Translate(x), AxisRemap::Translate(y)] = self.axes {
|
||||
return region.translated(x, y);
|
||||
}
|
||||
UiRegion {
|
||||
x: self.apply_span(region.x, self.from.x, self.to.x),
|
||||
y: self.apply_span(region.y, self.from.y, self.to.y),
|
||||
x: self.axes[0].apply_span(region.x),
|
||||
y: self.axes[1].apply_span(region.y),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_span(self, span: UiSpan, from: UiSpan, to: UiSpan) -> UiSpan {
|
||||
UiSpan {
|
||||
start: self.apply_scalar(span.start, from, to),
|
||||
end: self.apply_scalar(span.end, from, to),
|
||||
impl AxisRemap {
|
||||
fn new(from: UiSpan, to: UiSpan) -> Option<Self> {
|
||||
if from.len() == to.len() {
|
||||
return Some(Self::Translate(to.start - from.start));
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_scalar(self, scalar: Len, from: UiSpan, to: UiSpan) -> Len {
|
||||
let extent = from.end.rel - from.start.rel;
|
||||
// A box that only moved, or that has no relative extent to divide,
|
||||
// carries its parts by moving them, which is exact. Dividing to find
|
||||
// the fraction each sits at and multiplying to place it again are two
|
||||
// roundings, and they land a step from where growing the tree that
|
||||
// way does. Where the box changed length there is nothing else to do,
|
||||
// and the fraction is what a part means.
|
||||
if from.len() == to.len() || extent == Rel::ZERO {
|
||||
return scalar + to.start - from.start;
|
||||
// Without a relative extent there is no fraction to re-express: a box
|
||||
// of fixed length cannot say where its parts sit in a different one.
|
||||
if extent == Rel::ZERO {
|
||||
return None;
|
||||
}
|
||||
// A box that spans the whole of its parent's is the common one, and
|
||||
// dividing by one is the expensive way to write a subtraction.
|
||||
let offset = scalar.rel - from.start.rel;
|
||||
let fraction = match extent == Rel::ONE {
|
||||
true => offset,
|
||||
false => offset / extent,
|
||||
Some(Self::Scale(AxisScale {
|
||||
start_rel: from.start.rel,
|
||||
extent,
|
||||
whole: extent == Rel::ONE,
|
||||
from_px: from.start.px,
|
||||
from_px_span: from.end.px - from.start.px,
|
||||
to_rel: to.start.rel,
|
||||
to_rel_span: to.end.rel - to.start.rel,
|
||||
to_px: to.start.px,
|
||||
to_px_span: to.end.px - to.start.px,
|
||||
}))
|
||||
}
|
||||
|
||||
fn apply_span(&self, span: UiSpan) -> UiSpan {
|
||||
UiSpan {
|
||||
start: self.apply_scalar(span.start),
|
||||
end: self.apply_scalar(span.end),
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_scalar(&self, scalar: Len) -> Len {
|
||||
let scale = match self {
|
||||
Self::Translate(by) => return scalar + *by,
|
||||
Self::Scale(scale) => scale,
|
||||
};
|
||||
let from_px = fraction.lerp(from.start.px, from.end.px);
|
||||
let to_rel = fraction.lerp(to.start.rel, to.end.rel);
|
||||
let to_px = fraction.lerp(to.start.px, to.end.px);
|
||||
let offset = scalar.rel - scale.start_rel;
|
||||
let fraction = match scale.whole {
|
||||
true => offset,
|
||||
false => offset / scale.extent,
|
||||
};
|
||||
let from_px = scale.from_px + scale.from_px_span.mul(fraction);
|
||||
let to_rel = scale.to_rel + scale.to_rel_span.mul(fraction);
|
||||
let to_px = scale.to_px + scale.to_px_span.mul(fraction);
|
||||
Len::from_parts(to_rel, scalar.px - from_px + to_px)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,10 +20,14 @@ impl DefaultAppState for Client {
|
||||
let pad_test = (
|
||||
rrect.color(Color::BLUE),
|
||||
(
|
||||
// The square is one widget and the two shares of the row it
|
||||
// sits centred in are another: a length is a property of a
|
||||
// widget, so `.width` here would overwrite the `.sized`.
|
||||
rrect
|
||||
.color(Color::RED)
|
||||
.sized((100, 100))
|
||||
.center()
|
||||
.wrapper()
|
||||
.width(leftover(2)),
|
||||
(
|
||||
rrect.color(Color::ORANGE),
|
||||
@@ -143,7 +147,7 @@ impl DefaultAppState for Client {
|
||||
.span(Dir::DOWN)
|
||||
.add(rsc);
|
||||
|
||||
let main = WidgetPtr::new().add(rsc);
|
||||
let main = Wrapper::new().add(rsc);
|
||||
|
||||
let vals = Rc::new(RefCell::new((0, Vec::new())));
|
||||
let mut switch_button = |color, to: WeakWidget, label| {
|
||||
|
||||
+7
-3
@@ -28,10 +28,14 @@ impl DefaultAppState for State {
|
||||
.pad(16)
|
||||
.background(panel());
|
||||
|
||||
// Each one takes the whole width, because `text_align` puts the
|
||||
// glyphs somewhere in the box the text is given and a text that
|
||||
// reports the width of its own glyphs is given exactly that.
|
||||
let label = |text: &str, align| wtext(text).size(24).text_align(align).width(rel(1.0));
|
||||
let aligned = (
|
||||
wtext("left").size(24).text_align(Align::LEFT),
|
||||
wtext("centred").size(24).text_align(Align::CENTER),
|
||||
wtext("right").size(24).text_align(Align::RIGHT),
|
||||
label("left", Align::LEFT),
|
||||
label("centred", Align::H_CENTER),
|
||||
label("right", Align::RIGHT),
|
||||
)
|
||||
.span(Dir::DOWN)
|
||||
.gap(8)
|
||||
|
||||
+693
-154
@@ -29,6 +29,17 @@ pub struct Edits {
|
||||
/// one. Region nodes change what a move writes and how deep a primitive's
|
||||
/// chain is, so a tree that never grows one leaves both untested.
|
||||
pub nodes: HashMap<usize, bool>,
|
||||
/// Whether a [`Branch`] takes the side it would take at any measurement,
|
||||
/// rather than the side the one it made says. The oracle wants the
|
||||
/// measured side -- that is the whole point of a branch, and how a widget
|
||||
/// believing a measurement a cold start would not have given it becomes a
|
||||
/// different tree. A rig measuring cost wants this instead: a fixture
|
||||
/// whose shape moves with the thing being measured cannot be compared
|
||||
/// with itself across a change to it, and seed 1 at depth 8 went from 88
|
||||
/// drawn widgets and 2,298 primitive writes a frame to 115 and 8,209
|
||||
/// across fixed point, which is three and a half times the work behind a
|
||||
/// number read as three and a half times the cost.
|
||||
pub fixed_branches: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
@@ -90,10 +101,6 @@ pub struct Tree {
|
||||
pub nodes: Vec<WidgetId>,
|
||||
pub spans: Vec<Spanned>,
|
||||
pub scrolls: Vec<WeakWidget<Scroll>>,
|
||||
/// Children a `SpanEdit` took out, held so that dropping the last share
|
||||
/// of one does not free its id for the next widget to be given -- which
|
||||
/// would put the two trees' `ids` out of step.
|
||||
pub detached: Vec<StrongWidget>,
|
||||
}
|
||||
|
||||
/// Branches on a child's measured length. Comparing boxes catches a widget
|
||||
@@ -127,15 +134,463 @@ impl Widget for Branch {
|
||||
|
||||
pub struct Spanned {
|
||||
pub id: WeakWidget<Span>,
|
||||
/// Leaves grown with the span whether or not they end up in it, so both
|
||||
/// trees make the same widgets in the same order either way. Attaching
|
||||
/// one moves it out of here: a widget belongs to one parent, and one that
|
||||
/// belongs to nobody still has to be held or it reads as a leak.
|
||||
/// Everything made for this span that it does not hold -- spares never
|
||||
/// attached and children detached alike. A widget belongs to one parent,
|
||||
/// and one that belongs to nobody still has to be held here: dropping
|
||||
/// the last share of it frees its id for the next widget to be given,
|
||||
/// which puts two trees out of step.
|
||||
pub spares: Vec<StrongWidget>,
|
||||
/// How many children it was grown with, before any edit.
|
||||
pub grown: usize,
|
||||
}
|
||||
|
||||
/// A tree described rather than built: [`plan`] turns a seed into one of
|
||||
/// these and [`build`] turns it into widgets, where growing did both at once.
|
||||
///
|
||||
/// The split is what makes a counterexample readable. A failing seed used to
|
||||
/// be the entire record of one, because a grower that makes widgets as it
|
||||
/// draws leaves nothing to take apart -- a shrinker could only grow its own
|
||||
/// trees and hope to meet the same shape, which in practice it does not. A
|
||||
/// plan is reduced by [`Plan::smaller`] and built again, so any seed that
|
||||
/// fails can be cut down until what is left is small enough to read.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Plan {
|
||||
pub kind: Kind,
|
||||
/// The declared size this widget carries. Whoever grows a widget offers
|
||||
/// it one and the offer is taken or declined; a second offer to the same
|
||||
/// widget is dropped, because two rules on one widget would settle in the
|
||||
/// order they were applied rather than in grow order.
|
||||
pub size: Option<Lens>,
|
||||
/// The alignment it carries, under the same one-offer rule.
|
||||
pub align: Option<Aligns>,
|
||||
/// Whether it was offered a movable region of its own and what it
|
||||
/// answered. `Some(false)` is an offer declined, which still uses up the
|
||||
/// one offer, where `None` is an offer never made.
|
||||
pub region_node: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Kind {
|
||||
/// Wrapped and unwrapped text, because only one of them reads the width
|
||||
/// it is given and so only one has to be drawn again for a new one.
|
||||
Wrapped,
|
||||
OneLine,
|
||||
Rect {
|
||||
color: usize,
|
||||
alpha: u8,
|
||||
},
|
||||
/// Scrolling reads the pixel length of its box, which nothing else here
|
||||
/// does, and gives its child a box longer than its own.
|
||||
Scroll {
|
||||
axis: Axis,
|
||||
inner: Box<Plan>,
|
||||
},
|
||||
/// All three sides are grown either way, so a tree that draws one has the
|
||||
/// same ids as a tree that draws another.
|
||||
Branch {
|
||||
probe: Box<Plan>,
|
||||
wide: Box<Plan>,
|
||||
narrow: Box<Plan>,
|
||||
threshold: f32,
|
||||
},
|
||||
/// Each side its own, since a padding that is the same all round hides
|
||||
/// anything that treats one edge differently from another.
|
||||
Pad {
|
||||
padding: [i32; 4],
|
||||
inner: Box<Plan>,
|
||||
},
|
||||
Stack {
|
||||
children: Vec<Plan>,
|
||||
},
|
||||
Span {
|
||||
dir: usize,
|
||||
gap: i32,
|
||||
/// Grown for this span, in the order they are made.
|
||||
children: Vec<Plan>,
|
||||
/// Grown beside it whether or not they end up in it, so the widget
|
||||
/// after them has the same id in a tree that leaves them out as in
|
||||
/// one that puts them in.
|
||||
spares: Vec<Plan>,
|
||||
/// Which of `children` then `spares` are actually in the span, and
|
||||
/// in what order -- kept apart from the two lists above so that a
|
||||
/// tree which detaches, attaches or reorders its children still
|
||||
/// makes the same widgets in the same order, and two builds line up
|
||||
/// index for index. Anything not named here is built and held
|
||||
/// rather than dropped, since freeing an id hands it to the next
|
||||
/// widget and puts two trees out of step.
|
||||
order: Vec<usize>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Plan {
|
||||
/// A widget carrying nothing anybody has offered it yet.
|
||||
fn bare(kind: Kind) -> Self {
|
||||
Self {
|
||||
kind,
|
||||
size: None,
|
||||
align: None,
|
||||
region_node: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// How many widgets building it makes, spares and detached children
|
||||
/// included, since those are made either way.
|
||||
pub fn size(&self) -> usize {
|
||||
1 + match &self.kind {
|
||||
Kind::Scroll { inner, .. } | Kind::Pad { inner, .. } => inner.size(),
|
||||
Kind::Branch {
|
||||
probe,
|
||||
wide,
|
||||
narrow,
|
||||
..
|
||||
} => probe.size() + wide.size() + narrow.size(),
|
||||
Kind::Stack { children } => children.iter().map(Plan::size).sum(),
|
||||
Kind::Span {
|
||||
children, spares, ..
|
||||
} => children.iter().chain(spares).map(Plan::size).sum(),
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// The trees to try instead of this one when reducing a counterexample,
|
||||
/// biggest cut first: a shrinker takes the first that still fails, so
|
||||
/// offering "this subtree alone" before "this subtree with one child
|
||||
/// fewer" is what gets from six hundred widgets to six rather than to
|
||||
/// five hundred and ninety.
|
||||
///
|
||||
/// Every one of these is a tree the generator could have grown, so a
|
||||
/// reduced plan is a counterexample in its own right rather than a
|
||||
/// special case only the shrinker can make.
|
||||
pub fn smaller(&self) -> Vec<Plan> {
|
||||
let mut out = Vec::new();
|
||||
// Standing in for the whole of it, which is the largest cut there is.
|
||||
for kid in self.kids() {
|
||||
out.push(kid.clone());
|
||||
}
|
||||
// Then what it carries, which costs nothing to put back if it was
|
||||
// not the thing that mattered.
|
||||
for dropped in [
|
||||
self.region_node.map(|_| Plan {
|
||||
region_node: None,
|
||||
..self.clone()
|
||||
}),
|
||||
self.align.map(|_| Plan {
|
||||
align: None,
|
||||
..self.clone()
|
||||
}),
|
||||
self.size.map(|_| Plan {
|
||||
size: None,
|
||||
..self.clone()
|
||||
}),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
out.push(dropped);
|
||||
}
|
||||
out.extend(self.kind.smaller().into_iter().map(|kind| Plan {
|
||||
kind,
|
||||
..self.clone()
|
||||
}));
|
||||
out
|
||||
}
|
||||
|
||||
/// Visits every widget in the order [`build`] makes them, so a count
|
||||
/// kept by the visitor indexes the same widget as the matching [`Tree`]
|
||||
/// vector does.
|
||||
pub fn walk_mut(&mut self, at: &mut impl FnMut(&mut Plan)) {
|
||||
match &mut self.kind {
|
||||
Kind::Scroll { inner, .. } | Kind::Pad { inner, .. } => inner.walk_mut(at),
|
||||
Kind::Branch {
|
||||
probe,
|
||||
wide,
|
||||
narrow,
|
||||
..
|
||||
} => {
|
||||
probe.walk_mut(at);
|
||||
wide.walk_mut(at);
|
||||
narrow.walk_mut(at);
|
||||
}
|
||||
Kind::Stack { children } => {
|
||||
for child in children {
|
||||
child.walk_mut(at);
|
||||
}
|
||||
}
|
||||
Kind::Span {
|
||||
children, spares, ..
|
||||
} => {
|
||||
for child in children.iter_mut().chain(spares) {
|
||||
child.walk_mut(at);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
at(self);
|
||||
}
|
||||
|
||||
/// The same tree with `edits` applied, by the indices the generator would
|
||||
/// have used for them.
|
||||
///
|
||||
/// [`plan`] resolves edits while drawing, which needs a seed. A scenario
|
||||
/// needs them applied to a tree that already exists -- one it has built,
|
||||
/// and one a shrinker may already have cut down, where no seed grows it
|
||||
/// any more. Both routes take the same [`Edits`], so a case written
|
||||
/// against one reads the same against the other.
|
||||
pub fn edited(&self, edits: &Edits) -> Plan {
|
||||
let mut out = self.clone();
|
||||
let (mut sized, mut aligned, mut nodes, mut spans) = (0, 0, 0, 0);
|
||||
out.walk_mut(&mut |plan| {
|
||||
if let Kind::Span {
|
||||
children,
|
||||
spares,
|
||||
order,
|
||||
..
|
||||
} = &mut plan.kind
|
||||
{
|
||||
if let Some(edit) = edits.spans.get(&spans) {
|
||||
*order = span_edited(order, children.len(), spares.len(), edit);
|
||||
}
|
||||
spans += 1;
|
||||
}
|
||||
if let Kind::Branch { threshold, .. } = &mut plan.kind
|
||||
&& edits.fixed_branches
|
||||
{
|
||||
*threshold = f32::MIN;
|
||||
}
|
||||
if plan.size.is_some() {
|
||||
if let Some(lens) = edits.sizes.get(&sized) {
|
||||
plan.size = Some(*lens);
|
||||
}
|
||||
sized += 1;
|
||||
}
|
||||
if plan.align.is_some() {
|
||||
if let Some(align) = edits.aligns.get(&aligned) {
|
||||
plan.align = Some(*align);
|
||||
}
|
||||
aligned += 1;
|
||||
}
|
||||
if plan.region_node.is_some() {
|
||||
if let Some(take) = edits.nodes.get(&nodes) {
|
||||
plan.region_node = Some(*take);
|
||||
}
|
||||
nodes += 1;
|
||||
}
|
||||
});
|
||||
out
|
||||
}
|
||||
|
||||
fn kids(&self) -> Vec<&Plan> {
|
||||
match &self.kind {
|
||||
Kind::Scroll { inner, .. } | Kind::Pad { inner, .. } => vec![inner],
|
||||
Kind::Branch {
|
||||
probe,
|
||||
wide,
|
||||
narrow,
|
||||
..
|
||||
} => vec![probe, wide, narrow],
|
||||
Kind::Stack { children } => children.iter().collect(),
|
||||
Kind::Span { children, .. } => children.iter().collect(),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Kind {
|
||||
/// Simplifications of the shape alone, leaving what the widget carries to
|
||||
/// [`Plan::smaller`]. Replacing a node with one of its children is there
|
||||
/// rather than here, since it answers with a whole `Plan`.
|
||||
fn smaller(&self) -> Vec<Kind> {
|
||||
let mut out = Vec::new();
|
||||
/// One child reduced at a time, rebuilt into the same shape. Every
|
||||
/// answer has the same number of children as it was given, so it is
|
||||
/// for the shapes whose child count is part of what they are.
|
||||
fn reduced(kids: &[Plan], rebuild: &dyn Fn(Vec<Plan>) -> Kind) -> Vec<Kind> {
|
||||
let mut out = Vec::new();
|
||||
for (i, kid) in kids.iter().enumerate() {
|
||||
for small in kid.smaller() {
|
||||
let mut next = kids.to_vec();
|
||||
next[i] = small;
|
||||
out.push(rebuild(next));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// One child dropped, then [`reduced`]. For the shapes that hold any
|
||||
/// number of children, where dropping one is the cut that matters.
|
||||
fn each(kids: &[Plan], rebuild: &dyn Fn(Vec<Plan>) -> Kind) -> Vec<Kind> {
|
||||
let mut out = Vec::new();
|
||||
for i in 0..kids.len() {
|
||||
if kids.len() > 1 {
|
||||
let mut less = kids.to_vec();
|
||||
less.remove(i);
|
||||
out.push(rebuild(less));
|
||||
}
|
||||
}
|
||||
out.extend(reduced(kids, rebuild));
|
||||
out
|
||||
}
|
||||
match self {
|
||||
// The one leaf that reads the width it is given, then the one
|
||||
// that does not, then the one that measures nothing at all.
|
||||
Kind::Wrapped => out.push(Kind::OneLine),
|
||||
Kind::OneLine => out.push(Kind::Rect {
|
||||
color: 0,
|
||||
alpha: 255,
|
||||
}),
|
||||
Kind::Rect { .. } => {}
|
||||
Kind::Scroll { axis, inner } => {
|
||||
let axis = *axis;
|
||||
out.extend(each(std::slice::from_ref(inner), &|mut k| Kind::Scroll {
|
||||
axis,
|
||||
inner: Box::new(k.remove(0)),
|
||||
}));
|
||||
}
|
||||
Kind::Branch {
|
||||
probe,
|
||||
wide,
|
||||
narrow,
|
||||
threshold,
|
||||
} => {
|
||||
let threshold = *threshold;
|
||||
// All three sides stay: a branch is the widget that draws
|
||||
// one of two on a measurement, and one with a side missing
|
||||
// is a different widget rather than a smaller one. Dropping
|
||||
// the branch for a side is offered by `Plan::smaller`.
|
||||
let sides = [(**probe).clone(), (**wide).clone(), (**narrow).clone()];
|
||||
out.extend(reduced(&sides, &|k| Kind::Branch {
|
||||
probe: Box::new(k[0].clone()),
|
||||
wide: Box::new(k[1].clone()),
|
||||
narrow: Box::new(k[2].clone()),
|
||||
threshold,
|
||||
}));
|
||||
}
|
||||
Kind::Pad { padding, inner } => {
|
||||
let padding = *padding;
|
||||
if padding != [0; 4] {
|
||||
out.push(Kind::Pad {
|
||||
padding: [0; 4],
|
||||
inner: inner.clone(),
|
||||
});
|
||||
}
|
||||
out.extend(each(std::slice::from_ref(inner), &|mut k| Kind::Pad {
|
||||
padding,
|
||||
inner: Box::new(k.remove(0)),
|
||||
}));
|
||||
}
|
||||
Kind::Stack { children } => {
|
||||
out.extend(each(children, &|children| Kind::Stack { children }))
|
||||
}
|
||||
Kind::Span {
|
||||
dir,
|
||||
gap,
|
||||
children,
|
||||
spares,
|
||||
order,
|
||||
} => {
|
||||
let (dir, gap, n) = (*dir, *gap, children.len());
|
||||
let span = |children: Vec<Plan>, spares: Vec<Plan>, order: Vec<usize>| Kind::Span {
|
||||
dir,
|
||||
gap,
|
||||
children,
|
||||
spares,
|
||||
order,
|
||||
};
|
||||
let identity: Vec<usize> = (0..n).collect();
|
||||
// An order the generator did not choose is part of the tree,
|
||||
// so take that off before taking the tree apart.
|
||||
if *order != identity {
|
||||
out.push(span(children.clone(), spares.clone(), identity));
|
||||
}
|
||||
// Spares exist to be attached; with none attached they are
|
||||
// widgets the span never holds.
|
||||
if !spares.is_empty() && order.iter().all(|&i| i < n) {
|
||||
out.push(span(children.clone(), Vec::new(), order.clone()));
|
||||
}
|
||||
if gap != 0 {
|
||||
out.push(Kind::Span {
|
||||
dir,
|
||||
gap: 0,
|
||||
children: children.clone(),
|
||||
spares: spares.clone(),
|
||||
order: order.clone(),
|
||||
});
|
||||
}
|
||||
for k in 0..n {
|
||||
if n > 1 {
|
||||
let mut less = children.clone();
|
||||
less.remove(k);
|
||||
// Everything after it shifts down, spares included,
|
||||
// since they are indexed past the children.
|
||||
let order = order
|
||||
.iter()
|
||||
.filter(|&&i| i != k)
|
||||
.map(|&i| if i > k { i - 1 } else { i })
|
||||
.collect();
|
||||
out.push(span(less, spares.clone(), order));
|
||||
}
|
||||
}
|
||||
for (i, kid) in children.iter().enumerate() {
|
||||
for small in kid.smaller() {
|
||||
let mut next = children.clone();
|
||||
next[i] = small;
|
||||
out.push(span(next, spares.clone(), order.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`SpanEdit`] applied to the order a span already holds its children in.
|
||||
///
|
||||
/// `detach` names positions in that order and `attach` takes from the front
|
||||
/// of what the span is not holding, both of which is what a test changing a
|
||||
/// live span does -- so an edit means the same thing said to a tree and said
|
||||
/// to the plan it was built from. On a span nobody has edited the order is
|
||||
/// the children in the order they were grown, and this is then "leave these
|
||||
/// out and put that many spares on the end".
|
||||
fn span_edited(order: &[usize], children: usize, spares: usize, edit: &SpanEdit) -> Vec<usize> {
|
||||
let mut detach = edit.detach.clone();
|
||||
detach.sort_unstable();
|
||||
detach.dedup();
|
||||
let mut next: Vec<usize> = order
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(at, _)| !detach.contains(at))
|
||||
.map(|(_, &which)| which)
|
||||
.collect();
|
||||
// What the span is not holding, in the order it hands them back: what it
|
||||
// was already not holding first, in the order the widgets were made, and
|
||||
// what this edit takes out after that, highest position first. A child
|
||||
// just detached goes to the back rather than straight back in, which is
|
||||
// what makes detaching one and attaching one a trade.
|
||||
let mut free: Vec<usize> = (0..children + spares)
|
||||
.filter(|i| !order.contains(i))
|
||||
.collect();
|
||||
free.extend(detach.iter().rev().filter_map(|&at| order.get(at).copied()));
|
||||
next.extend(free.into_iter().take(edit.attach));
|
||||
next
|
||||
}
|
||||
|
||||
/// Plans the tree `seed` describes, `edits` replacing what it would otherwise
|
||||
/// have given the widgets that carry them.
|
||||
///
|
||||
/// The edits are resolved here rather than at build time, so that a plan is
|
||||
/// the whole of what a tree is and building one has nothing left to decide.
|
||||
pub fn plan(seed: u64, depth: usize, edits: &Edits) -> Plan {
|
||||
let mut sow = Sow {
|
||||
rng: Rng::new(seed),
|
||||
edits,
|
||||
sized: 0,
|
||||
aligned: 0,
|
||||
nodes: 0,
|
||||
spans: 0,
|
||||
};
|
||||
sow.node(depth)
|
||||
}
|
||||
|
||||
/// Grows the tree `seed` describes, `edits` replacing the declared sizes it
|
||||
/// would otherwise have given those wrappers.
|
||||
pub fn grow<Rsc: UiRsc + 'static>(
|
||||
@@ -144,41 +599,32 @@ pub fn grow<Rsc: UiRsc + 'static>(
|
||||
depth: usize,
|
||||
edits: &Edits,
|
||||
) -> (StrongWidget, Tree) {
|
||||
let mut grow = Grow {
|
||||
rsc,
|
||||
rng: Rng::new(seed),
|
||||
tree: Tree::default(),
|
||||
edits,
|
||||
};
|
||||
let root = grow.node(depth);
|
||||
(root, grow.tree)
|
||||
build(rsc, &plan(seed, depth, edits))
|
||||
}
|
||||
|
||||
struct Grow<'a, Rsc> {
|
||||
rsc: &'a mut Rsc,
|
||||
/// Draws a plan out of the random stream. Every draw happens in the order it
|
||||
/// always has and before the decision it feeds, including the decisions that
|
||||
/// are then dropped, because a seed has to keep meaning the same tree.
|
||||
struct Sow<'a> {
|
||||
rng: Rng,
|
||||
tree: Tree,
|
||||
edits: &'a Edits,
|
||||
sized: usize,
|
||||
aligned: usize,
|
||||
nodes: usize,
|
||||
spans: usize,
|
||||
}
|
||||
|
||||
impl<Rsc: UiRsc + 'static> Grow<'_, Rsc> {
|
||||
fn leaf(&mut self) -> StrongWidget {
|
||||
let id: StrongWidget = match self.rng.below(4) {
|
||||
// Wrapped and unwrapped, because only one of them reads the width
|
||||
// it is given and so only one has to be drawn again for a new one.
|
||||
0 => wtext(WORDS).size(16).wrap(true).add_strong(self.rsc),
|
||||
1 => wtext("one line, overflowing whatever it is given")
|
||||
.size(16)
|
||||
.wrap(false)
|
||||
.add_strong(self.rsc),
|
||||
impl Sow<'_> {
|
||||
fn leaf(&mut self) -> Plan {
|
||||
Plan::bare(match self.rng.below(4) {
|
||||
0 => Kind::Wrapped,
|
||||
1 => Kind::OneLine,
|
||||
_ => {
|
||||
let color = COLORS[self.rng.below(COLORS.len())];
|
||||
let color = self.rng.below(COLORS.len());
|
||||
let alpha = (self.rng.below(5) * 63) as u8;
|
||||
rect(color.alpha(alpha)).add_strong(self.rsc)
|
||||
Kind::Rect { color, alpha }
|
||||
}
|
||||
};
|
||||
self.tree.ids.push(id.id());
|
||||
id
|
||||
})
|
||||
}
|
||||
|
||||
fn len(&mut self) -> Option<LayoutLen> {
|
||||
@@ -189,181 +635,270 @@ impl<Rsc: UiRsc + 'static> Grow<'_, Rsc> {
|
||||
}
|
||||
}
|
||||
|
||||
fn align(&mut self) -> Align {
|
||||
let mut axis = || match self.rng.below(4) {
|
||||
fn align(&mut self) -> Aligns {
|
||||
let axis = |s: &mut Self| match s.rng.below(4) {
|
||||
0 => None,
|
||||
1 => Some(AxisAlign::NEG),
|
||||
2 => Some(AxisAlign::CENTER),
|
||||
_ => Some(AxisAlign::POS),
|
||||
};
|
||||
let (mut x, y) = (axis(), axis());
|
||||
let (x, y) = (axis(self), axis(self));
|
||||
// Aligning on neither axis leaves the branch unexercised.
|
||||
if x.is_none() && y.is_none() {
|
||||
x = Some(AxisAlign::CENTER);
|
||||
match x.is_none() && y.is_none() {
|
||||
true => [Some(AxisAlign::CENTER), y],
|
||||
false => [x, y],
|
||||
}
|
||||
Align { x, y }
|
||||
}
|
||||
|
||||
/// A declared size over half the tree, kept where a test can change it.
|
||||
fn sized(&mut self, inner: StrongWidget) -> StrongWidget {
|
||||
// A rule is a property now, so a node already carrying one would take
|
||||
// a second entry in `sized` -- and two edits naming one widget settle
|
||||
// in the order they are applied, which is grow order cold and edit
|
||||
// order warm. One entry per widget instead. Both draws are taken
|
||||
// whatever is decided, and the decision is grow order alone, so the
|
||||
// two trees consume the same random stream.
|
||||
fn sized(&mut self, inner: &mut Plan) {
|
||||
let take = self.rng.chance();
|
||||
let lens = [self.len(), self.len()];
|
||||
if !take || self.tree.sized.contains(&inner.id()) {
|
||||
return inner;
|
||||
if !take || inner.size.is_some() {
|
||||
return;
|
||||
}
|
||||
let idx = self.tree.sized.len();
|
||||
let lens = self.edits.sizes.get(&idx).copied().unwrap_or(lens);
|
||||
let id = inner.id();
|
||||
self.rsc
|
||||
.ui_mut()
|
||||
.widgets
|
||||
.set_size_rules(id, lens[0], lens[1]);
|
||||
self.tree.sized.push(id);
|
||||
inner
|
||||
let idx = self.sized;
|
||||
self.sized += 1;
|
||||
inner.size = Some(self.edits.sizes.get(&idx).copied().unwrap_or(lens));
|
||||
}
|
||||
|
||||
/// An alignment over some of the tree, kept where a test can change it.
|
||||
/// One entry per widget for the reason `sized` gives.
|
||||
fn aligned(&mut self, inner: StrongWidget) -> StrongWidget {
|
||||
fn aligned(&mut self, inner: &mut Plan) {
|
||||
let align = self.align();
|
||||
let align = [align.x, align.y];
|
||||
if self.tree.aligned.contains(&inner.id()) {
|
||||
return inner;
|
||||
if inner.align.is_some() {
|
||||
return;
|
||||
}
|
||||
let idx = self.tree.aligned.len();
|
||||
let align = self.edits.aligns.get(&idx).copied().unwrap_or(align);
|
||||
let id = inner.id();
|
||||
let widgets = &mut self.rsc.ui_mut().widgets;
|
||||
for (axis, align) in [Axis::X, Axis::Y].into_iter().zip(align) {
|
||||
widgets.set_alignment(id, axis, align.unwrap_or_default());
|
||||
}
|
||||
self.tree.aligned.push(id);
|
||||
inner
|
||||
let idx = self.aligned;
|
||||
self.aligned += 1;
|
||||
inner.align = Some(self.edits.aligns.get(&idx).copied().unwrap_or(align));
|
||||
}
|
||||
|
||||
/// A movable region of its own over some of the tree. What it changes is
|
||||
/// how a move is written and how long a primitive's chain is, neither of
|
||||
/// which any other branch here varies.
|
||||
fn noded(&mut self, inner: StrongWidget) -> StrongWidget {
|
||||
fn noded(&mut self, inner: &mut Plan) {
|
||||
let take = self.rng.below(4) == 0;
|
||||
if self.tree.nodes.contains(&inner.id()) {
|
||||
return inner;
|
||||
if inner.region_node.is_some() {
|
||||
return;
|
||||
}
|
||||
let idx = self.tree.nodes.len();
|
||||
let take = self.edits.nodes.get(&idx).copied().unwrap_or(take);
|
||||
let id = inner.id();
|
||||
self.rsc.ui_mut().widgets.set_region_node(id, take);
|
||||
self.tree.nodes.push(id);
|
||||
inner
|
||||
let idx = self.nodes;
|
||||
self.nodes += 1;
|
||||
inner.region_node = Some(self.edits.nodes.get(&idx).copied().unwrap_or(take));
|
||||
}
|
||||
|
||||
fn node(&mut self, depth: usize) -> StrongWidget {
|
||||
fn offered(&mut self, inner: &mut Plan) {
|
||||
self.sized(inner);
|
||||
self.noded(inner);
|
||||
}
|
||||
|
||||
fn node(&mut self, depth: usize) -> Plan {
|
||||
if depth == 0 {
|
||||
return self.leaf();
|
||||
}
|
||||
let positioned = self.rng.below(6);
|
||||
if positioned == 0 {
|
||||
// Scrolling reads the pixel length of its box, which nothing
|
||||
// else here does, and gives its child a box longer than its own.
|
||||
let inner = self.node(depth - 1);
|
||||
let inner = self.sized(inner);
|
||||
let inner = self.noded(inner);
|
||||
let mut inner = self.node(depth - 1);
|
||||
self.offered(&mut inner);
|
||||
let axis = if self.rng.chance() { Axis::X } else { Axis::Y };
|
||||
let id = Scroll::new(inner, axis).add(self.rsc);
|
||||
self.tree.scrolls.push(id);
|
||||
self.tree.ids.push(id.id());
|
||||
return id.add_strong(self.rsc);
|
||||
return Plan::bare(Kind::Scroll {
|
||||
axis,
|
||||
inner: Box::new(inner),
|
||||
});
|
||||
}
|
||||
if positioned == 2 {
|
||||
// Both sides are grown either way, so a tree that draws one has
|
||||
// the same ids as a tree that draws the other.
|
||||
let probe = self.node(depth - 1);
|
||||
let wide = self.node(depth - 1);
|
||||
let narrow = self.node(depth - 1);
|
||||
let threshold = self.rng.below(500) as f32;
|
||||
let id = Branch {
|
||||
probe,
|
||||
wide,
|
||||
narrow,
|
||||
// Drawn either way, so the side a fixed branch takes is still a
|
||||
// side the generator chose -- and it consumes the same randomness
|
||||
// as a measured one, so the two grow the same ids.
|
||||
let measured = self.rng.below(500) as f32;
|
||||
let threshold = match self.edits.fixed_branches {
|
||||
true => f32::MIN,
|
||||
false => measured,
|
||||
};
|
||||
return Plan::bare(Kind::Branch {
|
||||
probe: Box::new(probe),
|
||||
wide: Box::new(wide),
|
||||
narrow: Box::new(narrow),
|
||||
threshold,
|
||||
}
|
||||
.add(self.rsc);
|
||||
self.tree.ids.push(id.id());
|
||||
return id.add_strong(self.rsc);
|
||||
});
|
||||
}
|
||||
if positioned == 1 {
|
||||
let inner = self.node(depth - 1);
|
||||
let inner = self.sized(inner);
|
||||
let inner = self.noded(inner);
|
||||
return self.aligned(inner);
|
||||
// Carries an alignment and makes no widget of its own, so the
|
||||
// plan for it is the child it aligned.
|
||||
let mut inner = self.node(depth - 1);
|
||||
self.offered(&mut inner);
|
||||
self.aligned(&mut inner);
|
||||
return inner;
|
||||
}
|
||||
if self.rng.below(4) == 0 {
|
||||
let inner = self.node(depth - 1);
|
||||
let inner = self.sized(inner);
|
||||
let inner = self.noded(inner);
|
||||
// Each side its own, since a padding that is the same all round
|
||||
// hides anything that treats one edge differently from another.
|
||||
let mut side = || Px::from_int(self.rng.below(24) as i32);
|
||||
let padding = Padding {
|
||||
left: side(),
|
||||
right: side(),
|
||||
top: side(),
|
||||
bottom: side(),
|
||||
};
|
||||
let id = Pad { padding, inner }.add_strong(self.rsc);
|
||||
self.tree.ids.push(id.id());
|
||||
return id;
|
||||
let mut inner = self.node(depth - 1);
|
||||
self.offered(&mut inner);
|
||||
let side = |s: &mut Self| s.rng.below(24) as i32;
|
||||
let padding = [side(self), side(self), side(self), side(self)];
|
||||
return Plan::bare(Kind::Pad {
|
||||
padding,
|
||||
inner: Box::new(inner),
|
||||
});
|
||||
}
|
||||
let grown = 2 + self.rng.below(3);
|
||||
let mut children = Vec::with_capacity(grown);
|
||||
for _ in 0..grown {
|
||||
let child = self.node(depth - 1);
|
||||
let child = self.sized(child);
|
||||
let child = self.noded(child);
|
||||
let mut child = self.node(depth - 1);
|
||||
self.offered(&mut child);
|
||||
children.push(child);
|
||||
}
|
||||
if self.rng.chance() {
|
||||
let id = Stack {
|
||||
children,
|
||||
size: StackSize::Child(0),
|
||||
return Plan::bare(Kind::Stack { children });
|
||||
}
|
||||
.add_strong(self.rsc);
|
||||
self.tree.ids.push(id.id());
|
||||
return id;
|
||||
}
|
||||
// Grown either way, so the widget after them has the same id in a
|
||||
// tree that leaves them out as in one that puts them in.
|
||||
let mut spares: Vec<StrongWidget> = (0..SPARES).map(|_| self.leaf()).collect();
|
||||
let idx = self.tree.spans.len();
|
||||
let spares: Vec<Plan> = (0..SPARES).map(|_| self.leaf()).collect();
|
||||
let idx = self.spans;
|
||||
self.spans += 1;
|
||||
let edit = self.edits.spans.get(&idx).cloned().unwrap_or_default();
|
||||
// Highest first, so an index means the same child however many of its
|
||||
// neighbours are going too.
|
||||
let mut detach = edit.detach.clone();
|
||||
detach.sort_unstable();
|
||||
for j in detach.into_iter().rev() {
|
||||
if j < children.len() {
|
||||
self.tree.detached.push(children.remove(j));
|
||||
}
|
||||
}
|
||||
let attach = edit.attach.min(spares.len());
|
||||
children.extend(spares.drain(..attach));
|
||||
let dir = [Dir::RIGHT, Dir::DOWN, Dir::LEFT, Dir::UP][self.rng.below(4)];
|
||||
let id = Span {
|
||||
children,
|
||||
dir,
|
||||
gap: Px::from_int(self.rng.below(3) as i32 * 4),
|
||||
}
|
||||
.add(self.rsc);
|
||||
let dir = self.rng.below(4);
|
||||
// A row takes the height it is given rather than its tallest child,
|
||||
// which is a rule beside it. Derived from an existing choice and
|
||||
// consuming no randomness: a seed must keep growing the same tree
|
||||
// when the generator gains another configuration.
|
||||
let gap = self.rng.below(3) as i32 * 4;
|
||||
let grown: Vec<usize> = (0..children.len()).collect();
|
||||
let order = span_edited(&grown, children.len(), spares.len(), &edit);
|
||||
Plan::bare(Kind::Span {
|
||||
dir,
|
||||
gap,
|
||||
children,
|
||||
spares,
|
||||
order,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a plan's widgets in the order it describes them, so two builds of
|
||||
/// one plan line up index for index and their boxes can be compared.
|
||||
pub fn build<Rsc: UiRsc + 'static>(rsc: &mut Rsc, plan: &Plan) -> (StrongWidget, Tree) {
|
||||
let mut build = Build {
|
||||
rsc,
|
||||
tree: Tree::default(),
|
||||
};
|
||||
let root = build.node(plan);
|
||||
(root, build.tree)
|
||||
}
|
||||
|
||||
struct Build<'a, Rsc> {
|
||||
rsc: &'a mut Rsc,
|
||||
tree: Tree,
|
||||
}
|
||||
|
||||
impl<Rsc: UiRsc + 'static> Build<'_, Rsc> {
|
||||
fn node(&mut self, plan: &Plan) -> StrongWidget {
|
||||
let built = self.kind(&plan.kind);
|
||||
let id = built.id();
|
||||
if let Some(lens) = plan.size {
|
||||
self.rsc
|
||||
.ui_mut()
|
||||
.widgets
|
||||
.set_size_rules(id, lens[0], lens[1]);
|
||||
self.tree.sized.push(id);
|
||||
}
|
||||
if let Some(align) = plan.align {
|
||||
let widgets = &mut self.rsc.ui_mut().widgets;
|
||||
for (axis, align) in [Axis::X, Axis::Y].into_iter().zip(align) {
|
||||
widgets.set_alignment(id, axis, align.unwrap_or_default());
|
||||
}
|
||||
self.tree.aligned.push(id);
|
||||
}
|
||||
if let Some(take) = plan.region_node {
|
||||
self.rsc.ui_mut().widgets.set_region_node(id, take);
|
||||
self.tree.nodes.push(id);
|
||||
}
|
||||
built
|
||||
}
|
||||
|
||||
fn kind(&mut self, kind: &Kind) -> StrongWidget {
|
||||
let id: StrongWidget = match kind {
|
||||
Kind::Wrapped => wtext(WORDS).size(16).wrap(true).add_strong(self.rsc),
|
||||
Kind::OneLine => wtext("one line, overflowing whatever it is given")
|
||||
.size(16)
|
||||
.wrap(false)
|
||||
.add_strong(self.rsc),
|
||||
Kind::Rect { color, alpha } => rect(COLORS[*color].alpha(*alpha)).add_strong(self.rsc),
|
||||
Kind::Scroll { axis, inner } => {
|
||||
let inner = self.node(inner);
|
||||
let id = Scroll::new(inner, *axis).add(self.rsc);
|
||||
self.tree.scrolls.push(id);
|
||||
self.tree.ids.push(id.id());
|
||||
return id.add_strong(self.rsc);
|
||||
}
|
||||
Kind::Branch {
|
||||
probe,
|
||||
wide,
|
||||
narrow,
|
||||
threshold,
|
||||
} => {
|
||||
let probe = self.node(probe);
|
||||
let wide = self.node(wide);
|
||||
let narrow = self.node(narrow);
|
||||
let id = Branch {
|
||||
probe,
|
||||
wide,
|
||||
narrow,
|
||||
threshold: *threshold,
|
||||
}
|
||||
.add(self.rsc);
|
||||
self.tree.ids.push(id.id());
|
||||
return id.add_strong(self.rsc);
|
||||
}
|
||||
Kind::Pad { padding, inner } => {
|
||||
let inner = self.node(inner);
|
||||
let [left, right, top, bottom] = padding.map(Px::from_int);
|
||||
let padding = Padding {
|
||||
left,
|
||||
right,
|
||||
top,
|
||||
bottom,
|
||||
};
|
||||
Pad { padding, inner }.add_strong(self.rsc)
|
||||
}
|
||||
Kind::Stack { children } => {
|
||||
let children = children.iter().map(|c| self.node(c)).collect();
|
||||
Stack {
|
||||
children,
|
||||
size: StackSize::Child(0),
|
||||
}
|
||||
.add_strong(self.rsc)
|
||||
}
|
||||
Kind::Span {
|
||||
dir,
|
||||
gap,
|
||||
children,
|
||||
spares,
|
||||
order,
|
||||
} => {
|
||||
let grown = children.len();
|
||||
// Every one of them is made, in this order, whether or not
|
||||
// the span ends up holding it.
|
||||
let made: Vec<StrongWidget> = children
|
||||
.iter()
|
||||
.chain(spares)
|
||||
.map(|c| self.node(c))
|
||||
.collect();
|
||||
let mut left: Vec<Option<StrongWidget>> = made.into_iter().map(Some).collect();
|
||||
let children: Vec<StrongWidget> = order
|
||||
.iter()
|
||||
.filter_map(|&i| left.get_mut(i).and_then(Option::take))
|
||||
.collect();
|
||||
// What the span does not hold is still held here: dropping
|
||||
// the last share of a widget frees its id for the next one
|
||||
// to be given, which puts two trees out of step.
|
||||
let spares: Vec<StrongWidget> = left.into_iter().flatten().collect();
|
||||
let dir = [Dir::RIGHT, Dir::DOWN, Dir::LEFT, Dir::UP][*dir % 4];
|
||||
let id = Span {
|
||||
children,
|
||||
dir,
|
||||
gap: Px::from_int(*gap),
|
||||
}
|
||||
.add(self.rsc);
|
||||
if dir.axis == Axis::X {
|
||||
self.rsc
|
||||
.widgets_mut()
|
||||
@@ -371,6 +906,10 @@ impl<Rsc: UiRsc + 'static> Grow<'_, Rsc> {
|
||||
}
|
||||
self.tree.ids.push(id.id());
|
||||
self.tree.spans.push(Spanned { id, spares, grown });
|
||||
id.add_strong(self.rsc)
|
||||
return id.add_strong(self.rsc);
|
||||
}
|
||||
};
|
||||
self.tree.ids.push(id.id());
|
||||
id
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,15 +1,15 @@
|
||||
mod image;
|
||||
mod mask;
|
||||
mod position;
|
||||
mod ptr;
|
||||
mod rect;
|
||||
mod text;
|
||||
mod trait_fns;
|
||||
mod wrapper;
|
||||
|
||||
pub use image::*;
|
||||
pub use mask::*;
|
||||
pub use position::*;
|
||||
pub use ptr::*;
|
||||
pub use rect::*;
|
||||
pub use text::*;
|
||||
pub use trait_fns::*;
|
||||
pub use wrapper::*;
|
||||
@@ -7,8 +7,14 @@ pub struct Pad {
|
||||
|
||||
impl Widget for Pad {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
// The inner's own alignment, not the near edge. This reports the
|
||||
// inner's size plus the padding, so where the box is that answer the
|
||||
// inner is exactly what it asked for and alignment has no room to
|
||||
// move it; where the box is bigger -- a share of a row, a rule over
|
||||
// this widget -- the slack is the inner's to sit in, and forcing the
|
||||
// near edge pinned it to a corner it had not asked for.
|
||||
let inner = painter
|
||||
.widget_aligned(&self.inner, self.padding.region(), RegionAlign::NEAR)
|
||||
.widget_within(&self.inner, self.padding.region())
|
||||
.size();
|
||||
Size {
|
||||
x: LayoutLen {
|
||||
@@ -23,6 +29,45 @@ impl Widget for Pad {
|
||||
}
|
||||
}
|
||||
|
||||
/// Room taken off the inside rather than added round the outside: the child
|
||||
/// draws in what is left once both edges are gone, and this widget is
|
||||
/// exactly as long as the box it was given.
|
||||
///
|
||||
/// So `rel(1.0)` under an [`Inset`] is the room inside it, where the same
|
||||
/// rule under a [`Pad`] is the pad's whole box and overflows it by the
|
||||
/// padding. Both are wanted; which one a layout means is which widget it
|
||||
/// reaches for.
|
||||
pub struct Inset {
|
||||
pub padding: Padding,
|
||||
pub inner: StrongWidget,
|
||||
}
|
||||
|
||||
impl Widget for Inset {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
let region = self.padding.inset_region();
|
||||
let inner = painter.widget_within(&self.inner, region).size();
|
||||
// What a fraction the child reported is a fraction of is this
|
||||
// widget's to say, and it says the room inside: the child asked for
|
||||
// a part of the box it drew in, and that box is shorter than this
|
||||
// one by both edges. Then the edges go back on, so this widget is
|
||||
// its child and the room taken off around it.
|
||||
let (x, y) = (
|
||||
inner.x.within_len(region.x.len()),
|
||||
inner.y.within_len(region.y.len()),
|
||||
);
|
||||
Size {
|
||||
x: LayoutLen {
|
||||
px: x.px + self.padding.left + self.padding.right,
|
||||
..x
|
||||
},
|
||||
y: LayoutLen {
|
||||
px: y.px + self.padding.top + self.padding.bottom,
|
||||
..y
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Padding {
|
||||
pub left: Px,
|
||||
pub right: Px,
|
||||
@@ -47,7 +92,24 @@ impl Padding {
|
||||
bottom: amt,
|
||||
}
|
||||
}
|
||||
/// The box a [`Pad`] gives its child: as long as the pad's own, moved in
|
||||
/// by the near edge. Padding is outside what it pads, so a fraction the
|
||||
/// child asks for is a fraction of the same length whether a rule beside
|
||||
/// it states one or it reports one, and its pixels are the same pixels.
|
||||
/// Shrinking the box instead would make `rel` mean the inner box while
|
||||
/// `px` meant the outer one. [`Inset`] is the widget that shrinks.
|
||||
pub fn region(&self) -> UiRegion {
|
||||
let mut region = UiRegion::FULL;
|
||||
region.x.start.px += self.left;
|
||||
region.y.start.px += self.top;
|
||||
region.x.end.px += self.left;
|
||||
region.y.end.px += self.top;
|
||||
region
|
||||
}
|
||||
|
||||
/// The box an [`Inset`] gives its child: shorter than its own by both
|
||||
/// edges, so what the child fills is the room left inside.
|
||||
pub fn inset_region(&self) -> UiRegion {
|
||||
let mut region = UiRegion::FULL;
|
||||
region.x.start.px += self.left;
|
||||
region.y.start.px += self.top;
|
||||
|
||||
@@ -63,7 +63,7 @@ impl Widget for Scroll {
|
||||
region = region.offset(offset);
|
||||
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
|
||||
}
|
||||
painter.widget_aligned(&self.inner, region, RegionAlign::NEAR);
|
||||
painter.widget_at(&self.inner, region, [true; 2]);
|
||||
// What it occupies is its box, on both axes: it clips its content to
|
||||
// that box, so it can neither take less of one nor honestly ask for
|
||||
// more. The content's length is what it scrolls through, not what it
|
||||
|
||||
@@ -20,9 +20,13 @@ impl Widget for Span {
|
||||
span.flip();
|
||||
}
|
||||
let region = UiRegion::from_axis(axis, span, UiSpan::FULL);
|
||||
// Offered the room left from the cursor, because a text has to
|
||||
// wrap at the width actually there, while what it reports is a
|
||||
// fraction of the whole row: `rel(0.5)` is half the span
|
||||
// whatever else is in it and wherever this child sits.
|
||||
let len = match painter.known_len(child, axis, region) {
|
||||
Some(len) => len,
|
||||
None => painter.widget_within(child, region).len(axis),
|
||||
None => painter.widget_at(child, region, [false; 2]).len(axis),
|
||||
};
|
||||
cursor.px += len.px + self.gap;
|
||||
cursor.rel += len.rel;
|
||||
@@ -119,7 +123,10 @@ impl Widget for Span {
|
||||
if self.dir.sign == Sign::Neg {
|
||||
region.flip(axis);
|
||||
}
|
||||
let placed = painter.widget_within(child, region);
|
||||
// Along the row this box is the child's own answer, so the answer
|
||||
// is not placed in it again; across it the child sits where its
|
||||
// alignment says.
|
||||
let placed = painter.widget_at(child, region, [axis == Axis::X, axis == Axis::Y]);
|
||||
if shrinks {
|
||||
let used = placed.len(!axis);
|
||||
// Choosing between a fixed and a relative length from the
|
||||
|
||||
@@ -29,7 +29,15 @@ impl Widget for Stack {
|
||||
let region = painter.box_of(size);
|
||||
for (i, child) in self.children.iter().enumerate() {
|
||||
painter.child_layer_at(i);
|
||||
painter.widget_aligned(child, region, RegionAlign::NEAR);
|
||||
// The sizing child placed its own content in the box its answer
|
||||
// decided, and this box was derived from that answer, so applying
|
||||
// its alignment again here would place it twice. Every other
|
||||
// child is handed a box that owes nothing to its own answer, and
|
||||
// where it sits in one bigger than itself is its own business.
|
||||
match sizing == Some(i) {
|
||||
true => painter.widget_at(child, region, [true; 2]),
|
||||
false => painter.widget_within(child, region),
|
||||
};
|
||||
}
|
||||
size
|
||||
}
|
||||
|
||||
+12
-2
@@ -55,8 +55,13 @@ impl TextView {
|
||||
// line up to the one it was made at: each line still fits, and none
|
||||
// could take a word that did not fit in the wider box. A line too
|
||||
// long to fit at all says nothing about narrower boxes.
|
||||
//
|
||||
// The step at or above that longest line rather than the nearest
|
||||
// one, since the shaper measures in floats: the nearest step is
|
||||
// under the line half the time, and a range starting there admits a
|
||||
// box the line does not fit in, where the break is not this one.
|
||||
if let Some(width) = width {
|
||||
painter.holds(Axis::X, Px::from_f32(text.size.x).min(width)..=width);
|
||||
painter.holds(Axis::X, Px::ceil_from_f32(text.size.x).min(width)..=width);
|
||||
}
|
||||
text
|
||||
}
|
||||
@@ -78,7 +83,12 @@ impl TextView {
|
||||
|
||||
let tex = self.render(painter);
|
||||
let region = tex.size.align(align);
|
||||
let size = Size::px(tex.size);
|
||||
// The step at or above what the shaper measured, so a parent that
|
||||
// hands back the length this reports hands back a box the longest
|
||||
// line fits in. Rounded to the nearest step it is half the time a
|
||||
// hair under that line, and the break made in it is not the break a
|
||||
// cold layout makes there.
|
||||
let size = Size::from_px(PxVec2::ceil_from_f32(tex.size));
|
||||
let within = region.within(&painter.region());
|
||||
painter.glyphs(tex, within);
|
||||
(region, size)
|
||||
|
||||
+17
-3
@@ -12,6 +12,16 @@ widget_trait! {
|
||||
}
|
||||
}
|
||||
|
||||
fn inset(self, padding: impl Into<Padding>) -> impl WidgetFn<Rsc, Inset> {
|
||||
// Room taken off the inside, where `pad` adds it round the outside:
|
||||
// this is as long as the box it is given and the child fills what is
|
||||
// left of it.
|
||||
|state| Inset {
|
||||
padding: padding.into(),
|
||||
inner: self.add_strong(state),
|
||||
}
|
||||
}
|
||||
|
||||
fn align(self, align: impl Into<Align>) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||
// An axis left out keeps whatever it had, which is centered unless
|
||||
// something else set it.
|
||||
@@ -134,9 +144,13 @@ widget_trait! {
|
||||
|state| self.add(state)
|
||||
}
|
||||
|
||||
fn set_ptr(self, ptr: WeakWidget<WidgetPtr>, state: &mut Rsc) {
|
||||
let id = self.add_strong(state);
|
||||
state.ui_mut().widgets[ptr].inner = Some(id);
|
||||
// Named for the type it makes rather than as `wrapped`, which would read
|
||||
// as the text setting. `widget_trait!` takes no attributes, so what it is
|
||||
// for is on `Wrapper` itself.
|
||||
fn wrapper(self) -> impl WidgetFn<Rsc, Wrapper> {
|
||||
|state| Wrapper {
|
||||
inner: Some(self.add_strong(state)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
use crate::prelude::*;
|
||||
use std::marker::Unsize;
|
||||
|
||||
pub struct WidgetPtr {
|
||||
/// One widget in a box of its own, doing as little as possible on the way:
|
||||
/// it draws its child in the whole of its box and reports back what the child
|
||||
/// said. It exists because a length and an alignment are properties of one
|
||||
/// widget, so a widget cannot both be 100 wide and take two shares of a row
|
||||
/// -- the two lengths need two widgets, and this is the smaller one.
|
||||
///
|
||||
/// Its child is optional so it can also be the swappable slot a tab bar
|
||||
/// needs, which is what it was written for.
|
||||
pub struct Wrapper {
|
||||
pub inner: Option<StrongWidget>,
|
||||
}
|
||||
|
||||
impl Widget for WidgetPtr {
|
||||
impl Widget for Wrapper {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
match &self.inner {
|
||||
Some(id) => painter.widget(id).size(),
|
||||
@@ -14,7 +22,7 @@ impl Widget for WidgetPtr {
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetPtr {
|
||||
impl Wrapper {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
@@ -35,7 +43,7 @@ impl WidgetPtr {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for WidgetPtr {
|
||||
impl Default for Wrapper {
|
||||
fn default() -> Self {
|
||||
Self::empty()
|
||||
}
|
||||
+248
-13
@@ -20,6 +20,102 @@ fn a_span_gives_each_child_the_width_it_asked_for() {
|
||||
assert_corners!(h, right, (100, 0), (400, 200));
|
||||
}
|
||||
|
||||
/// A span offers each child the room left after the one before, because a
|
||||
/// text has to wrap at the width actually there, but reads what the child
|
||||
/// reports as a fraction of the whole row. So two children asking for half
|
||||
/// each take the whole row between them, however much of it was left when
|
||||
/// each was asked, and a third overflows.
|
||||
#[test]
|
||||
fn a_span_reads_a_child_report_as_a_fraction_of_the_row() {
|
||||
let mut h = Harness::new((400, 100));
|
||||
let half = rect(Color::RED).width(rel(0.5)).add(&mut h.rsc);
|
||||
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc);
|
||||
let nested = (inner,).span(Dir::RIGHT).add(&mut h.rsc);
|
||||
let tail = rect(Color::BLUE).width(100).add(&mut h.rsc);
|
||||
h.set_root((half, nested, tail).span(Dir::RIGHT).width(rel(1.0)));
|
||||
|
||||
// The nested span is placed at the length it reported and drawn there
|
||||
// once more; half of that final box is what its own child takes.
|
||||
assert_corners!(h, nested, (200, 0), (400, 100));
|
||||
assert_corners!(h, inner, (200, 0), (300, 100));
|
||||
assert_corners!(h, tail, (400, 0), (500, 100));
|
||||
}
|
||||
|
||||
/// The same fraction either way round: after a 100 px child in a 400 px row,
|
||||
/// `rel(0.5)` is 100 to 300 whether the child's own rule says so or the child
|
||||
/// drew half of what it was offered and reported that. Half the row, not half
|
||||
/// of the 300 px left of it.
|
||||
#[test]
|
||||
fn a_reported_fraction_is_of_the_row_like_a_declared_one() {
|
||||
let mut declaring = Harness::new((400, 100));
|
||||
let head = rect(Color::RED).width(100).add(&mut declaring.rsc);
|
||||
let declared = rect(Color::GREEN).width(rel(0.5)).add(&mut declaring.rsc);
|
||||
declaring.set_root((head, declared).span(Dir::RIGHT).width(rel(1.0)));
|
||||
assert_corners!(declaring, declared, (100, 0), (300, 100));
|
||||
|
||||
let mut reporting = Harness::new((400, 100));
|
||||
let head = rect(Color::RED).width(100).add(&mut reporting.rsc);
|
||||
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut reporting.rsc);
|
||||
let reported = (inner,).span(Dir::RIGHT).add(&mut reporting.rsc);
|
||||
reporting.set_root((head, reported).span(Dir::RIGHT).width(rel(1.0)));
|
||||
assert_corners!(reporting, reported, (100, 0), (300, 100));
|
||||
}
|
||||
|
||||
/// What the fraction a child reports is of and what box it is offered are
|
||||
/// two different lengths, and only the first is the whole row: a text still
|
||||
/// wraps at the room actually left after its neighbour, so the same
|
||||
/// paragraph is taller where less of the row is left for it.
|
||||
#[test]
|
||||
fn a_text_in_a_span_wraps_at_the_room_left_rather_than_the_whole_row() {
|
||||
let paragraph = "Wrapping shapes one source into as many lines as the box \
|
||||
leaves room for, so a paragraph's height is an answer.";
|
||||
let height_after = |head_width: i32| {
|
||||
let mut h = Harness::new((400, 400));
|
||||
let head = rect(Color::RED).width(head_width).add(&mut h.rsc);
|
||||
let text = wtext(paragraph).size(16).wrap(true).add(&mut h.rsc);
|
||||
h.set_root((head, text).span(Dir::RIGHT).width(rel(1.0)));
|
||||
let region = h.region(&text).unwrap();
|
||||
(region.bot_right.y - region.top_left.y).to_f32()
|
||||
};
|
||||
|
||||
let (crowded, whole_row) = (height_after(300), height_after(0));
|
||||
assert!(crowded > whole_row, "{crowded} against {whole_row}");
|
||||
}
|
||||
|
||||
/// Padding is outside what it pads, so a fraction under one is a fraction of
|
||||
/// the box the padding is measured from: half of a 400 px row is 200, and
|
||||
/// the pad is that plus both edges. Inset it instead and `rel` would mean the
|
||||
/// inner box while `px` meant the outer one, which is the one thing a length
|
||||
/// may not do.
|
||||
#[test]
|
||||
fn a_pad_is_outside_the_fraction_its_child_asked_for() {
|
||||
let mut h = Harness::new((400, 100));
|
||||
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc);
|
||||
let padded = (inner,).span(Dir::RIGHT).pad(10).add(&mut h.rsc);
|
||||
let tail = rect(Color::BLUE).width(100).add(&mut h.rsc);
|
||||
// Ruled to the window: a root reporting a fraction of it is otherwise
|
||||
// placed inside it by its own alignment, which is not what is under test.
|
||||
h.set_root((padded, tail).span(Dir::RIGHT).width(rel(1.0)));
|
||||
|
||||
assert_corners!(h, padded, (0, 0), (220, 100));
|
||||
assert_corners!(h, tail, (220, 0), (320, 100));
|
||||
}
|
||||
|
||||
/// The other half of the pair: an inset takes its room off the inside, so it
|
||||
/// is exactly as long as the box it was given and the fraction its child
|
||||
/// asked for is a fraction of what is left inside. Half of the 380 left in a
|
||||
/// 400 px row is 190, and the inset is the whole 400.
|
||||
#[test]
|
||||
fn an_inset_is_inside_the_fraction_its_child_asked_for() {
|
||||
let mut h = Harness::new((400, 100));
|
||||
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc);
|
||||
let inset = (inner,).span(Dir::RIGHT).inset(10).add(&mut h.rsc);
|
||||
h.set_root((inset,).span(Dir::RIGHT).width(rel(1.0)));
|
||||
|
||||
assert_corners!(h, inset, (0, 0), (200, 100));
|
||||
assert_corners!(h, inner, (10, 0), (200, 100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_span_ruled_across_itself_does_not_measure_its_children_there() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
@@ -158,7 +254,7 @@ fn a_moved_subtree_takes_its_children_with_it() {
|
||||
let mut h = Harness::new((400, 400));
|
||||
let first = rect(Color::RED).height(40).add(&mut h.rsc);
|
||||
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
||||
let row = inner.pad(10).height(40).region_node().add(&mut h.rsc);
|
||||
let row = inner.inset(10).height(40).region_node().add(&mut h.rsc);
|
||||
// 80 of fixed rows in a 400 window, so the span takes 80 and sits in the
|
||||
// middle of what it was given.
|
||||
h.set_root((first, row).span(Dir::DOWN));
|
||||
@@ -201,7 +297,7 @@ fn a_box_with_a_fixed_length_can_be_stretched_on_its_other_axis() {
|
||||
// impossible to take out of: recovering a fraction of a box needs a
|
||||
// relative extent, and it has none on that axis.
|
||||
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
||||
let row = inner.pad(10).height(40).add(&mut h.rsc);
|
||||
let row = inner.inset(10).height(40).add(&mut h.rsc);
|
||||
let filler = rect(Color::GREEN).add(&mut h.rsc);
|
||||
// This column is an item in a row, so it takes the width left for it
|
||||
// rather than asking for a full row-width in addition to the bar.
|
||||
@@ -225,21 +321,21 @@ fn only_a_region_node_lengthens_the_chain_and_it_can_be_removed() {
|
||||
h.set_root((bar, buried).span(Dir::RIGHT));
|
||||
|
||||
let move_idx = h.render.active[&leaf.id()].parent_move;
|
||||
assert_eq!(h.render.moves.depth(move_idx), 1, "only the root region");
|
||||
assert_eq!(h.render.moves.depth(move_idx), 0, "the window is no entry");
|
||||
|
||||
h.rsc.widgets_mut().set_region_node(buried, true);
|
||||
h.frame();
|
||||
let move_idx = h.render.active[&leaf.id()].parent_move;
|
||||
assert_eq!(
|
||||
h.render.moves.depth(move_idx),
|
||||
2,
|
||||
"the opted-in widget's region and the root region"
|
||||
1,
|
||||
"the opted-in widget's region alone"
|
||||
);
|
||||
|
||||
h.rsc.widgets_mut().set_region_node(buried, false);
|
||||
h.frame();
|
||||
let move_idx = h.render.active[&leaf.id()].parent_move;
|
||||
assert_eq!(h.render.moves.depth(move_idx), 1);
|
||||
assert_eq!(h.render.moves.depth(move_idx), 0);
|
||||
}
|
||||
|
||||
/// A span that sizes from its children passes their `leftover` weight up
|
||||
@@ -266,6 +362,11 @@ fn nested_spans_divide_the_space_once_however_deep_the_nesting_is() {
|
||||
|
||||
/// The same space, unevenly nested: weights carried up mean a share is a
|
||||
/// share of the whole, not of whatever branch a widget happens to sit in.
|
||||
///
|
||||
/// Each edge lands on the even division or one step below it, since a share
|
||||
/// is a fraction of the room and a truncating multiply gives up what that
|
||||
/// fraction does not divide. What stays exact is that each share starts
|
||||
/// where the last one ended and the row ends at its own edge.
|
||||
#[test]
|
||||
fn an_uneven_nesting_still_gives_every_share_the_same_length() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
@@ -279,10 +380,24 @@ fn an_uneven_nesting_still_gives_every_share_the_same_length() {
|
||||
let three = (b, c, d).span(Dir::RIGHT).add(&mut h.rsc);
|
||||
h.set_root((one, three).span(Dir::RIGHT));
|
||||
|
||||
let mut start = Px::ZERO;
|
||||
for (i, id) in [a, b, c, d].into_iter().enumerate() {
|
||||
let x = i as f32 * 100.0;
|
||||
assert_corners!(h, id, (x, 0), (x + 100.0, 200));
|
||||
let got = h.region(&id).expect("widget drew nothing");
|
||||
let even = Px::from_int((i as i32 + 1) * 100);
|
||||
assert_eq!(got.top_left, PxVec2::new(start, Px::ZERO), "share {i}");
|
||||
assert_eq!(got.bot_right.y, Px::from_int(200), "share {i}");
|
||||
assert!(
|
||||
got.bot_right.x == even || got.bot_right.x == even.next_down(),
|
||||
"share {i} ends at {:?}, not {even:?}",
|
||||
got.bot_right.x
|
||||
);
|
||||
start = got.bot_right.x;
|
||||
}
|
||||
assert_eq!(
|
||||
start,
|
||||
Px::from_int(400),
|
||||
"the row stopped short of its edge"
|
||||
);
|
||||
}
|
||||
|
||||
/// However many ways a row is divided, the shares add up to the row: each
|
||||
@@ -322,16 +437,15 @@ fn a_row_of_equal_shares_fills_it_exactly() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the shader puts an edge: the two parts of a scalar are floored
|
||||
/// apart, so a fraction and a pixel offset snap independently, and each is
|
||||
/// taken to the boundary it composes to within half a step of. Kept in step
|
||||
/// with `snap_floor` in `prelude.wgsl`.
|
||||
/// Where the shader puts an edge: the fraction resolved against the window
|
||||
/// plus the pixel offset, taken to the boundary it composes to within half
|
||||
/// a step of. Kept in step with `snap_floor` in `prelude.wgsl`.
|
||||
fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) {
|
||||
let active = &h.render.active[&id];
|
||||
let region = h.render.moves.resolve(active.parent_move, active.region);
|
||||
let dim = h.size().axis(axis);
|
||||
let snap = |v: f32| (v + Px::STEP.to_f32() * 0.5).floor();
|
||||
let edge = |s: Len| snap(s.rel.to_f32() * dim) + snap(s.px.to_f32());
|
||||
let edge = |s: Len| snap(s.rel.to_f32() * dim + s.px.to_f32());
|
||||
let span = region.axis(axis);
|
||||
(edge(span.start), edge(span.end))
|
||||
}
|
||||
@@ -480,3 +594,124 @@ fn leftover_children_disappear_at_the_exact_fixed_content_boundary() {
|
||||
assert!(h.region(&a).is_none());
|
||||
assert!(h.region(&b).is_none());
|
||||
}
|
||||
|
||||
/// **A stack child smaller than the stack sits where its own alignment
|
||||
/// says.** `Stack` gives every child the box its sizing child defines and
|
||||
/// used to force the near edge on all of them; that override is owed only to
|
||||
/// the sizing child, which has already placed its own content in the box the
|
||||
/// stack derived from its answer. Every other child is handed a box that owes
|
||||
/// nothing to it, so where it sits in one bigger than itself is its own
|
||||
/// business -- and with the override it could not be aligned at all, which is
|
||||
/// what moved the `tabs` example's counters to the wrong corner.
|
||||
#[test]
|
||||
fn a_stack_child_smaller_than_the_stack_keeps_its_own_alignment() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let big = rect(Color::BLUE).add(&mut h.rsc);
|
||||
let small = rect(Color::RED).sized((50, 50)).add(&mut h.rsc);
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_alignment(small.id(), Axis::X, AxisAlign::POS);
|
||||
let (a, b) = (big.add_strong(&mut h.rsc), small.add_strong(&mut h.rsc));
|
||||
let children: Vec<StrongWidget> = vec![a, b];
|
||||
h.set_root(Stack {
|
||||
children,
|
||||
size: StackSize::Default,
|
||||
});
|
||||
|
||||
assert_corners!(h, big, (0, 0), (400, 200));
|
||||
// The far edge on X because it asked for it, the middle on Y because
|
||||
// that is the default.
|
||||
assert_corners!(h, small, (350, 75), (400, 125));
|
||||
}
|
||||
/// Five children of one span, buried under three containers that are each a
|
||||
/// fraction of their parent so no length reaches the window without being
|
||||
/// composed and rounded on the way. Returns each child's drawn width and
|
||||
/// each gap between them, in pixels.
|
||||
fn row_under_fractions(kid: Option<LayoutLen>, gap: f32, box_w: f32) -> (Vec<Px>, Vec<Px>) {
|
||||
let mut h = Harness::new((box_w, 400.0));
|
||||
let mut ids = Vec::new();
|
||||
let mut kids: Vec<StrongWidget> = Vec::new();
|
||||
for _ in 0..5 {
|
||||
let r = rect(Color::RED).add(&mut h.rsc);
|
||||
if let Some(len) = kid {
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_size_rule(r.id(), Axis::X, SizeRule::Exact(len));
|
||||
}
|
||||
ids.push(r.id());
|
||||
kids.push(r.add_strong(&mut h.rsc));
|
||||
}
|
||||
let span = Span {
|
||||
children: kids,
|
||||
dir: Dir::RIGHT,
|
||||
gap: Px::from_f32(gap),
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
let a = (span.width(rel(0.9)),).span(Dir::RIGHT).add(&mut h.rsc);
|
||||
let b = (a.width(rel(0.8)),).span(Dir::RIGHT).add(&mut h.rsc);
|
||||
h.set_root((b.width(rel(0.7)),).span(Dir::RIGHT));
|
||||
let boxes: Vec<_> = ids
|
||||
.iter()
|
||||
.map(|id| h.region(id).expect("a child drew nothing"))
|
||||
.collect();
|
||||
(
|
||||
boxes.iter().map(|b| b.bot_right.x - b.top_left.x).collect(),
|
||||
boxes
|
||||
.windows(2)
|
||||
.map(|p| p[1].top_left.x - p[0].bot_right.x)
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// **A length given in pixels is that many pixels, wherever it ends up.** A
|
||||
/// gap and a declared width compose additively -- `Len::within` adds a part's
|
||||
/// own pixels rather than scaling them, and both ends of a gap carry the same
|
||||
/// fraction, so the multiply that rounds is the same on each -- which is why
|
||||
/// nesting the row inside fractions of fractions cannot move them. Swept over
|
||||
/// 2,100 box widths when this was written and exact at every one; five here,
|
||||
/// including widths that divide badly by five.
|
||||
#[test]
|
||||
fn a_length_in_pixels_is_that_many_pixels_however_it_is_nested() {
|
||||
for box_w in [300.0, 1000.0, 1001.0, 1003.0, 1920.0] {
|
||||
let want = Px::from_int(7);
|
||||
let (_, gaps) = row_under_fractions(None, 7.0, box_w);
|
||||
assert!(
|
||||
gaps.iter().all(|g| *g == want),
|
||||
"box {box_w}: gaps between leftover children are {gaps:?}"
|
||||
);
|
||||
let (widths, gaps) = row_under_fractions(Some(LayoutLen::px(100.0)), 7.0, box_w);
|
||||
assert!(
|
||||
gaps.iter().all(|g| *g == want),
|
||||
"box {box_w}: gaps between fixed children are {gaps:?}"
|
||||
);
|
||||
assert!(
|
||||
widths.iter().all(|w| *w == Px::from_int(100)),
|
||||
"box {box_w}: declared widths came out {widths:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// **Children asking for the same share of a row are not the same length**,
|
||||
/// and this pins by how much rather than claiming they are equal. A position
|
||||
/// is the quantity that gets rounded, so the row fills exactly and no two
|
||||
/// children leave a seam; what that costs is a step or two between lengths
|
||||
/// that were asked for identically. Exact composition would shrink the
|
||||
/// spread, not remove it: five equal lengths cannot fill a row whose step
|
||||
/// count is not a multiple of five.
|
||||
#[test]
|
||||
fn equal_shares_differ_by_at_most_two_steps_and_fill_the_row() {
|
||||
for kid in [None, Some(LayoutLen::rel(0.2))] {
|
||||
for box_w in [300.0, 1000.0, 1001.0, 1003.0, 1920.0] {
|
||||
let (widths, gaps) = row_under_fractions(kid, 0.0, box_w);
|
||||
let spread = *widths.iter().max().unwrap() - *widths.iter().min().unwrap();
|
||||
assert!(
|
||||
spread <= Px::from_raw(2),
|
||||
"box {box_w}, {kid:?}: widths {widths:?} spread {spread:?}"
|
||||
);
|
||||
assert!(
|
||||
gaps.iter().all(|g| *g == Px::ZERO),
|
||||
"box {box_w}, {kid:?}: children left seams {gaps:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
//! The tree a seed describes, as a value rather than as widgets.
|
||||
//!
|
||||
//! Two things have to hold for a plan to be worth having. Editing a plan has
|
||||
//! to mean what growing with those edits means, or a scenario reads one thing
|
||||
//! and the oracle another. And reducing a plan has to end, or a shrinker
|
||||
//! searching for the smallest counterexample never returns.
|
||||
|
||||
use iris::random::{Edits, Kind, Plan, Rng, SpanEdit, plan};
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn some_edits(seed: u64, of: &Plan) -> Edits {
|
||||
let mut rng = Rng::new(seed);
|
||||
let (mut sized, mut aligned, mut nodes, mut spans) = (0, 0, 0, 0);
|
||||
let mut of = of.clone();
|
||||
of.walk_mut(&mut |p| {
|
||||
if matches!(p.kind, Kind::Span { .. }) {
|
||||
spans += 1;
|
||||
}
|
||||
sized += p.size.is_some() as usize;
|
||||
aligned += p.align.is_some() as usize;
|
||||
nodes += p.region_node.is_some() as usize;
|
||||
});
|
||||
let pick =
|
||||
|n: usize, rng: &mut Rng| -> Vec<usize> { (0..n).filter(|_| rng.chance()).collect() };
|
||||
Edits {
|
||||
sizes: pick(sized, &mut rng)
|
||||
.into_iter()
|
||||
.map(|i| (i, [Some(LayoutLen::LEFTOVER), None]))
|
||||
.collect(),
|
||||
aligns: pick(aligned, &mut rng)
|
||||
.into_iter()
|
||||
.map(|i| (i, [Some(AxisAlign::POS), None]))
|
||||
.collect(),
|
||||
nodes: pick(nodes, &mut rng)
|
||||
.into_iter()
|
||||
.map(|i| (i, true))
|
||||
.collect(),
|
||||
spans: pick(spans, &mut rng)
|
||||
.into_iter()
|
||||
.map(|i| {
|
||||
(
|
||||
i,
|
||||
SpanEdit {
|
||||
detach: vec![0],
|
||||
attach: 2,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<HashMap<_, _>>(),
|
||||
fixed_branches: false,
|
||||
}
|
||||
}
|
||||
|
||||
use iris::prelude::*;
|
||||
|
||||
/// The two routes to an edited tree are one tree. `plan` resolves edits out
|
||||
/// of the random stream as it draws; `edited` puts them on a tree that
|
||||
/// already exists, which is the only route a shrunk plan has, since no seed
|
||||
/// grows one. A scenario written against either has to read the same.
|
||||
#[test]
|
||||
fn editing_a_plan_is_growing_one_with_those_edits() {
|
||||
for seed in 1..=60 {
|
||||
let bare = plan(seed, 5, &Edits::default());
|
||||
let edits = some_edits(seed, &bare);
|
||||
assert_eq!(
|
||||
bare.edited(&edits),
|
||||
plan(seed, 5, &edits),
|
||||
"seed {seed}: edited and grown-with-edits disagree"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Every simplification is strictly smaller, so taking them in turn reaches a
|
||||
/// fixed point instead of circling. A shrinker that can return to a tree it
|
||||
/// has already tried does not stop.
|
||||
#[test]
|
||||
fn every_simplification_of_a_plan_is_smaller_than_it() {
|
||||
for seed in 1..=60 {
|
||||
let tree = plan(seed, 4, &Edits::default());
|
||||
let mut queue = vec![tree];
|
||||
let mut seen = 0;
|
||||
while let Some(node) = queue.pop() {
|
||||
seen += 1;
|
||||
if seen > 400 {
|
||||
break;
|
||||
}
|
||||
for small in node.smaller() {
|
||||
assert!(
|
||||
small.size() <= node.size(),
|
||||
"seed {seed}: a simplification grew from {} to {}",
|
||||
node.size(),
|
||||
small.size()
|
||||
);
|
||||
if small.size() < node.size() {
|
||||
queue.push(small);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reducing until nothing reduces ends, and ends at something small enough to
|
||||
/// read rather than at the tree it started from.
|
||||
#[test]
|
||||
fn reducing_a_plan_all_the_way_ends() {
|
||||
for seed in 1..=30 {
|
||||
let mut node = plan(seed, 5, &Edits::default());
|
||||
let grown = node.size();
|
||||
let mut steps = 0;
|
||||
while let Some(next) = node.smaller().into_iter().next() {
|
||||
node = next;
|
||||
steps += 1;
|
||||
assert!(steps < 10_000, "seed {seed}: reducing did not end");
|
||||
}
|
||||
assert!(
|
||||
node.size() < grown.max(2),
|
||||
"seed {seed}: reduced {grown} widgets to {}",
|
||||
node.size()
|
||||
);
|
||||
}
|
||||
}
|
||||
+16
-1
@@ -462,7 +462,7 @@ fn a_change_two_levels_under_its_reader_still_reaches_it() {
|
||||
let (leaf, _) = counted(&mut h, Size::px((100, 100).into()), true);
|
||||
let padded = leaf.pad(10).add(&mut h.rsc);
|
||||
let below = rect(Color::RED).add(&mut h.rsc);
|
||||
h.set_root((padded, below).span(Dir::DOWN).pad(12));
|
||||
h.set_root((padded, below).span(Dir::DOWN).inset(12));
|
||||
assert_corners!(h, below, (12, 132), (388, 388));
|
||||
|
||||
h.rsc[leaf].size = Size::px((100, 200).into());
|
||||
@@ -613,3 +613,18 @@ fn a_stacks_sizing_child_is_drawn_once_where_it_belongs() {
|
||||
assert_ne!(layer(front.id()), layer(background.id()));
|
||||
assert_eq!(draws.get(), 1);
|
||||
}
|
||||
|
||||
/// A widget's own mask is not the one it inherited, and a redraw of it
|
||||
/// inherits the second: handing back the first is handing it its own mask to
|
||||
/// set a second time, which `set_mask` asserts against.
|
||||
#[test]
|
||||
fn a_masked_widget_redrawn_on_its_own_sets_its_mask_again() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
||||
let masked = inner.masked().add(&mut h.rsc);
|
||||
let other = rect(Color::RED).width(100).add(&mut h.rsc);
|
||||
h.set_root((other, masked).span(Dir::RIGHT));
|
||||
h.rsc.widgets_mut().get_dyn_mut(masked.id());
|
||||
h.frame();
|
||||
assert_corners!(h, inner, (100, 0), (400, 200));
|
||||
}
|
||||
+219
-3
@@ -3,12 +3,16 @@
|
||||
//! frame that had not settled: a wrapping text shaped at a width it was
|
||||
//! measured in rather than the one it was given. The rest are a widget
|
||||
//! measured again in a box its own answer had decided, where the old answer
|
||||
//! is a fixed point whatever the content now says. The last is neither: one
|
||||
//! box length, composed two ways, landing either side of the boundary that
|
||||
//! decided whether a child was drawn at all.
|
||||
//! is a fixed point whatever the content now says. The last three are
|
||||
//! neither: one box length, composed two ways, landing either side of the
|
||||
//! boundary that decided whether a child was drawn at all, and two boxes
|
||||
//! reached through a region node's own entry rather than through the offer
|
||||
//! that node was given. The last is a wrapping text handed back the width
|
||||
//! it measured, rounded to a step below the line it measured there.
|
||||
|
||||
use iris::harness::Harness;
|
||||
use iris::prelude::*;
|
||||
use iris::random::Branch;
|
||||
|
||||
/// Six widgets, shrunk from a 402-widget tree the fuzzer found. Nothing about
|
||||
/// the tree changes -- every widget is marked for redraw and the frame is
|
||||
@@ -398,3 +402,215 @@ fn a_box_that_only_rounds_past_its_fixed_children_leaves_nothing_over() {
|
||||
}
|
||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||
}
|
||||
|
||||
/// Five widgets, shrunk by `tests/shrink.rs` from the 277 the oracle's seed
|
||||
/// 18 grows at depth 6. A scroll inside a scroll, the inner one owning a
|
||||
/// movable region of its own, and only its text marked for redraw. Nothing
|
||||
/// about the tree changes, so no box may.
|
||||
fn plant_nested_scrolls(h: &mut Harness) -> Vec<WidgetId> {
|
||||
let text = wtext("one line, overflowing whatever it is given")
|
||||
.size(16)
|
||||
.wrap(false)
|
||||
.add(&mut h.rsc);
|
||||
let inner = Scroll::new(text.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
|
||||
h.rsc.widgets_mut().set_region_node(inner.id(), true);
|
||||
let filler = rect(Color::RED).add(&mut h.rsc);
|
||||
h.rsc.widgets_mut().set_size_rules(
|
||||
filler.id(),
|
||||
Some(LayoutLen::px(87.0)),
|
||||
Some(LayoutLen::px(24.0)),
|
||||
);
|
||||
let span = Span {
|
||||
children: vec![inner.add_strong(&mut h.rsc), filler.add_strong(&mut h.rsc)],
|
||||
dir: Dir::DOWN,
|
||||
gap: Px::ZERO,
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
let root = Scroll::new(span.add_strong(&mut h.rsc), Axis::Y).add(&mut h.rsc);
|
||||
h.set_root(root);
|
||||
vec![text.id(), inner.id(), filler.id(), span.id(), root.id()]
|
||||
}
|
||||
|
||||
/// A local redraw asks a dirty widget in the box its parent gave it, and only
|
||||
/// where that box is as long as the one it was offered; anything else is a
|
||||
/// question its parent has to ask. This inner scroll's offer is the outer
|
||||
/// scroll's whole viewport and the box it was given is 24px shorter -- the
|
||||
/// height of the sized child the outer scroll snaps to the end of -- so what
|
||||
/// it must not do is settle itself. It was drawn at its offer once, and the
|
||||
/// inner scroll and its text stayed 24px too low.
|
||||
#[test]
|
||||
fn redrawing_one_widget_does_not_move_what_scrolls_around_it() {
|
||||
let mut warm = Harness::new((900, 1200));
|
||||
let ids = plant_nested_scrolls(&mut warm);
|
||||
warm.rsc.widgets_mut().get_dyn_mut(ids[0]);
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((900, 1200));
|
||||
let cold_ids = plant_nested_scrolls(&mut cold);
|
||||
|
||||
let mut wrong = Vec::new();
|
||||
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||
if got != want {
|
||||
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
||||
}
|
||||
}
|
||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||
}
|
||||
|
||||
/// Ten widgets, of the shape `tests/shrink.rs` reduces the oracle's seed 220
|
||||
/// to. The pad owns a movable region and is the scroll's content, so the box
|
||||
/// the scroll places it in is as long as that content while the box it was
|
||||
/// offered is the viewport -- and with no padding to tell those two apart,
|
||||
/// the span inside it looked like it was still at its offer. So everything
|
||||
/// under the pad was asked again in the *placed* box, the offer resolving
|
||||
/// against the node's own entry, which holds that box: the texts kept the
|
||||
/// widths they had, the content stayed the length those widths make, and the
|
||||
/// old answer confirmed itself. What the branch adds is a tree that differs
|
||||
/// rather than a box that moved, since a probe measured at the wrong width
|
||||
/// takes the other side.
|
||||
fn plant_under_a_node(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget<Span>; 2]) {
|
||||
let probe = rect(Color::RED).add(&mut h.rsc);
|
||||
let wide = rect(Color::GREEN).add(&mut h.rsc);
|
||||
let narrow = rect(Color::BLUE).add(&mut h.rsc);
|
||||
let branch = Branch {
|
||||
probe: probe.add_strong(&mut h.rsc),
|
||||
wide: wide.add_strong(&mut h.rsc),
|
||||
narrow: narrow.add_strong(&mut h.rsc),
|
||||
threshold: 213.0,
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
let wrapped = wtext(
|
||||
"Wrapping shapes one source into as many lines as the box \
|
||||
leaves room for, so a paragraph's height is an answer and not a setting.",
|
||||
)
|
||||
.size(16)
|
||||
.wrap(true)
|
||||
.add(&mut h.rsc);
|
||||
let plain = wtext("one line, overflowing whatever it is given")
|
||||
.size(16)
|
||||
.wrap(false)
|
||||
.add(&mut h.rsc);
|
||||
let row = |h: &mut Harness, mut children: Vec<StrongWidget>| {
|
||||
if swapped {
|
||||
children.rotate_left(1);
|
||||
}
|
||||
Span {
|
||||
children,
|
||||
dir: Dir::RIGHT,
|
||||
gap: Px::ZERO,
|
||||
}
|
||||
.add(&mut h.rsc)
|
||||
};
|
||||
let texts: Vec<StrongWidget> =
|
||||
vec![wrapped.add_strong(&mut h.rsc), plain.add_strong(&mut h.rsc)];
|
||||
let inner = row(h, texts);
|
||||
let pair: Vec<StrongWidget> = vec![branch.add_strong(&mut h.rsc), inner.add_strong(&mut h.rsc)];
|
||||
let outer = row(h, pair);
|
||||
let pad = Pad {
|
||||
padding: Padding::ZERO,
|
||||
inner: outer.add_strong(&mut h.rsc),
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
h.rsc.widgets_mut().set_region_node(pad.id(), true);
|
||||
let root = Scroll::new(pad.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
|
||||
h.set_root(root);
|
||||
(
|
||||
vec![
|
||||
probe.id(),
|
||||
wide.id(),
|
||||
narrow.id(),
|
||||
branch.id(),
|
||||
wrapped.id(),
|
||||
plain.id(),
|
||||
inner.id(),
|
||||
outer.id(),
|
||||
pad.id(),
|
||||
root.id(),
|
||||
],
|
||||
[outer, inner],
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered() {
|
||||
let mut warm = Harness::new((900, 1200));
|
||||
let (ids, spans) = plant_under_a_node(&mut warm, false);
|
||||
warm.frame();
|
||||
for span in spans {
|
||||
warm.rsc[span].children.rotate_left(1);
|
||||
}
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((900, 1200));
|
||||
let (cold_ids, _) = plant_under_a_node(&mut cold, true);
|
||||
cold.frame();
|
||||
|
||||
let mut wrong = Vec::new();
|
||||
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||
if got != want {
|
||||
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
||||
}
|
||||
}
|
||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||
}
|
||||
|
||||
const PARAGRAPH: &str = "Wrapping shapes one source into as many lines as the \
|
||||
box leaves room for, so a paragraph's height is an answer and not a setting.";
|
||||
|
||||
/// Eight widgets, shrunk from a 118-widget tree (seed 1121, depth 4,
|
||||
/// `shuffle-swap-for-three`). The stack takes its size from the span above,
|
||||
/// the span takes its width from the longest line of the texts in it, and
|
||||
/// the text below the span is then wrapped at that width -- so a width the
|
||||
/// shaper measured comes back to it as the box to break in.
|
||||
fn plant_a_measured_width(h: &mut Harness, swapped: bool) -> (WeakWidget<Span>, WidgetId) {
|
||||
let first: StrongWidget = rect(Color::YELLOW).add_strong(&mut h.rsc);
|
||||
let mut inner = Span::empty(Dir::UP);
|
||||
inner.children = match swapped {
|
||||
true => swapped_in(h),
|
||||
false => vec![first],
|
||||
};
|
||||
let inner = inner.height(142).add(&mut h.rsc);
|
||||
let text = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
|
||||
let stack = Stack {
|
||||
children: vec![inner.add_strong(&mut h.rsc), text.add_strong(&mut h.rsc)],
|
||||
size: StackSize::Child(0),
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
h.set_root((stack,).span(Dir::DOWN).width(195));
|
||||
(inner, text.id())
|
||||
}
|
||||
|
||||
/// What the span holds once its children have been swapped, which is what
|
||||
/// the warm tree is changed to and what the cold one is grown with.
|
||||
fn swapped_in(h: &mut Harness) -> Vec<StrongWidget> {
|
||||
let paragraph = |h: &mut Harness| -> StrongWidget {
|
||||
wtext(PARAGRAPH).size(16).wrap(true).add_strong(&mut h.rsc)
|
||||
};
|
||||
vec![
|
||||
paragraph(h),
|
||||
rect(Color::YELLOW).add_strong(&mut h.rsc),
|
||||
paragraph(h),
|
||||
]
|
||||
}
|
||||
|
||||
/// A text handed back the width it measured breaks there the way it broke
|
||||
/// when it measured it. The width the shaper answers is not on the grid, and
|
||||
/// a report rounded to the nearest step is under the longest line half the
|
||||
/// time: a warm tree then keeps a break made in a wider box while a cold one
|
||||
/// makes a narrower break in the same box, and the paragraph gains a line.
|
||||
#[test]
|
||||
fn a_text_is_given_back_a_box_the_line_it_measured_fits_in() {
|
||||
let mut warm = Harness::new((900, 1200));
|
||||
let (inner, text) = plant_a_measured_width(&mut warm, false);
|
||||
warm.frame();
|
||||
warm.rsc[inner].children = swapped_in(&mut warm);
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((900, 1200));
|
||||
let (_, cold_text) = plant_a_measured_width(&mut cold, true);
|
||||
cold.frame();
|
||||
|
||||
assert_eq!(warm.region(&text), cold.region(&cold_text));
|
||||
}
|
||||
+80
-538
@@ -1,19 +1,21 @@
|
||||
//! Random trees, checked against building the same tree cold.
|
||||
//! Laying a tree out again has to land where growing it that way would.
|
||||
//!
|
||||
//! A frame reaches its layout by keeping most of the last one: movable regions
|
||||
//! or primitive boxes rewritten, some widgets drawn again, the rest untouched.
|
||||
//! The result must be the tree a cold start would have produced, so anything
|
||||
//! wrongly retained shows up as a difference in somebody's box.
|
||||
//! Every case is one of `scenario`'s, over the trees `iris::random` grows
|
||||
//! from a seed. The fast test takes a handful of seeds and the ignored one
|
||||
//! takes as many as it is asked for; both run the same cases the shrinker
|
||||
//! does over the same trees, so a seed that fails here is reduced by
|
||||
//!
|
||||
//! `iris::random` grows the tree and `examples/random.rs` draws one. A seed is
|
||||
//! the whole reproduction; `a_long_run_of_seeds_agrees` is the ignored sweep
|
||||
//! for when it is worth spending the time.
|
||||
//! SHRINK_SEED=<seed> SHRINK_DEPTH=<depth> SHRINK_CASE=<case> \
|
||||
//! cargo test --release --test shrink -- --ignored --nocapture
|
||||
//!
|
||||
//! `IRIS_GENERATED_SEED`, `IRIS_GENERATED_SEEDS` and `IRIS_GENERATED_DEPTH`
|
||||
//! select what the long run covers.
|
||||
|
||||
use std::collections::HashMap;
|
||||
#[path = "scenario/mod.rs"]
|
||||
mod scenario;
|
||||
|
||||
use iris::harness::Harness;
|
||||
use iris::prelude::*;
|
||||
use iris::random::{Aligns, Edits, Lens, Rng, SpanEdit, Tree, grow};
|
||||
use iris::random::{Edits, plan};
|
||||
use scenario::{ALL, Case, diverges, env, over_seeds};
|
||||
|
||||
/// How deep the generator branches. The generator widens two to four ways per
|
||||
/// level, so depth is exponential in width and a deep narrow tree is not
|
||||
@@ -23,562 +25,102 @@ fn depth() -> usize {
|
||||
env("IRIS_GENERATED_DEPTH", 4)
|
||||
}
|
||||
|
||||
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
|
||||
std::env::var(name)
|
||||
.ok()
|
||||
.and_then(|value| value.parse().ok())
|
||||
.unwrap_or(fallback)
|
||||
}
|
||||
const SEEDS: [u64; 9] = [1, 2, 3, 5, 8, 10, 13, 86, 98];
|
||||
/// The seeds the ordinary tests take. Seven that have never failed; 86,
|
||||
/// which a `Scroll` fixed point once settled differently on; and 20, which
|
||||
/// caught a locally redrawn widget being placed twice in the box its parent
|
||||
/// had already placed it in.
|
||||
const SEEDS: [u64; 10] = [1, 2, 3, 5, 8, 10, 13, 20, 86, 98];
|
||||
|
||||
/// The same box, to a step of the grid per level of nesting between the two
|
||||
/// ways of reaching it. A move, a repaint and a row of shares land on the
|
||||
/// same number now; what is left is a box centred in a fraction of its parent
|
||||
/// against the same box centred in its own pixels. A step is a thousandth of
|
||||
/// a pixel, where this was a twentieth of one before any of it was on a grid.
|
||||
const AGREE_STEPS: i32 = 2;
|
||||
|
||||
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,
|
||||
fn check(seed: u64, depth: usize, case: Case) {
|
||||
let grown = plan(seed, depth, &Edits::default());
|
||||
if let Some(how) = diverges(&grown, case, seed) {
|
||||
panic!(
|
||||
"seed {seed} at depth {depth} differs after {}: {how}\n\
|
||||
reduce it with SHRINK_SEED={seed} SHRINK_DEPTH={depth} \
|
||||
SHRINK_CASE={} cargo test --release --test shrink -- --ignored --nocapture",
|
||||
case.name(),
|
||||
case.name(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn plant(h: &mut Harness, seed: u64, edits: &Edits) -> Tree {
|
||||
let (root, tree) = grow(&mut h.rsc, seed, depth(), edits);
|
||||
h.state.root = Some(root);
|
||||
h.frame();
|
||||
tree
|
||||
macro_rules! case {
|
||||
($name:ident, $case:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
for seed in SEEDS {
|
||||
check(seed, depth(), $case);
|
||||
}
|
||||
|
||||
fn resize_one(h: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Lens {
|
||||
let lens = [
|
||||
Some(LayoutLen::px(20.0 + rng.below(180) as f32)),
|
||||
Some(LayoutLen::px(20.0 + rng.below(180) as f32)),
|
||||
];
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_size_rules(tree.sized[idx], lens[0], lens[1]);
|
||||
lens
|
||||
}
|
||||
|
||||
/// Changes a few of the declared sizes, and says which, so the cold tree can
|
||||
/// be grown with the same ones.
|
||||
fn edit(h: &mut Harness, tree: &Tree, rng: &mut Rng) -> HashMap<usize, Lens> {
|
||||
let mut edits = HashMap::new();
|
||||
for _ in 0..4 {
|
||||
let idx = rng.below(tree.sized.len());
|
||||
edits.insert(idx, resize_one(h, tree, idx, rng));
|
||||
}
|
||||
edits
|
||||
}
|
||||
|
||||
/// Every declared size at once, so every reader of a size in the tree has a
|
||||
/// changed descendant in the same frame and the whole dirty set has to settle
|
||||
/// together.
|
||||
fn edit_every(h: &mut Harness, tree: &Tree, rng: &mut Rng) -> HashMap<usize, Lens> {
|
||||
(0..tree.sized.len())
|
||||
.map(|idx| (idx, resize_one(h, tree, idx, rng)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum Shuffle {
|
||||
/// Every other child, so what is left is interleaved with what went.
|
||||
EveryOther,
|
||||
/// Everything but the first, which is the last step before empty.
|
||||
AllButFirst,
|
||||
/// Three more on the end at once.
|
||||
AddThree,
|
||||
/// The first out and three more on, so the count moves both ways.
|
||||
SwapForThree,
|
||||
/// One out of the middle and one on the end.
|
||||
TradeOne,
|
||||
}
|
||||
|
||||
const SHUFFLES: [Shuffle; 5] = [
|
||||
Shuffle::EveryOther,
|
||||
Shuffle::AllButFirst,
|
||||
Shuffle::AddThree,
|
||||
Shuffle::SwapForThree,
|
||||
Shuffle::TradeOne,
|
||||
];
|
||||
|
||||
impl Shuffle {
|
||||
fn of(self, grown: usize) -> SpanEdit {
|
||||
let all = |step: usize, from: usize| (from..grown).step_by(step).collect();
|
||||
match self {
|
||||
Self::EveryOther => SpanEdit {
|
||||
detach: all(2, 0),
|
||||
attach: 0,
|
||||
},
|
||||
Self::AllButFirst => SpanEdit {
|
||||
detach: all(1, 1),
|
||||
attach: 0,
|
||||
},
|
||||
Self::AddThree => SpanEdit {
|
||||
detach: Vec::new(),
|
||||
attach: 3,
|
||||
},
|
||||
Self::SwapForThree => SpanEdit {
|
||||
detach: vec![0],
|
||||
attach: 3,
|
||||
},
|
||||
Self::TradeOne => SpanEdit {
|
||||
detach: vec![grown / 2],
|
||||
attach: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies `shuffle` to every third span, and says what it did so the cold
|
||||
/// tree can be grown that way. The widgets it takes out are given back: the
|
||||
/// last share of one must outlive the comparison, or its id is handed to
|
||||
/// something else and the two trees stop lining up.
|
||||
fn reshuffle(
|
||||
h: &mut Harness,
|
||||
tree: &mut Tree,
|
||||
shuffle: Shuffle,
|
||||
) -> (HashMap<usize, SpanEdit>, Vec<StrongWidget>) {
|
||||
let mut edits = HashMap::new();
|
||||
let mut detached = Vec::new();
|
||||
for (idx, span) in tree.spans.iter_mut().enumerate().step_by(3) {
|
||||
let span_edit = shuffle.of(span.grown);
|
||||
let mut take = span_edit.detach.clone();
|
||||
take.sort_unstable();
|
||||
let children = &mut h.rsc[span.id].children;
|
||||
// Highest first, so an index means the same child however many of
|
||||
// its neighbours are going too.
|
||||
for j in take.into_iter().rev() {
|
||||
if j < children.len() {
|
||||
detached.push(children.remove(j));
|
||||
}
|
||||
}
|
||||
let attach = span_edit.attach.min(span.spares.len());
|
||||
children.extend(span.spares.drain(..attach));
|
||||
edits.insert(idx, span_edit);
|
||||
}
|
||||
(edits, detached)
|
||||
}
|
||||
|
||||
/// What a widget was configured with, so a tree the generator found can be
|
||||
/// written out by hand. A fuzz failure is a lead; the fast test that replaces
|
||||
/// it has to be buildable from what the failure printed.
|
||||
fn describe(id: WidgetId, h: &Harness) -> String {
|
||||
let rules = h.rsc.widgets().size_rules(id);
|
||||
let rule = |r: SizeRule| match r.exact() {
|
||||
Some(len) => format!("{len}"),
|
||||
None => "-".into(),
|
||||
};
|
||||
let align = h.rsc.widgets().alignment(id);
|
||||
let side = |a: AxisAlign| {
|
||||
if a == AxisAlign::NEG {
|
||||
"neg".into()
|
||||
} else if a == AxisAlign::CENTER {
|
||||
"mid".into()
|
||||
} else if a == AxisAlign::POS {
|
||||
"pos".into()
|
||||
} else {
|
||||
format!("{:.2}", a.rel())
|
||||
}
|
||||
};
|
||||
// A rule and an alignment are properties of whatever carries them, so
|
||||
// they print with that widget rather than as widgets of their own.
|
||||
let mut out = describe_widget(id, h);
|
||||
if (rules.x, rules.y) != (SizeRule::Free, SizeRule::Free) {
|
||||
out += &format!("[x:{},y:{}]", rule(rules.x), rule(rules.y));
|
||||
}
|
||||
if align != RegionAlign::default() {
|
||||
out += &format!("@{},{}", side(align.x), side(align.y));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn describe_widget(id: WidgetId, h: &Harness) -> String {
|
||||
let label = h.rsc.widgets().label(id).to_string();
|
||||
let Some(widget) = h.rsc.widgets().get_dyn(id) else {
|
||||
return label;
|
||||
};
|
||||
let any: &dyn std::any::Any = widget;
|
||||
if let Some(w) = any.downcast_ref::<Span>() {
|
||||
let sign = if w.dir.sign == Sign::Neg { "-" } else { "+" };
|
||||
return format!(
|
||||
"Span{{dir:{:?}{sign},gap:{},n:{}}}",
|
||||
w.dir.axis,
|
||||
w.gap,
|
||||
w.children.len()
|
||||
case!(
|
||||
many_widgets_redrawing_at_once_leaves_every_box_where_it_was,
|
||||
Case::RepaintSome
|
||||
);
|
||||
}
|
||||
if let Some(w) = any.downcast_ref::<Pad>() {
|
||||
let p = &w.padding;
|
||||
return format!(
|
||||
"Pad{{l:{},r:{},t:{},b:{}}}",
|
||||
p.left, p.right, p.top, p.bottom
|
||||
case!(
|
||||
everything_redrawing_at_once_leaves_every_box_where_it_was,
|
||||
Case::Repaint
|
||||
);
|
||||
}
|
||||
if let Some(w) = any.downcast_ref::<Stack>() {
|
||||
return format!("Stack{{n:{}}}", w.children.len());
|
||||
}
|
||||
label
|
||||
}
|
||||
|
||||
/// Every widget in one tree against the matching widget in the other. A
|
||||
/// mismatch prints the widget's ancestry, marking region nodes, since where
|
||||
/// two trees disagree is rarely where the cause is.
|
||||
fn assert_same(seed: u64, what: &str, warm: (&Harness, &Tree), cold: (&Harness, &Tree)) {
|
||||
let ((wh, wt), (ch, ct)) = (warm, cold);
|
||||
assert_eq!(wt.ids.len(), ct.ids.len(), "seed {seed}: different trees");
|
||||
let mut drawn = 0;
|
||||
let mut wrong = 0;
|
||||
for (i, (&w, &c)) in wt.ids.iter().zip(&ct.ids).enumerate() {
|
||||
let (got, want) = (wh.region(&w), ch.region(&c));
|
||||
drawn += usize::from(got.is_some());
|
||||
// This oracle cares where rasterization lands, not whether equivalent
|
||||
// arithmetic produced the same f32. Keep the tolerance to one
|
||||
// twentieth of a physical pixel, while whether a widget drew remains
|
||||
// exact.
|
||||
if same_region(got, want) {
|
||||
continue;
|
||||
}
|
||||
wrong += 1;
|
||||
if wrong <= 3 {
|
||||
let mut chain = Vec::new();
|
||||
let mut at = Some(w);
|
||||
while let Some(id) = at {
|
||||
let active = &wh.render.active[&id];
|
||||
let node = match active.move_idx == active.parent_move {
|
||||
true => "",
|
||||
false => "*",
|
||||
};
|
||||
chain.push(format!("{}{node}", describe(id, wh)));
|
||||
at = active.parent;
|
||||
}
|
||||
println!(
|
||||
"seed {seed} after {what}: widget {i}\n warm {got:?}\n cold {want:?}\n {}",
|
||||
chain.join(" < ")
|
||||
case!(
|
||||
a_resize_lands_where_starting_at_that_size_would,
|
||||
Case::Resize
|
||||
);
|
||||
}
|
||||
}
|
||||
assert!(drawn > 0, "seed {seed}: nothing was drawn");
|
||||
assert_eq!(wrong, 0, "seed {seed}: {wrong} widgets differ after {what}");
|
||||
}
|
||||
|
||||
fn changed_size(seed: u64) {
|
||||
let mut warm = Harness::new((900, 1200));
|
||||
let grown = plant(&mut warm, seed, &Edits::default());
|
||||
// Not every tree grows a declared size to change.
|
||||
if grown.sized.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut rng = Rng::new(seed ^ 0x5eed);
|
||||
let sizes = edit(&mut warm, &grown, &mut rng);
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((900, 1200));
|
||||
let same = plant(
|
||||
&mut cold,
|
||||
seed,
|
||||
&Edits {
|
||||
sizes,
|
||||
..Default::default()
|
||||
},
|
||||
case!(
|
||||
a_resize_and_a_repaint_land_where_starting_that_way_would,
|
||||
Case::ResizeRepaint
|
||||
);
|
||||
assert_same(seed, "a size change", (&warm, &grown), (&cold, &same));
|
||||
}
|
||||
|
||||
/// Moves one widget to a different corner of the box it is given.
|
||||
fn realign_one(h: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Aligns {
|
||||
let mut side = || match rng.below(4) {
|
||||
0 => None,
|
||||
1 => Some(AxisAlign::NEG),
|
||||
2 => Some(AxisAlign::CENTER),
|
||||
_ => Some(AxisAlign::POS),
|
||||
};
|
||||
let aligns = [side(), side()];
|
||||
for (axis, align) in [Axis::X, Axis::Y].into_iter().zip(aligns) {
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_alignment(tree.aligned[idx], axis, align.unwrap_or_default());
|
||||
}
|
||||
aligns
|
||||
}
|
||||
|
||||
fn changed_alignment(seed: u64) {
|
||||
let mut warm = Harness::new((900, 1200));
|
||||
let grown = plant(&mut warm, seed, &Edits::default());
|
||||
if grown.aligned.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut rng = Rng::new(seed ^ 0xa11);
|
||||
let aligns = (0..grown.aligned.len())
|
||||
.step_by(3)
|
||||
.map(|idx| (idx, realign_one(&mut warm, &grown, idx, &mut rng)))
|
||||
.collect();
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((900, 1200));
|
||||
let same = plant(
|
||||
&mut cold,
|
||||
seed,
|
||||
&Edits {
|
||||
aligns,
|
||||
..Default::default()
|
||||
},
|
||||
case!(
|
||||
a_size_change_after_a_resize_lands_the_same_way,
|
||||
Case::ResizeSize
|
||||
);
|
||||
assert_same(seed, "an alignment change", (&warm, &grown), (&cold, &same));
|
||||
}
|
||||
|
||||
/// Giving a widget a movable region of its own, or taking it away, is a
|
||||
/// structural change: every primitive under it changes which chain resolves
|
||||
/// it. A cold tree built that way is what says the rebuild was complete.
|
||||
fn changed_region_node(seed: u64) {
|
||||
let mut warm = Harness::new((900, 1200));
|
||||
let grown = plant(&mut warm, seed, &Edits::default());
|
||||
if grown.nodes.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let nodes: HashMap<usize, bool> = (0..grown.nodes.len())
|
||||
.step_by(2)
|
||||
.map(|idx| {
|
||||
let id = grown.nodes[idx];
|
||||
let was = warm.rsc.widgets().is_region_node(id);
|
||||
warm.rsc.widgets_mut().set_region_node(id, !was);
|
||||
(idx, !was)
|
||||
})
|
||||
.collect();
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((900, 1200));
|
||||
let same = plant(
|
||||
&mut cold,
|
||||
seed,
|
||||
&Edits {
|
||||
nodes,
|
||||
..Default::default()
|
||||
},
|
||||
case!(
|
||||
a_size_change_lands_where_growing_it_that_way_would,
|
||||
Case::Size
|
||||
);
|
||||
assert_same(
|
||||
seed,
|
||||
"a region-node change",
|
||||
(&warm, &grown),
|
||||
(&cold, &same),
|
||||
case!(
|
||||
every_size_changing_at_once_lands_where_growing_it_that_way_would,
|
||||
Case::EverySize
|
||||
);
|
||||
}
|
||||
|
||||
fn reshuffled(seed: u64, shuffle: Shuffle) {
|
||||
let mut warm = Harness::new((900, 1200));
|
||||
let mut grown = plant(&mut warm, seed, &Edits::default());
|
||||
// Some seeds grow nothing but wrappers, and a shuffle with no span to
|
||||
// shuffle is not the same thing as one that had no effect. A span behind
|
||||
// a branch nobody took is the same kind of nothing: it is not drawn, so
|
||||
// shuffling it cannot move anything.
|
||||
let shuffles = grown
|
||||
.spans
|
||||
.iter()
|
||||
.step_by(3)
|
||||
.any(|span| warm.region(&span.id.id()).is_some());
|
||||
if !shuffles {
|
||||
return;
|
||||
}
|
||||
let (spans, _held) = reshuffle(&mut warm, &mut grown, shuffle);
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((900, 1200));
|
||||
let same = plant(
|
||||
&mut cold,
|
||||
seed,
|
||||
&Edits {
|
||||
spans,
|
||||
..Default::default()
|
||||
},
|
||||
case!(
|
||||
an_alignment_change_lands_where_growing_it_that_way_would,
|
||||
Case::Align
|
||||
);
|
||||
|
||||
let what = format!("{shuffle:?}");
|
||||
assert_same(seed, &what, (&warm, &grown), (&cold, &same));
|
||||
}
|
||||
|
||||
fn changed_every_size(seed: u64) {
|
||||
let mut warm = Harness::new((900, 1200));
|
||||
let grown = plant(&mut warm, seed, &Edits::default());
|
||||
if grown.sized.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut rng = Rng::new(seed ^ 0xa11);
|
||||
let sizes = edit_every(&mut warm, &grown, &mut rng);
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((900, 1200));
|
||||
let same = plant(
|
||||
&mut cold,
|
||||
seed,
|
||||
&Edits {
|
||||
sizes,
|
||||
..Default::default()
|
||||
},
|
||||
case!(
|
||||
giving_and_taking_a_movable_region_rebuilds_what_resolves_it,
|
||||
Case::RegionNode
|
||||
);
|
||||
assert_same(seed, "every size at once", (&warm, &grown), (&cold, &same));
|
||||
}
|
||||
|
||||
/// Marks a spread of widgets for redraw at once. Nothing changes, so no box
|
||||
/// may either; what this exercises is the order a frame settles a dirty set
|
||||
/// in, which the other cases reach one dependency path at a time.
|
||||
fn repainted_together(seed: u64) {
|
||||
let mut warm = Harness::new((900, 1200));
|
||||
let grown = plant(&mut warm, seed, &Edits::default());
|
||||
for &id in grown.ids.iter().step_by(5) {
|
||||
warm.rsc.widgets_mut().get_dyn_mut(id);
|
||||
}
|
||||
assert!(
|
||||
!warm.rsc.widgets().needs_redraw.is_empty(),
|
||||
"seed {seed}: nothing was marked"
|
||||
case!(
|
||||
reordering_a_span_lands_where_growing_it_that_way_would,
|
||||
Case::Reorder
|
||||
);
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((900, 1200));
|
||||
let same = plant(&mut cold, seed, &Edits::default());
|
||||
|
||||
let what = "many repaints at once";
|
||||
assert_same(seed, what, (&warm, &grown), (&cold, &same));
|
||||
}
|
||||
|
||||
fn resized(seed: u64) {
|
||||
let mut warm = Harness::new((1920, 1200));
|
||||
let grown = plant(&mut warm, seed, &Edits::default());
|
||||
warm.resize((640, 900));
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((640, 900));
|
||||
let same = plant(&mut cold, seed, &Edits::default());
|
||||
|
||||
assert_same(seed, "a resize", (&warm, &grown), (&cold, &same));
|
||||
}
|
||||
|
||||
fn resized_then_changed(seed: u64) {
|
||||
let mut warm = Harness::new((1920, 1200));
|
||||
let grown = plant(&mut warm, seed, &Edits::default());
|
||||
if grown.sized.is_empty() {
|
||||
return;
|
||||
}
|
||||
warm.resize((640, 900));
|
||||
warm.frame();
|
||||
|
||||
let mut rng = Rng::new(seed ^ 0xb0a7);
|
||||
let sizes = edit(&mut warm, &grown, &mut rng);
|
||||
warm.frame();
|
||||
|
||||
let mut cold = Harness::new((640, 900));
|
||||
let same = plant(
|
||||
&mut cold,
|
||||
seed,
|
||||
&Edits {
|
||||
sizes,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
let what = "a resize then a size change";
|
||||
assert_same(seed, what, (&warm, &grown), (&cold, &same));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_changed_size_lands_where_growing_it_that_way_would() {
|
||||
SEEDS.into_iter().for_each(changed_size);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_changed_alignment_lands_where_growing_it_that_way_would() {
|
||||
SEEDS.into_iter().for_each(changed_alignment);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_toggled_region_node_lands_where_growing_it_that_way_would() {
|
||||
SEEDS.into_iter().for_each(changed_region_node);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_size_changing_at_once_lands_where_growing_it_that_way_would() {
|
||||
SEEDS.into_iter().for_each(changed_every_size);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn many_widgets_redrawing_at_once_leaves_every_box_where_it_was() {
|
||||
SEEDS.into_iter().for_each(repainted_together);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_resize_lands_where_starting_at_that_size_would() {
|
||||
SEEDS.into_iter().for_each(resized);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_size_change_after_a_resize_lands_the_same_way() {
|
||||
SEEDS.into_iter().for_each(resized_then_changed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adding_and_removing_span_children_lands_where_growing_it_that_way_would() {
|
||||
for shuffle in SHUFFLES {
|
||||
for case in ALL {
|
||||
if matches!(case, Case::Shuffle(_)) {
|
||||
for seed in SEEDS {
|
||||
reshuffled(seed, shuffle);
|
||||
check(seed, depth(), case);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The same property over a hundred seeds and every scenario. What it has
|
||||
/// found so far was never where the trees disagreed: a text measured in a box
|
||||
/// it was not going to get, and a widget re-measured in a box its own answer
|
||||
/// had decided. `tests/shrink.rs` is how a seed from here becomes a tree
|
||||
/// small enough to read.
|
||||
#[test]
|
||||
#[ignore = "a hundred seeds, rather than the nine the others check"]
|
||||
#[ignore = "as many seeds as it is asked for, rather than the nine the others check"]
|
||||
fn a_long_run_of_seeds_agrees() {
|
||||
let seeds = std::env::var("IRIS_GENERATED_SEED")
|
||||
let depth = depth();
|
||||
let seeds: Vec<u64> = match std::env::var("IRIS_GENERATED_SEED")
|
||||
.ok()
|
||||
.and_then(|seed| seed.parse().ok())
|
||||
.map(|seed| seed..=seed)
|
||||
.unwrap_or_else(|| 1..=env("IRIS_GENERATED_SEEDS", 100));
|
||||
over_seeds(seeds.collect(), |seed| {
|
||||
changed_size(seed);
|
||||
changed_every_size(seed);
|
||||
repainted_together(seed);
|
||||
resized(seed);
|
||||
resized_then_changed(seed);
|
||||
for shuffle in SHUFFLES {
|
||||
reshuffled(seed, shuffle);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Every seed on its own thread's share of them. A tree is grown, laid out
|
||||
/// and dropped inside one call, so seeds share nothing, and this is most of
|
||||
/// the time a run takes. A thread that fails takes the scope down with it,
|
||||
/// which is the same panic libtest would have seen.
|
||||
///
|
||||
/// One core short of all of them, so the machine this runs on stays usable.
|
||||
pub fn over_seeds(seeds: Vec<u64>, run: impl Fn(u64) + Sync) {
|
||||
let threads =
|
||||
std::thread::available_parallelism().map_or(1, |n| n.get().saturating_sub(1).max(1));
|
||||
let chunk = seeds.len().div_ceil(threads).max(1);
|
||||
std::thread::scope(|scope| {
|
||||
for part in seeds.chunks(chunk) {
|
||||
let run = &run;
|
||||
scope.spawn(move || part.iter().for_each(|&seed| run(seed)));
|
||||
.and_then(|v| v.parse().ok())
|
||||
{
|
||||
Some(seed) => vec![seed],
|
||||
None => (1..=env("IRIS_GENERATED_SEEDS", 100_u64)).collect(),
|
||||
};
|
||||
over_seeds(seeds, |seed| {
|
||||
for case in ALL {
|
||||
check(seed, depth, case);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -92,9 +92,18 @@ fn trace_selected(tree: &Tree) {
|
||||
#[cfg(not(feature = "layout-diagnostics"))]
|
||||
fn trace_selected(_: &Tree) {}
|
||||
|
||||
/// The shape a cost is measured on must not depend on what layout measured,
|
||||
/// or two commits are compared on two different trees. See `Edits`.
|
||||
fn rig_edits() -> Edits {
|
||||
Edits {
|
||||
fixed_branches: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn warm(seed: u64, depth: usize) -> (Harness, Tree) {
|
||||
let mut harness = Harness::new(OUTPUT);
|
||||
let (root, tree) = grow(&mut harness.rsc, seed, depth, &Edits::default());
|
||||
let (root, tree) = grow(&mut harness.rsc, seed, depth, &rig_edits());
|
||||
harness.state.root = Some(root);
|
||||
harness.frame();
|
||||
println!(
|
||||
@@ -173,7 +182,7 @@ fn layout_cost() {
|
||||
|
||||
if selected("cold") {
|
||||
let mut harness = Harness::new(OUTPUT);
|
||||
let (root, tree) = grow(&mut harness.rsc, seed, depth, &Edits::default());
|
||||
let (root, tree) = grow(&mut harness.rsc, seed, depth, &rig_edits());
|
||||
harness.state.root = Some(root);
|
||||
println!(
|
||||
"fixture: seed {seed}, depth {depth}, {} widgets",
|
||||
|
||||
@@ -0,0 +1,467 @@
|
||||
//! The scenarios both fuzzers run, over the tree a [`Plan`] describes.
|
||||
//!
|
||||
//! One implementation rather than two. The oracle grew its trees from a seed
|
||||
//! and the shrinker grew its own, with every scenario written out on each
|
||||
//! side, so a failure the oracle found could not be handed to the shrinker:
|
||||
//! there was no tree to pass it, only a seed, and a seed cannot be made
|
||||
//! smaller. Both take a plan now, so whatever finds a counterexample can also
|
||||
//! reduce it.
|
||||
//!
|
||||
//! Each target compiles this for itself, so what only one of them calls is
|
||||
//! dead code in the other.
|
||||
#![allow(dead_code)]
|
||||
|
||||
use iris::harness::Harness;
|
||||
use iris::prelude::*;
|
||||
use iris::random::{Aligns, Edits, Kind, Lens, Plan, Rng, SpanEdit, Tree, build};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// A seed per thread but one, since a seed grows, lays out and drops its tree
|
||||
/// alone. A failing seed still shrinks and panics on its own thread.
|
||||
pub fn over_seeds(seeds: Vec<u64>, run: impl Fn(u64) + Sync) {
|
||||
let threads =
|
||||
std::thread::available_parallelism().map_or(1, |n| n.get().saturating_sub(1).max(1));
|
||||
let chunk = seeds.len().div_ceil(threads).max(1);
|
||||
std::thread::scope(|scope| {
|
||||
for part in seeds.chunks(chunk) {
|
||||
let run = &run;
|
||||
scope.spawn(move || part.iter().for_each(|&seed| run(seed)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
|
||||
std::env::var(name)
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(fallback)
|
||||
}
|
||||
|
||||
/// The window a tree is grown in, and the one a resize takes it to.
|
||||
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.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Shuffle {
|
||||
/// Every other child, so what is left is interleaved with what went.
|
||||
EveryOther,
|
||||
/// Everything but the first, which is the last step before empty.
|
||||
AllButFirst,
|
||||
/// Three more on the end at once.
|
||||
AddThree,
|
||||
/// The first out and three more on, so the count moves both ways.
|
||||
SwapForThree,
|
||||
/// One out of the middle and one on the end.
|
||||
TradeOne,
|
||||
}
|
||||
|
||||
impl Shuffle {
|
||||
fn of(self, grown: usize) -> SpanEdit {
|
||||
let all = |step: usize, from: usize| (from..grown).step_by(step).collect();
|
||||
match self {
|
||||
Self::EveryOther => SpanEdit {
|
||||
detach: all(2, 0),
|
||||
attach: 0,
|
||||
},
|
||||
Self::AllButFirst => SpanEdit {
|
||||
detach: all(1, 1),
|
||||
attach: 0,
|
||||
},
|
||||
Self::AddThree => SpanEdit {
|
||||
detach: Vec::new(),
|
||||
attach: 3,
|
||||
},
|
||||
Self::SwapForThree => SpanEdit {
|
||||
detach: vec![0],
|
||||
attach: 3,
|
||||
},
|
||||
Self::TradeOne => SpanEdit {
|
||||
detach: vec![grown / 2],
|
||||
attach: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What a warm tree is put through before it is compared with a cold one
|
||||
/// grown the way it was left.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Case {
|
||||
/// Nothing changes, so no box may either. What this exercises is the
|
||||
/// order a frame settles a dirty set in.
|
||||
Repaint,
|
||||
/// Every fifth widget rather than all of them: marking all of them
|
||||
/// redraws the whole tree, which is a cold start reached the long way,
|
||||
/// where the mixed case leaves a redrawn subtree beside a retained one.
|
||||
RepaintSome,
|
||||
Resize,
|
||||
ResizeRepaint,
|
||||
/// A resize and then a size change, so a retained answer is asked to
|
||||
/// survive two different kinds of invalidation in a row.
|
||||
ResizeSize,
|
||||
/// A few declared sizes.
|
||||
Size,
|
||||
/// Every declared size at once, so every reader of a size has a changed
|
||||
/// descendant in the same frame and the whole dirty set settles together.
|
||||
EverySize,
|
||||
Align,
|
||||
/// Giving a widget a movable region of its own, or taking it away, is a
|
||||
/// structural change: every primitive under it changes which chain
|
||||
/// resolves it.
|
||||
RegionNode,
|
||||
/// The same children in a different order, which moves every one of them
|
||||
/// without changing what any of them is.
|
||||
Reorder,
|
||||
Shuffle(Shuffle),
|
||||
}
|
||||
|
||||
pub const ALL: [Case; 15] = [
|
||||
Case::Repaint,
|
||||
Case::RepaintSome,
|
||||
Case::Resize,
|
||||
Case::ResizeRepaint,
|
||||
Case::ResizeSize,
|
||||
Case::Size,
|
||||
Case::EverySize,
|
||||
Case::Align,
|
||||
Case::RegionNode,
|
||||
Case::Reorder,
|
||||
Case::Shuffle(Shuffle::EveryOther),
|
||||
Case::Shuffle(Shuffle::AllButFirst),
|
||||
Case::Shuffle(Shuffle::AddThree),
|
||||
Case::Shuffle(Shuffle::SwapForThree),
|
||||
Case::Shuffle(Shuffle::TradeOne),
|
||||
];
|
||||
|
||||
impl Case {
|
||||
/// The name `CASE` selects it by, and the one a failure prints.
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Repaint => "repaint",
|
||||
Self::RepaintSome => "repaint-some",
|
||||
Self::Resize => "resize",
|
||||
Self::ResizeRepaint => "resize-repaint",
|
||||
Self::ResizeSize => "resize-size",
|
||||
Self::Size => "size",
|
||||
Self::EverySize => "every-size",
|
||||
Self::Align => "align",
|
||||
Self::RegionNode => "region-node",
|
||||
Self::Reorder => "reorder",
|
||||
Self::Shuffle(Shuffle::EveryOther) => "shuffle-every-other",
|
||||
Self::Shuffle(Shuffle::AllButFirst) => "shuffle-all-but-first",
|
||||
Self::Shuffle(Shuffle::AddThree) => "shuffle-add-three",
|
||||
Self::Shuffle(Shuffle::SwapForThree) => "shuffle-swap-for-three",
|
||||
Self::Shuffle(Shuffle::TradeOne) => "shuffle-trade-one",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn named(name: &str) -> Option<Self> {
|
||||
ALL.into_iter().find(|case| case.name() == name)
|
||||
}
|
||||
|
||||
/// Grown in the first, compared in the second.
|
||||
fn window(self) -> ((f32, f32), (f32, f32)) {
|
||||
match self {
|
||||
Self::Resize | Self::ResizeRepaint | Self::ResizeSize => (OUTER, INNER),
|
||||
_ => (STILL, STILL),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mark(warm: &mut Harness, tree: &Tree, step: usize) {
|
||||
for &id in tree.ids.iter().step_by(step) {
|
||||
warm.rsc.widgets_mut().get_dyn_mut(id);
|
||||
}
|
||||
}
|
||||
|
||||
fn a_len(rng: &mut Rng) -> Option<LayoutLen> {
|
||||
Some(LayoutLen::px(20.0 + rng.below(180) as f32))
|
||||
}
|
||||
|
||||
fn resize_one(warm: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Lens {
|
||||
let lens = [a_len(rng), a_len(rng)];
|
||||
warm.rsc
|
||||
.widgets_mut()
|
||||
.set_size_rules(tree.sized[idx], lens[0], lens[1]);
|
||||
lens
|
||||
}
|
||||
|
||||
fn realign_one(warm: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Aligns {
|
||||
let side = |rng: &mut Rng| match rng.below(4) {
|
||||
0 => None,
|
||||
1 => Some(AxisAlign::NEG),
|
||||
2 => Some(AxisAlign::CENTER),
|
||||
_ => Some(AxisAlign::POS),
|
||||
};
|
||||
let align = [side(rng), side(rng)];
|
||||
let id = tree.aligned[idx];
|
||||
for (axis, align) in [Axis::X, Axis::Y].into_iter().zip(align) {
|
||||
warm.rsc
|
||||
.widgets_mut()
|
||||
.set_alignment(id, axis, align.unwrap_or_default());
|
||||
}
|
||||
align
|
||||
}
|
||||
|
||||
/// Every span's children in a different order, said both to the warm tree and
|
||||
/// to the plan the cold one is grown from.
|
||||
fn reorder(warm: &mut Harness, tree: &Tree, plan: &Plan) -> Plan {
|
||||
for span in &tree.spans {
|
||||
let children = &mut warm.rsc[span.id].children;
|
||||
if !children.is_empty() {
|
||||
children.rotate_left(1);
|
||||
}
|
||||
}
|
||||
let mut out = plan.clone();
|
||||
out.walk_mut(&mut |node| {
|
||||
if let Kind::Span { order, .. } = &mut node.kind
|
||||
&& !order.is_empty()
|
||||
{
|
||||
order.rotate_left(1);
|
||||
}
|
||||
});
|
||||
out
|
||||
}
|
||||
|
||||
/// Applies `shuffle` to every third span. What it takes out is given back to
|
||||
/// the span's spares: the last share of a widget must outlive the comparison,
|
||||
/// or its id is handed to something else and the two trees stop lining up.
|
||||
fn reshuffle(warm: &mut Harness, tree: &mut Tree, shuffle: Shuffle) -> HashMap<usize, SpanEdit> {
|
||||
let mut edits = HashMap::new();
|
||||
for (idx, span) in tree.spans.iter_mut().enumerate().step_by(3) {
|
||||
let edit = shuffle.of(span.grown);
|
||||
let mut take = edit.detach.clone();
|
||||
take.sort_unstable();
|
||||
let children = &mut warm.rsc[span.id].children;
|
||||
// Highest first, so an index means the same child however many of its
|
||||
// neighbours are going too.
|
||||
for j in take.into_iter().rev() {
|
||||
if j < children.len() {
|
||||
span.spares.push(children.remove(j));
|
||||
}
|
||||
}
|
||||
let attach = edit.attach.min(span.spares.len());
|
||||
let moved: Vec<_> = span.spares.drain(..attach).collect();
|
||||
warm.rsc[span.id].children.extend(moved);
|
||||
edits.insert(idx, edit);
|
||||
}
|
||||
edits
|
||||
}
|
||||
|
||||
/// Changes the warm tree and answers with the plan a cold tree grown that way
|
||||
/// comes from. Each arm settles its own frame, so a case that changes nothing
|
||||
/// does not get a second one that could settle what the first left.
|
||||
fn change(case: Case, warm: &mut Harness, tree: &mut Tree, plan: &Plan, rng: &mut Rng) -> Plan {
|
||||
let some_sizes = |warm: &mut Harness, tree: &Tree, rng: &mut Rng| {
|
||||
let mut sizes = HashMap::new();
|
||||
for _ in 0..4 {
|
||||
if tree.sized.is_empty() {
|
||||
break;
|
||||
}
|
||||
let idx = rng.below(tree.sized.len());
|
||||
sizes.insert(idx, resize_one(warm, tree, idx, rng));
|
||||
}
|
||||
sizes
|
||||
};
|
||||
let edits = match case {
|
||||
Case::Resize => return plan.clone(),
|
||||
Case::Repaint | Case::ResizeRepaint => {
|
||||
mark(warm, tree, 1);
|
||||
warm.frame();
|
||||
return plan.clone();
|
||||
}
|
||||
Case::RepaintSome => {
|
||||
mark(warm, tree, 5);
|
||||
warm.frame();
|
||||
return plan.clone();
|
||||
}
|
||||
Case::Reorder => {
|
||||
let out = reorder(warm, tree, plan);
|
||||
warm.frame();
|
||||
return out;
|
||||
}
|
||||
Case::Size | Case::ResizeSize => Edits {
|
||||
sizes: some_sizes(warm, tree, rng),
|
||||
..Default::default()
|
||||
},
|
||||
Case::EverySize => Edits {
|
||||
sizes: (0..tree.sized.len())
|
||||
.map(|idx| (idx, resize_one(warm, tree, idx, rng)))
|
||||
.collect(),
|
||||
..Default::default()
|
||||
},
|
||||
Case::Align => Edits {
|
||||
aligns: (0..tree.aligned.len())
|
||||
.step_by(3)
|
||||
.map(|idx| (idx, realign_one(warm, tree, idx, rng)))
|
||||
.collect(),
|
||||
..Default::default()
|
||||
},
|
||||
Case::RegionNode => {
|
||||
let mut nodes = HashMap::new();
|
||||
for idx in (0..tree.nodes.len()).step_by(2) {
|
||||
let id = tree.nodes[idx];
|
||||
let take = !warm.rsc.widgets().is_region_node(id);
|
||||
warm.rsc.widgets_mut().set_region_node(id, take);
|
||||
nodes.insert(idx, take);
|
||||
}
|
||||
Edits {
|
||||
nodes,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
Case::Shuffle(shuffle) => Edits {
|
||||
spans: reshuffle(warm, tree, shuffle),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
warm.frame();
|
||||
plan.edited(&edits)
|
||||
}
|
||||
|
||||
/// What a widget was configured with, so a tree a fuzzer found can be written
|
||||
/// out by hand. A failure is a lead; the fast test that replaces it has to be
|
||||
/// buildable from what the failure printed.
|
||||
fn describe(id: WidgetId, h: &Harness) -> String {
|
||||
let rules = h.rsc.widgets().size_rules(id);
|
||||
let rule = |r: SizeRule| match r.exact() {
|
||||
Some(len) => format!("{len}"),
|
||||
None => "-".into(),
|
||||
};
|
||||
let align = h.rsc.widgets().alignment(id);
|
||||
let side = |a: AxisAlign| {
|
||||
if a == AxisAlign::NEG {
|
||||
"neg".into()
|
||||
} else if a == AxisAlign::CENTER {
|
||||
"mid".into()
|
||||
} else if a == AxisAlign::POS {
|
||||
"pos".into()
|
||||
} else {
|
||||
format!("{:.2}", a.rel())
|
||||
}
|
||||
};
|
||||
// A rule and an alignment are properties of whatever carries them, so
|
||||
// they print with that widget rather than as widgets of their own.
|
||||
let mut out = describe_widget(id, h);
|
||||
if (rules.x, rules.y) != (SizeRule::Free, SizeRule::Free) {
|
||||
out += &format!("[x:{},y:{}]", rule(rules.x), rule(rules.y));
|
||||
}
|
||||
if align != RegionAlign::default() {
|
||||
out += &format!("@{},{}", side(align.x), side(align.y));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn describe_widget(id: WidgetId, h: &Harness) -> String {
|
||||
let label = h.rsc.widgets().label(id).to_string();
|
||||
let Some(widget) = h.rsc.widgets().get_dyn(id) else {
|
||||
return label;
|
||||
};
|
||||
let any: &dyn std::any::Any = widget;
|
||||
if let Some(w) = any.downcast_ref::<Span>() {
|
||||
let sign = if w.dir.sign == Sign::Neg { "-" } else { "+" };
|
||||
return format!(
|
||||
"Span{{dir:{:?}{sign},gap:{},n:{}}}",
|
||||
w.dir.axis,
|
||||
w.gap,
|
||||
w.children.len()
|
||||
);
|
||||
}
|
||||
if let Some(w) = any.downcast_ref::<Pad>() {
|
||||
let p = &w.padding;
|
||||
return format!(
|
||||
"Pad{{l:{},r:{},t:{},b:{}}}",
|
||||
p.left, p.right, p.top, p.bottom
|
||||
);
|
||||
}
|
||||
if let Some(w) = any.downcast_ref::<Stack>() {
|
||||
return format!("Stack{{n:{}}}", w.children.len());
|
||||
}
|
||||
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.
|
||||
pub fn diverges(plan: &Plan, case: Case, seed: u64) -> Option<String> {
|
||||
let (start, end) = case.window();
|
||||
let mut warm = Harness::new(start);
|
||||
let (root, mut tree) = build(&mut warm.rsc, plan);
|
||||
warm.state.root = Some(root);
|
||||
// The frame that makes it warm: without it nothing is retained and the
|
||||
// comparison is two cold starts agreeing with each other.
|
||||
warm.frame();
|
||||
if start != end {
|
||||
warm.resize(end);
|
||||
warm.frame();
|
||||
}
|
||||
let cold_plan = change(case, &mut warm, &mut tree, plan, &mut Rng::new(seed));
|
||||
|
||||
let mut cold = Harness::new(end);
|
||||
let (root, cold_tree) = build(&mut cold.rsc, &cold_plan);
|
||||
cold.state.root = Some(root);
|
||||
cold.frame();
|
||||
|
||||
let mut drawn = 0;
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
// Where two trees disagree is rarely where the cause is, so the
|
||||
// ancestry comes with it, marking the widgets that own a region.
|
||||
let mut chain = Vec::new();
|
||||
let mut at = Some(w);
|
||||
while let Some(id) = at {
|
||||
let active = &warm.render.active[&id];
|
||||
let node = match active.move_idx == active.parent_move {
|
||||
true => "",
|
||||
false => "*",
|
||||
};
|
||||
chain.push(format!("{}{node}", describe(id, &warm)));
|
||||
at = active.parent;
|
||||
}
|
||||
return Some(format!(
|
||||
"widget {i}\n warm {got:?}\n cold {want:?}\n {}",
|
||||
chain.join(" < ")
|
||||
));
|
||||
}
|
||||
match drawn {
|
||||
0 => Some("nothing was drawn".into()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
+61
-581
@@ -1,556 +1,38 @@
|
||||
//! A property test that shrinks its own counterexample.
|
||||
//! A fuzzer that reduces its own counterexample.
|
||||
//!
|
||||
//! `generated.rs` reproduces a failure from a seed, but a seed is not a lead
|
||||
//! anybody can read: the tree is hundreds of widgets, and reconstructing the
|
||||
//! part that matters by hand has failed every time it has been tried. This
|
||||
//! grows trees it can take apart, so a failure is reduced to the smallest
|
||||
//! tree that still shows it and printed as something to write a fast test
|
||||
//! from.
|
||||
//! A seed is not a lead anybody can read: the tree is hundreds of widgets,
|
||||
//! and reconstructing the part that matters by hand has failed every time it
|
||||
//! has been tried. This grows the trees `iris::random` describes, takes them
|
||||
//! apart, and prints the smallest one that still fails as something to write
|
||||
//! a fast test from.
|
||||
//!
|
||||
//! cargo test --release --test shrink -- --ignored --nocapture
|
||||
//!
|
||||
//! `SHRINK_SEEDS` how many trees to try, `SHRINK_DEPTH` how deep to grow
|
||||
//! them, `SHRINK_CASE` which scenario. It is a fuzzer: run it once the
|
||||
//! ordinary tests pass, and turn what it finds into a test of its own rather
|
||||
//! than leaving a seed as the record.
|
||||
//! them, `SHRINK_CASE` which scenario or `all` for every one. `SHRINK_SEED`
|
||||
//! takes a single seed, which is how a failure `generated` printed is handed
|
||||
//! straight here: the two run the same cases over the same trees, so a seed
|
||||
//! that fails there fails here and is reduced.
|
||||
//!
|
||||
//! It is a fuzzer: run it once the ordinary tests pass, and turn what it
|
||||
//! finds into a test of its own rather than leaving a seed as the record.
|
||||
|
||||
use iris::harness::Harness;
|
||||
use iris::prelude::*;
|
||||
use iris::random::{Branch, Rng};
|
||||
#[path = "scenario/mod.rs"]
|
||||
mod scenario;
|
||||
|
||||
/// The same two leaves `iris::random` grows, since only one of them reads the
|
||||
/// width it is given and that is the difference that matters.
|
||||
const WORDS: &[&str] = &[
|
||||
"Wrapping",
|
||||
"shapes",
|
||||
"one",
|
||||
"source",
|
||||
"into",
|
||||
"as",
|
||||
"many",
|
||||
"lines",
|
||||
"as",
|
||||
"the",
|
||||
"box",
|
||||
"leaves",
|
||||
"room",
|
||||
"for,",
|
||||
"so",
|
||||
"a",
|
||||
"paragraph's",
|
||||
"height",
|
||||
"is",
|
||||
"an",
|
||||
"answer",
|
||||
"and",
|
||||
"not",
|
||||
"a",
|
||||
"setting.",
|
||||
];
|
||||
use iris::random::{Edits, Plan, plan};
|
||||
use scenario::{ALL, Case, diverges, env, over_seeds};
|
||||
|
||||
const ONE_LINE: &str = "one line, overflowing whatever it is given";
|
||||
|
||||
const OUTER: (f32, f32) = (1920.0, 1200.0);
|
||||
/// Steps of the grid two ways of reaching a box may differ by: one per level
|
||||
/// of nesting between them, and these trees are five deep. See
|
||||
/// `docs/HANDOFF.md`'s "Fixed point" in `ai-app-2` for what is left.
|
||||
const AGREE_STEPS: i32 = 2;
|
||||
const INNER: (f32, f32) = (640.0, 900.0);
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
enum Node {
|
||||
/// Words taken from [`WORDS`], and whether it wraps.
|
||||
Text(usize, bool),
|
||||
/// The leaf that overflows whatever box it is given rather than wrapping.
|
||||
OneLine,
|
||||
Rect,
|
||||
/// Direction, gap, children in creation order, and the order they are
|
||||
/// attached in -- separate so a tree that reorders its children
|
||||
/// still makes the same widgets in the same order, and two
|
||||
/// builds line up index for index.
|
||||
Span(bool, f32, Vec<Node>, Vec<usize>),
|
||||
Stack(Vec<Node>),
|
||||
Pad(f32, Box<Node>),
|
||||
Aligned(u8, u8, Box<Node>),
|
||||
Sized(Option<LayoutLen>, Option<LayoutLen>, Box<Node>),
|
||||
Scroll(bool, Box<Node>),
|
||||
Branch(Box<Node>, Box<Node>, Box<Node>, f32),
|
||||
}
|
||||
|
||||
fn axis_align(v: u8) -> Option<AxisAlign> {
|
||||
match v % 4 {
|
||||
0 => None,
|
||||
1 => Some(AxisAlign::NEG),
|
||||
2 => Some(AxisAlign::CENTER),
|
||||
_ => Some(AxisAlign::POS),
|
||||
}
|
||||
}
|
||||
|
||||
fn dir(down: bool) -> Dir {
|
||||
if down { Dir::DOWN } else { Dir::RIGHT }
|
||||
}
|
||||
|
||||
impl Node {
|
||||
/// Builds into `h`, pushing every id in tree order, so two builds of one
|
||||
/// node line up index for index and their boxes can be compared.
|
||||
fn build(
|
||||
&self,
|
||||
h: &mut Harness,
|
||||
out: &mut Vec<WidgetId>,
|
||||
spans: &mut Vec<WeakWidget<Span>>,
|
||||
sized: &mut Vec<WidgetId>,
|
||||
) -> StrongWidget {
|
||||
let id: StrongWidget = match self {
|
||||
Node::Text(words, wrap) => {
|
||||
let n = (*words).clamp(1, WORDS.len());
|
||||
wtext(WORDS[..n].join(" "))
|
||||
.size(16)
|
||||
.wrap(*wrap)
|
||||
.add_strong(&mut h.rsc)
|
||||
}
|
||||
Node::OneLine => wtext(ONE_LINE).size(16).wrap(false).add_strong(&mut h.rsc),
|
||||
Node::Rect => rect(Color::RED).add_strong(&mut h.rsc),
|
||||
Node::Span(down, gap, kids, order) => {
|
||||
let mut built: Vec<_> = kids
|
||||
.iter()
|
||||
.map(|k| Some(k.build(h, out, spans, sized)))
|
||||
.collect();
|
||||
// `order` is a permutation, so each is taken exactly once.
|
||||
let children = order
|
||||
.iter()
|
||||
.map(|&i| built[i].take().expect("order repeats an index"))
|
||||
.collect();
|
||||
let handle = Span {
|
||||
children,
|
||||
dir: dir(*down),
|
||||
gap: Px::from_f32(*gap),
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
// A row takes the height it is given; a column is as wide
|
||||
// as its widest child, which needs no rule.
|
||||
if !*down {
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_size_rules(handle, None, Some(LayoutLen::rel(1.0)));
|
||||
}
|
||||
spans.push(handle);
|
||||
handle.add_strong(&mut h.rsc)
|
||||
}
|
||||
Node::Stack(kids) => {
|
||||
let children = kids.iter().map(|k| k.build(h, out, spans, sized)).collect();
|
||||
Stack {
|
||||
children,
|
||||
size: StackSize::Child(0),
|
||||
}
|
||||
.add_strong(&mut h.rsc)
|
||||
}
|
||||
Node::Pad(p, kid) => {
|
||||
let inner = kid.build(h, out, spans, sized);
|
||||
Pad {
|
||||
padding: Padding::uniform(*p),
|
||||
inner,
|
||||
}
|
||||
.add_strong(&mut h.rsc)
|
||||
}
|
||||
Node::Aligned(x, y, kid) => {
|
||||
let inner = kid.build(h, out, spans, sized);
|
||||
for (axis, align) in [(Axis::X, axis_align(*x)), (Axis::Y, axis_align(*y))] {
|
||||
if let Some(align) = align {
|
||||
h.rsc.widgets_mut().set_alignment(&inner, axis, align);
|
||||
}
|
||||
}
|
||||
inner
|
||||
}
|
||||
Node::Sized(x, y, kid) => {
|
||||
let inner = kid.build(h, out, spans, sized);
|
||||
h.rsc.widgets_mut().set_size_rules(&inner, *x, *y);
|
||||
sized.push(inner.id());
|
||||
inner
|
||||
}
|
||||
Node::Scroll(down, kid) => {
|
||||
let inner = kid.build(h, out, spans, sized);
|
||||
let axis = if *down { Axis::Y } else { Axis::X };
|
||||
Scroll::new(inner, axis).add_strong(&mut h.rsc)
|
||||
}
|
||||
Node::Branch(probe, a, b, at) => {
|
||||
let probe = probe.build(h, out, spans, sized);
|
||||
let wide = a.build(h, out, spans, sized);
|
||||
let narrow = b.build(h, out, spans, sized);
|
||||
Branch {
|
||||
probe,
|
||||
wide,
|
||||
narrow,
|
||||
threshold: *at,
|
||||
}
|
||||
.add_strong(&mut h.rsc)
|
||||
}
|
||||
};
|
||||
out.push(id.id());
|
||||
id
|
||||
}
|
||||
|
||||
/// The lengths every `Sized` node would carry after `resized`, in the
|
||||
/// order `build` pushes them.
|
||||
fn sized_lens(&self, out: &mut Vec<(Option<LayoutLen>, Option<LayoutLen>)>) {
|
||||
match self {
|
||||
Node::Text(..) | Node::OneLine | Node::Rect => {}
|
||||
Node::Span(_, _, kids, _) | Node::Stack(kids) => {
|
||||
kids.iter().for_each(|k| k.sized_lens(out));
|
||||
}
|
||||
Node::Pad(_, k) | Node::Aligned(_, _, k) | Node::Scroll(_, k) => k.sized_lens(out),
|
||||
Node::Sized(x, y, k) => {
|
||||
k.sized_lens(out);
|
||||
out.push((resized_len(*x), resized_len(*y)));
|
||||
}
|
||||
Node::Branch(p, a, b, _) => {
|
||||
p.sized_lens(out);
|
||||
a.sized_lens(out);
|
||||
b.sized_lens(out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn size(&self) -> usize {
|
||||
1 + match self {
|
||||
Node::Text(..) | Node::OneLine | Node::Rect => 0,
|
||||
Node::Span(_, _, kids, _) | Node::Stack(kids) => kids.iter().map(Node::size).sum(),
|
||||
Node::Pad(_, k)
|
||||
| Node::Aligned(_, _, k)
|
||||
| Node::Sized(_, _, k)
|
||||
| Node::Scroll(_, k) => k.size(),
|
||||
Node::Branch(p, a, b, _) => p.size() + a.size() + b.size(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Every one-step simplification: a wrapper replaced by what it wrapped, a
|
||||
/// child dropped, a length or a word count reduced. Ordered cheapest-first
|
||||
/// so the greedy walk takes the biggest bites early.
|
||||
fn smaller(&self) -> Vec<Node> {
|
||||
let mut out = Vec::new();
|
||||
let leaf = Node::Rect;
|
||||
match self {
|
||||
Node::Text(words, wrap) => {
|
||||
if *words > 1 {
|
||||
out.push(Node::Text(words / 2, *wrap));
|
||||
out.push(Node::Text(words - 1, *wrap));
|
||||
}
|
||||
if *wrap {
|
||||
out.push(Node::Text(*words, false));
|
||||
}
|
||||
out.push(leaf);
|
||||
}
|
||||
Node::OneLine => out.push(Node::Rect),
|
||||
Node::Rect => {}
|
||||
Node::Span(down, gap, kids, order) => {
|
||||
out.extend(order.iter().map(|&i| kids[i].clone()));
|
||||
for i in 0..kids.len() {
|
||||
if kids.len() > 1 {
|
||||
let mut less = kids.clone();
|
||||
less.remove(i);
|
||||
let order = (0..less.len()).collect();
|
||||
out.push(Node::Span(*down, *gap, less, order));
|
||||
}
|
||||
}
|
||||
if *gap != 0.0 {
|
||||
out.push(Node::Span(*down, 0.0, kids.clone(), order.clone()));
|
||||
}
|
||||
for (i, kid) in kids.iter().enumerate() {
|
||||
for small in kid.smaller() {
|
||||
let mut next = kids.clone();
|
||||
next[i] = small;
|
||||
out.push(Node::Span(*down, *gap, next, order.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
Node::Stack(kids) => {
|
||||
out.extend(kids.iter().cloned());
|
||||
for i in 0..kids.len() {
|
||||
if kids.len() > 1 {
|
||||
let mut less = kids.clone();
|
||||
less.remove(i);
|
||||
out.push(Node::Stack(less));
|
||||
}
|
||||
}
|
||||
for (i, kid) in kids.iter().enumerate() {
|
||||
for small in kid.smaller() {
|
||||
let mut next = kids.clone();
|
||||
next[i] = small;
|
||||
out.push(Node::Stack(next));
|
||||
}
|
||||
}
|
||||
}
|
||||
Node::Pad(p, kid) => {
|
||||
out.push((**kid).clone());
|
||||
if *p != 0.0 {
|
||||
out.push(Node::Pad(0.0, kid.clone()));
|
||||
}
|
||||
out.extend(
|
||||
kid.smaller()
|
||||
.into_iter()
|
||||
.map(|k| Node::Pad(*p, Box::new(k))),
|
||||
);
|
||||
}
|
||||
Node::Aligned(x, y, kid) => {
|
||||
out.push((**kid).clone());
|
||||
for (nx, ny) in [(0, *y), (*x, 0)] {
|
||||
if (nx, ny) != (*x, *y) {
|
||||
out.push(Node::Aligned(nx, ny, kid.clone()));
|
||||
}
|
||||
}
|
||||
out.extend(
|
||||
kid.smaller()
|
||||
.into_iter()
|
||||
.map(|k| Node::Aligned(*x, *y, Box::new(k))),
|
||||
);
|
||||
}
|
||||
Node::Sized(x, y, kid) => {
|
||||
out.push((**kid).clone());
|
||||
if x.is_some() {
|
||||
out.push(Node::Sized(None, *y, kid.clone()));
|
||||
}
|
||||
if y.is_some() {
|
||||
out.push(Node::Sized(*x, None, kid.clone()));
|
||||
}
|
||||
out.extend(
|
||||
kid.smaller()
|
||||
.into_iter()
|
||||
.map(|k| Node::Sized(*x, *y, Box::new(k))),
|
||||
);
|
||||
}
|
||||
Node::Scroll(down, kid) => {
|
||||
out.push((**kid).clone());
|
||||
out.extend(
|
||||
kid.smaller()
|
||||
.into_iter()
|
||||
.map(|k| Node::Scroll(*down, Box::new(k))),
|
||||
);
|
||||
}
|
||||
Node::Branch(p, a, b, at) => {
|
||||
out.push((**p).clone());
|
||||
out.push((**a).clone());
|
||||
out.push((**b).clone());
|
||||
for small in p.smaller() {
|
||||
out.push(Node::Branch(Box::new(small), a.clone(), b.clone(), *at));
|
||||
}
|
||||
for small in a.smaller() {
|
||||
out.push(Node::Branch(p.clone(), Box::new(small), b.clone(), *at));
|
||||
}
|
||||
for small in b.smaller() {
|
||||
out.push(Node::Branch(p.clone(), a.clone(), Box::new(small), *at));
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// A declared size over about half the tree, the way `iris::random` puts them
|
||||
/// in: on the way into every child rather than as a node kind of its own, so
|
||||
/// readers of a size are dense rather than occasional.
|
||||
fn sized(rng: &mut Rng, inner: Node) -> Node {
|
||||
if !rng.chance() {
|
||||
return inner;
|
||||
}
|
||||
let len = |rng: &mut Rng| match rng.below(4) {
|
||||
0 => Some(LayoutLen::px(20.0 + rng.below(180) as f32)),
|
||||
1 => Some(LayoutLen::LEFTOVER),
|
||||
_ => None,
|
||||
};
|
||||
Node::Sized(len(rng), len(rng), Box::new(inner))
|
||||
}
|
||||
|
||||
fn grow(rng: &mut Rng, depth: usize) -> Node {
|
||||
if depth == 0 {
|
||||
return match rng.below(4) {
|
||||
0 => Node::Text(1 + rng.below(WORDS.len()), true),
|
||||
1 => Node::OneLine,
|
||||
_ => Node::Rect,
|
||||
};
|
||||
}
|
||||
let len = |rng: &mut Rng| match rng.below(4) {
|
||||
0 => Some(LayoutLen::px(20.0 + rng.below(180) as f32)),
|
||||
1 => Some(LayoutLen::LEFTOVER),
|
||||
2 => Some(LayoutLen::rel(0.25 + rng.below(3) as f32 * 0.25)),
|
||||
_ => None,
|
||||
};
|
||||
let kid = |rng: &mut Rng| {
|
||||
let inner = grow(rng, depth - 1);
|
||||
sized(rng, inner)
|
||||
};
|
||||
match rng.below(8) {
|
||||
0 => Node::Scroll(rng.chance(), Box::new(kid(rng))),
|
||||
1 => Node::Aligned(rng.below(4) as u8, rng.below(4) as u8, Box::new(kid(rng))),
|
||||
2 => Node::Pad(rng.below(24) as f32, Box::new(kid(rng))),
|
||||
3 => Node::Sized(len(rng), len(rng), Box::new(kid(rng))),
|
||||
4 => Node::Branch(
|
||||
Box::new(kid(rng)),
|
||||
Box::new(kid(rng)),
|
||||
Box::new(kid(rng)),
|
||||
rng.below(500) as f32,
|
||||
),
|
||||
5 => Node::Stack((0..2 + rng.below(2)).map(|_| kid(rng)).collect()),
|
||||
_ => {
|
||||
let kids: Vec<_> = (0..2 + rng.below(3)).map(|_| kid(rng)).collect();
|
||||
let order = (0..kids.len()).collect();
|
||||
Node::Span(rng.chance(), rng.below(3) as f32 * 4.0, kids, order)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Case {
|
||||
Resize,
|
||||
Repaint,
|
||||
ResizeRepaint,
|
||||
Reorder,
|
||||
SizeChange,
|
||||
}
|
||||
|
||||
/// A different declared length, kept the same kind so the change is to the
|
||||
/// value alone.
|
||||
fn resized_len(len: Option<LayoutLen>) -> Option<LayoutLen> {
|
||||
let half = Rel::from_f32(0.5);
|
||||
len.map(|len| LayoutLen {
|
||||
px: len.px.mul(half) + Px::from_int(13),
|
||||
rel: len.rel.mul(half),
|
||||
leftover: len.leftover,
|
||||
})
|
||||
}
|
||||
|
||||
/// Every declared size changed, as a tree rather than as a change.
|
||||
fn resized(node: &Node) -> Node {
|
||||
match node {
|
||||
Node::Span(down, gap, kids, order) => Node::Span(
|
||||
*down,
|
||||
*gap,
|
||||
kids.iter().map(resized).collect(),
|
||||
order.clone(),
|
||||
),
|
||||
Node::Stack(kids) => Node::Stack(kids.iter().map(resized).collect()),
|
||||
Node::Pad(p, k) => Node::Pad(*p, Box::new(resized(k))),
|
||||
Node::Aligned(x, y, k) => Node::Aligned(*x, *y, Box::new(resized(k))),
|
||||
Node::Sized(x, y, k) => Node::Sized(resized_len(*x), resized_len(*y), Box::new(resized(k))),
|
||||
Node::Scroll(d, k) => Node::Scroll(*d, Box::new(resized(k))),
|
||||
Node::Branch(p, a, b, at) => Node::Branch(
|
||||
Box::new(resized(p)),
|
||||
Box::new(resized(a)),
|
||||
Box::new(resized(b)),
|
||||
*at,
|
||||
),
|
||||
leaf => leaf.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Every span's children rotated by one, as a tree rather than as a change:
|
||||
/// what a warm frame reaches by moving them has to be where growing them that
|
||||
/// way lands.
|
||||
fn reordered(node: &Node) -> Node {
|
||||
match node {
|
||||
Node::Span(down, gap, kids, order) => {
|
||||
let kids = kids.iter().map(reordered).collect::<Vec<_>>();
|
||||
let mut order = order.clone();
|
||||
order.rotate_left(1);
|
||||
Node::Span(*down, *gap, kids, order)
|
||||
}
|
||||
Node::Stack(kids) => Node::Stack(kids.iter().map(reordered).collect()),
|
||||
Node::Pad(p, k) => Node::Pad(*p, Box::new(reordered(k))),
|
||||
Node::Aligned(x, y, k) => Node::Aligned(*x, *y, Box::new(reordered(k))),
|
||||
Node::Sized(x, y, k) => Node::Sized(*x, *y, Box::new(reordered(k))),
|
||||
Node::Scroll(d, k) => Node::Scroll(*d, Box::new(reordered(k))),
|
||||
Node::Branch(p, a, b, at) => Node::Branch(
|
||||
Box::new(reordered(p)),
|
||||
Box::new(reordered(a)),
|
||||
Box::new(reordered(b)),
|
||||
*at,
|
||||
),
|
||||
leaf => leaf.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs one scenario warm and cold and says where they disagree.
|
||||
fn diverges(node: &Node, case: Case) -> Option<String> {
|
||||
let resizes = matches!(case, Case::Resize | Case::ResizeRepaint);
|
||||
let repaints = matches!(case, Case::Repaint | Case::ResizeRepaint);
|
||||
let start = if resizes { OUTER } else { INNER };
|
||||
let mut warm = Harness::new(start);
|
||||
let mut warm_ids = Vec::new();
|
||||
let mut warm_spans = Vec::new();
|
||||
let mut warm_sized = Vec::new();
|
||||
let root = node.build(&mut warm, &mut warm_ids, &mut warm_spans, &mut warm_sized);
|
||||
warm.state.root = Some(root);
|
||||
// The frame that makes it warm: without it there is nothing retained and
|
||||
// the comparison is two cold starts agreeing with each other.
|
||||
warm.frame();
|
||||
if resizes {
|
||||
warm.resize(INNER);
|
||||
warm.frame();
|
||||
}
|
||||
if repaints {
|
||||
for &id in &warm_ids {
|
||||
warm.rsc.widgets_mut().get_dyn_mut(id);
|
||||
}
|
||||
warm.frame();
|
||||
}
|
||||
if case == Case::Reorder {
|
||||
for span in &warm_spans {
|
||||
warm.rsc[*span].children.rotate_left(1);
|
||||
}
|
||||
warm.frame();
|
||||
}
|
||||
if case == Case::SizeChange {
|
||||
let mut lens = Vec::new();
|
||||
node.sized_lens(&mut lens);
|
||||
for (id, (x, y)) in warm_sized.iter().zip(lens) {
|
||||
warm.rsc.widgets_mut().set_size_rules(*id, x, y);
|
||||
}
|
||||
warm.frame();
|
||||
}
|
||||
|
||||
// What the warm tree was moved into, grown that way from the start.
|
||||
let want = match case {
|
||||
Case::Reorder => reordered(node),
|
||||
Case::SizeChange => resized(node),
|
||||
_ => node.clone(),
|
||||
};
|
||||
let mut cold = Harness::new(INNER);
|
||||
let mut cold_ids = Vec::new();
|
||||
let mut cold_spans = Vec::new();
|
||||
let mut cold_sized = Vec::new();
|
||||
let root = want.build(&mut cold, &mut cold_ids, &mut cold_spans, &mut cold_sized);
|
||||
cold.state.root = Some(root);
|
||||
cold.frame();
|
||||
|
||||
for (i, (&w, &c)) in warm_ids.iter().zip(&cold_ids).enumerate() {
|
||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||
// To a couple of steps of the grid, each a thousandth of a pixel: a
|
||||
// move or a resize lands on the same number now, and a length
|
||||
// measured one way against the same length composed another can
|
||||
// still be a step out per composition between them.
|
||||
let same = match (got, want) {
|
||||
(Some(g), Some(c)) => {
|
||||
let d = |a: Px, b: Px| (a - b).abs() <= Px::STEP.mul_int(AGREE_STEPS);
|
||||
d(g.top_left.x, c.top_left.x)
|
||||
&& d(g.top_left.y, c.top_left.y)
|
||||
&& d(g.bot_right.x, c.bot_right.x)
|
||||
&& d(g.bot_right.y, c.bot_right.y)
|
||||
}
|
||||
(None, None) => true,
|
||||
_ => false,
|
||||
};
|
||||
if !same {
|
||||
return Some(format!("widget {i}: warm {got:?} cold {want:?}"));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Takes the first simplification that still fails, until none does.
|
||||
fn shrink(mut node: Node, case: Case) -> Node {
|
||||
/// Takes the first simplification that still fails, until none does. The
|
||||
/// simplifications come biggest first, so this walks down rather than
|
||||
/// nibbling: a six-hundred-widget tree reaches single figures in a few
|
||||
/// hundred builds.
|
||||
fn shrink(mut node: Plan, case: Case, seed: u64) -> Plan {
|
||||
loop {
|
||||
let Some(next) = node
|
||||
.smaller()
|
||||
.into_iter()
|
||||
.find(|small| diverges(small, case).is_some())
|
||||
.find(|small| diverges(small, case, seed).is_some())
|
||||
else {
|
||||
return node;
|
||||
};
|
||||
@@ -558,62 +40,60 @@ fn shrink(mut node: Node, case: Case) -> Node {
|
||||
}
|
||||
}
|
||||
|
||||
/// One thread per core but one, each taking a share of the seeds: a tree is
|
||||
/// grown, laid out and dropped within a seed, so nothing is shared. A seed
|
||||
/// that fails shrinks on its own thread and panics there, which brings the
|
||||
/// scope down with it.
|
||||
fn over_seeds(seeds: Vec<u64>, run: impl Fn(u64) + Sync) {
|
||||
let threads =
|
||||
std::thread::available_parallelism().map_or(1, |n| n.get().saturating_sub(1).max(1));
|
||||
let chunk = seeds.len().div_ceil(threads).max(1);
|
||||
std::thread::scope(|scope| {
|
||||
for part in seeds.chunks(chunk) {
|
||||
let run = &run;
|
||||
scope.spawn(move || part.iter().for_each(|&seed| run(seed)));
|
||||
fn cases() -> Vec<Case> {
|
||||
match env("SHRINK_CASE", String::from("all")).as_str() {
|
||||
"all" => ALL.to_vec(),
|
||||
name => match Case::named(name) {
|
||||
Some(case) => vec![case],
|
||||
None => panic!(
|
||||
"unknown SHRINK_CASE {name:?}; one of all, {}",
|
||||
ALL.map(Case::name).join(", ")
|
||||
),
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
|
||||
std::env::var(name)
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(fallback)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "a fuzzer; run it once the ordinary tests pass"]
|
||||
fn no_grown_tree_lays_out_differently_warm_than_cold() {
|
||||
let seeds: u64 = env("SHRINK_SEEDS", 400);
|
||||
let depth: usize = env("SHRINK_DEPTH", 5);
|
||||
let case = match env("SHRINK_CASE", String::from("resize")).as_str() {
|
||||
"repaint" => Case::Repaint,
|
||||
"resize-repaint" => Case::ResizeRepaint,
|
||||
"reorder" => Case::Reorder,
|
||||
"size-change" => Case::SizeChange,
|
||||
_ => Case::Resize,
|
||||
let cases = cases();
|
||||
let seeds: Vec<u64> = match std::env::var("SHRINK_SEED")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
{
|
||||
Some(seed) => vec![seed],
|
||||
None => (1..=env("SHRINK_SEEDS", 400_u64)).collect(),
|
||||
};
|
||||
let count = seeds.len();
|
||||
|
||||
over_seeds((1..=seeds).collect(), |seed| {
|
||||
let node = grow(&mut Rng::new(seed), depth);
|
||||
let Some(how) = diverges(&node, case) else {
|
||||
return;
|
||||
over_seeds(seeds, |seed| {
|
||||
let grown = plan(seed, depth, &Edits::default());
|
||||
for &case in &cases {
|
||||
let Some(how) = diverges(&grown, case, seed) else {
|
||||
continue;
|
||||
};
|
||||
let small = shrink(node.clone(), case);
|
||||
let small = shrink(grown.clone(), case, seed);
|
||||
println!(
|
||||
"seed {seed}: {how}\ngrown {} widgets, shrank to {}\n{small:#?}",
|
||||
node.size(),
|
||||
"seed {seed} case {}: {how}\ngrown {} widgets, shrank to {}\n{small:#?}",
|
||||
case.name(),
|
||||
grown.size(),
|
||||
small.size()
|
||||
);
|
||||
panic!("seed {seed} lays out differently warm than cold");
|
||||
panic!(
|
||||
"seed {seed} lays out differently warm than cold after {}",
|
||||
case.name()
|
||||
);
|
||||
}
|
||||
});
|
||||
let sizes: Vec<usize> = (1..=seeds)
|
||||
.map(|seed| grow(&mut Rng::new(seed), depth).size())
|
||||
|
||||
let sizes: Vec<usize> = (1..=count as u64)
|
||||
.map(|seed| plan(seed, depth, &Edits::default()).size())
|
||||
.collect();
|
||||
let total: usize = sizes.iter().sum();
|
||||
println!(
|
||||
"{seeds} trees at depth {depth} agree: {} widgets total, largest {}",
|
||||
total,
|
||||
"{count} trees at depth {depth} agree over {} case(s): {} widgets total, largest {}",
|
||||
cases.len(),
|
||||
sizes.iter().sum::<usize>(),
|
||||
sizes.iter().max().copied().unwrap_or(0)
|
||||
);
|
||||
}
|
||||
@@ -16,6 +16,8 @@ mod drift;
|
||||
mod idempotence;
|
||||
#[path = "cases/layout.rs"]
|
||||
mod layout;
|
||||
#[path = "cases/plan.rs"]
|
||||
mod plan;
|
||||
#[path = "cases/pointer.rs"]
|
||||
mod pointer;
|
||||
#[path = "cases/pointer_routing.rs"]
|
||||
|
||||
Reference in new issue
Block a user