Scrolling is the one thing in these trees that reads the pixel length of its box, and the one that hands its child a box longer than its own, so a warm layout under it has to be rebuilt where the rest can be carried over. A sixth of the nodes at each level is now a scroll over a subtree, on either axis. Four of a hundred seeds now grow nothing but wrappers, so `reshuffled` returns early where there is no span to shuffle: a case with nothing to do is not the same as a shuffle that had no effect, which is what the assertion below it is for. 50 tests, and the ignored sweep over 100 seeds and eight scenarios, 800 comparisons. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
244 lines
8.1 KiB
Rust
244 lines
8.1 KiB
Rust
//! A seeded random widget tree, for tests and for looking at.
|
|
//!
|
|
//! One seed is one tree, on any machine and after any upgrade, so a test can
|
|
//! grow the same tree twice and a failing seed is reproduced by its number.
|
|
//! `examples/random.rs` draws one; `tests/generated.rs` checks that laying one
|
|
//! out again lands where growing it from scratch would.
|
|
|
|
use crate::prelude::*;
|
|
use std::collections::HashMap;
|
|
|
|
/// The declared lengths of one `SetSize`, by axis.
|
|
pub type Lens = [Option<Len>; 2];
|
|
|
|
/// What a test changes between two trees grown from the same seed, so the
|
|
/// warm one can be mutated and the cold one grown that way to begin with.
|
|
#[derive(Default)]
|
|
pub struct Edits {
|
|
/// Declared sizes, by the order the `SetSize` wrappers were made.
|
|
pub sizes: HashMap<usize, Lens>,
|
|
/// Which children a span has, by the order the spans were made.
|
|
pub spans: HashMap<usize, SpanEdit>,
|
|
}
|
|
|
|
#[derive(Default, Clone)]
|
|
pub struct SpanEdit {
|
|
/// Children to leave out, by index among the ones grown.
|
|
pub detach: Vec<usize>,
|
|
/// How many of the span's spares are in it, appended in order.
|
|
pub attach: usize,
|
|
}
|
|
|
|
/// xorshift64, written out rather than taken from a crate so that a seed
|
|
/// keeps meaning the same tree.
|
|
pub struct Rng(u64);
|
|
|
|
impl Rng {
|
|
pub fn new(seed: u64) -> Self {
|
|
Self(seed | 1)
|
|
}
|
|
|
|
pub fn bits(&mut self) -> u64 {
|
|
self.0 ^= self.0 << 13;
|
|
self.0 ^= self.0 >> 7;
|
|
self.0 ^= self.0 << 17;
|
|
self.0
|
|
}
|
|
|
|
pub fn below(&mut self, n: usize) -> usize {
|
|
(self.bits() % n as u64) as usize
|
|
}
|
|
|
|
pub fn chance(&mut self) -> bool {
|
|
self.bits() & 1 == 0
|
|
}
|
|
}
|
|
|
|
const COLORS: [UiColor; 6] = [
|
|
UiColor::RED,
|
|
UiColor::GREEN,
|
|
UiColor::BLUE,
|
|
UiColor::YELLOW,
|
|
UiColor::CYAN,
|
|
UiColor::MAGENTA,
|
|
];
|
|
|
|
/// Leaves grown beside every span, for a test to put into it.
|
|
const SPARES: usize = 3;
|
|
|
|
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.";
|
|
|
|
/// What growing a tree gives back: every widget in creation order, so two
|
|
/// trees from one seed line up index for index, and the declared sizes, which
|
|
/// are what a test changes to watch the change propagate.
|
|
#[derive(Default)]
|
|
pub struct Tree {
|
|
pub ids: Vec<WidgetId>,
|
|
pub sized: Vec<WeakWidget<SetSize>>,
|
|
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>,
|
|
}
|
|
|
|
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.
|
|
pub spares: Vec<StrongWidget>,
|
|
/// How many children it was grown with, before any edit.
|
|
pub grown: usize,
|
|
}
|
|
|
|
/// Grows the tree `seed` describes, `edits` replacing the declared sizes it
|
|
/// would otherwise have given those wrappers.
|
|
pub fn grow<Rsc: UiRsc + 'static>(
|
|
rsc: &mut Rsc,
|
|
seed: u64,
|
|
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)
|
|
}
|
|
|
|
struct Grow<'a, Rsc> {
|
|
rsc: &'a mut Rsc,
|
|
rng: Rng,
|
|
tree: Tree,
|
|
edits: &'a Edits,
|
|
}
|
|
|
|
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),
|
|
_ => {
|
|
let color = COLORS[self.rng.below(COLORS.len())];
|
|
let alpha = (self.rng.below(5) * 63) as u8;
|
|
rect(color.alpha(alpha)).add_strong(self.rsc)
|
|
}
|
|
};
|
|
self.tree.ids.push(id.id());
|
|
id
|
|
}
|
|
|
|
fn len(&mut self) -> Option<Len> {
|
|
match self.rng.below(4) {
|
|
0 => Some(Len::abs(20.0 + self.rng.below(180) as f32)),
|
|
1 => Some(Len::REST),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// A declared size over half the tree, kept where a test can change it.
|
|
fn sized(&mut self, inner: StrongWidget) -> StrongWidget {
|
|
if !self.rng.chance() {
|
|
return inner;
|
|
}
|
|
let idx = self.tree.sized.len();
|
|
let lens = [self.len(), self.len()];
|
|
let lens = self.edits.sizes.get(&idx).copied().unwrap_or(lens);
|
|
let id = SetSize {
|
|
inner,
|
|
x: lens[0],
|
|
y: lens[1],
|
|
}
|
|
.add(self.rsc);
|
|
self.tree.sized.push(id);
|
|
self.tree.ids.push(id.id());
|
|
id.add_strong(self.rsc)
|
|
}
|
|
|
|
fn node(&mut self, depth: usize) -> StrongWidget {
|
|
if depth == 0 {
|
|
return self.leaf();
|
|
}
|
|
if self.rng.below(6) == 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 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);
|
|
}
|
|
if self.rng.below(4) == 0 {
|
|
let inner = self.node(depth - 1);
|
|
let inner = self.sized(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 = || self.rng.below(24) as f32;
|
|
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 grown = 2 + self.rng.below(3);
|
|
let mut children = Vec::with_capacity(grown);
|
|
for _ in 0..grown {
|
|
let child = self.node(depth - 1);
|
|
children.push(self.sized(child));
|
|
}
|
|
if self.rng.chance() {
|
|
let id = Stack {
|
|
children,
|
|
size: StackSize::Child(0),
|
|
}
|
|
.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 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: self.rng.below(3) as f32 * 4.0,
|
|
}
|
|
.add(self.rsc);
|
|
self.tree.ids.push(id.id());
|
|
self.tree.spans.push(Spanned { id, spares, grown });
|
|
id.add_strong(self.rsc)
|
|
}
|
|
}
|