Grow images in the generated trees
`Image` is the only widget in the repository whose size hint is a length in pixels -- everything else hints a share, or nothing -- so it is the only one that exercises a rule beside a hint, a box a widget knows before it is drawn, and the answer the commit before this one changed. The generated trees had none, which is why nothing there could reach that case. `Kind::Image` is a fifth leaf, drawn one time in five, and it steps to a plain rect when the shrinker reduces it: a picture measures nothing either, but its length is its own, so the leaf that takes whatever it is given is the simpler one. The picture is a 64x64 checkerboard of purple and black in 8 px cells, committed at `src/assets/checkerboard.png` beside the generator that draws it -- the way `examples/tabs` keeps its own -- and included rather than opened, so that growing a tree does not depend on a working directory and one seed is one tree whatever anything else does. One upload per tree, however many images it grows: a `TextureHandle` is a counted reference, so the first image in a tree uploads the checkerboard and every one after it clones the handle. Measured: seed 1 at depth 4 grows 13 images and holds 1 texture, seed 6 grows none and holds none, and `a_tree_of_images_uploads_one_texture` asserts it. `Image::new` is what a caller holding a handle needs, since `image` uploads what it is given. A seed names a tree only while the generator draws the same things in the same order, so every seed now grows a different tree. The seed list in `generated.rs` says so: 20 and 86 no longer grow the trees whose defects they once caught, and both of those live on as shrunk fixtures in `unsettled.rs`, which are trees rather than numbers. The seeds those fixtures name are similarly historical, and their file says that too. Format, clippy with and without layout-diagnostics, and the suite (135 + 19 + 13 + 4) are clean. The cold dump is a new baseline of 34,571 boxes over the 400 depth-5 trees, since the trees themselves changed; all three seed scans pass over the new ones -- 400 at depth 5 in 62.79s, 1,000 at depth 6 in 160.20s, 2,000 at depth 4 in 299.58s -- which is what actually checks that images lay out warm the way they do cold. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b295c8b97a
commit
2dba90bd0f
6 files changed
+72
-10
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 191 B |
+29
-3
@@ -190,6 +190,9 @@ pub enum Kind {
|
|||||||
color: usize,
|
color: usize,
|
||||||
alpha: u8,
|
alpha: u8,
|
||||||
},
|
},
|
||||||
|
/// The one leaf whose own length is a number of pixels it knows before it
|
||||||
|
/// is drawn, which is the hint a rule beside it has to win over.
|
||||||
|
Image,
|
||||||
/// Scrolling reads the pixel length of its box, which nothing else here
|
/// Scrolling reads the pixel length of its box, which nothing else here
|
||||||
/// does, and gives its child a box longer than its own.
|
/// does, and gives its child a box longer than its own.
|
||||||
Scroll {
|
Scroll {
|
||||||
@@ -443,9 +446,11 @@ impl Kind {
|
|||||||
}
|
}
|
||||||
match self {
|
match self {
|
||||||
// The one leaf that reads the width it is given, then the one
|
// The one leaf that reads the width it is given, then the one
|
||||||
// that does not, then the one that measures nothing at all.
|
// that does not, then the one that measures nothing at all. A
|
||||||
|
// picture measures nothing either, but its length is its own, so
|
||||||
|
// it steps to the leaf that takes whatever it is given.
|
||||||
Kind::Wrapped => out.push(Kind::OneLine),
|
Kind::Wrapped => out.push(Kind::OneLine),
|
||||||
Kind::OneLine => out.push(Kind::Rect {
|
Kind::OneLine | Kind::Image => out.push(Kind::Rect {
|
||||||
color: 0,
|
color: 0,
|
||||||
alpha: 255,
|
alpha: 255,
|
||||||
}),
|
}),
|
||||||
@@ -627,9 +632,10 @@ struct Sow<'a> {
|
|||||||
|
|
||||||
impl Sow<'_> {
|
impl Sow<'_> {
|
||||||
fn leaf(&mut self) -> Plan {
|
fn leaf(&mut self) -> Plan {
|
||||||
Plan::bare(match self.rng.below(4) {
|
Plan::bare(match self.rng.below(5) {
|
||||||
0 => Kind::Wrapped,
|
0 => Kind::Wrapped,
|
||||||
1 => Kind::OneLine,
|
1 => Kind::OneLine,
|
||||||
|
2 => Kind::Image,
|
||||||
_ => {
|
_ => {
|
||||||
let color = self.rng.below(COLORS.len());
|
let color = self.rng.below(COLORS.len());
|
||||||
let alpha = (self.rng.below(5) * 63) as u8;
|
let alpha = (self.rng.below(5) * 63) as u8;
|
||||||
@@ -793,6 +799,7 @@ pub fn build<Rsc: UiRsc + 'static>(rsc: &mut Rsc, plan: &Plan) -> (StrongWidget,
|
|||||||
let mut build = Build {
|
let mut build = Build {
|
||||||
rsc,
|
rsc,
|
||||||
tree: Tree::default(),
|
tree: Tree::default(),
|
||||||
|
checkerboard: None,
|
||||||
};
|
};
|
||||||
let root = build.node(plan);
|
let root = build.node(plan);
|
||||||
(root, build.tree)
|
(root, build.tree)
|
||||||
@@ -801,6 +808,10 @@ pub fn build<Rsc: UiRsc + 'static>(rsc: &mut Rsc, plan: &Plan) -> (StrongWidget,
|
|||||||
struct Build<'a, Rsc> {
|
struct Build<'a, Rsc> {
|
||||||
rsc: &'a mut Rsc,
|
rsc: &'a mut Rsc,
|
||||||
tree: Tree,
|
tree: Tree,
|
||||||
|
/// The checkerboard, uploaded when the first image in this tree is built.
|
||||||
|
/// A handle is a reference to the texture, so every image after that one
|
||||||
|
/// clones this rather than uploading the same picture again.
|
||||||
|
checkerboard: Option<TextureHandle>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Rsc: UiRsc + 'static> Build<'_, Rsc> {
|
impl<Rsc: UiRsc + 'static> Build<'_, Rsc> {
|
||||||
@@ -826,6 +837,20 @@ impl<Rsc: UiRsc + 'static> Build<'_, Rsc> {
|
|||||||
built
|
built
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The one picture the generated trees draw: a 64x64 checkerboard of purple
|
||||||
|
/// and black in 8 px cells. Committed rather than drawn here, so that one
|
||||||
|
/// seed is one tree whatever anything else does, and included rather than
|
||||||
|
/// opened, so that growing a tree does not depend on a working directory.
|
||||||
|
fn checkerboard(&mut self) -> TextureHandle {
|
||||||
|
if self.checkerboard.is_none() {
|
||||||
|
let image = include_bytes!("assets/checkerboard.png")
|
||||||
|
.get_image()
|
||||||
|
.expect("the checkerboard is committed beside this file");
|
||||||
|
self.checkerboard = Some(self.rsc.ui_mut().textures.add(image));
|
||||||
|
}
|
||||||
|
self.checkerboard.clone().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
fn kind(&mut self, kind: &Kind) -> StrongWidget {
|
fn kind(&mut self, kind: &Kind) -> StrongWidget {
|
||||||
let id: StrongWidget = match kind {
|
let id: StrongWidget = match kind {
|
||||||
Kind::Wrapped => wtext(WORDS).size(16).wrap(true).add_strong(self.rsc),
|
Kind::Wrapped => wtext(WORDS).size(16).wrap(true).add_strong(self.rsc),
|
||||||
@@ -834,6 +859,7 @@ impl<Rsc: UiRsc + 'static> Build<'_, Rsc> {
|
|||||||
.wrap(false)
|
.wrap(false)
|
||||||
.add_strong(self.rsc),
|
.add_strong(self.rsc),
|
||||||
Kind::Rect { color, alpha } => rect(COLORS[*color].alpha(*alpha)).add_strong(self.rsc),
|
Kind::Rect { color, alpha } => rect(COLORS[*color].alpha(*alpha)).add_strong(self.rsc),
|
||||||
|
Kind::Image => Image::new(self.checkerboard()).add_strong(self.rsc),
|
||||||
Kind::Scroll { axis, inner } => {
|
Kind::Scroll { axis, inner } => {
|
||||||
let inner = self.node(inner);
|
let inner = self.node(inner);
|
||||||
let id = Scroll::new(inner, *axis).add(self.rsc);
|
let id = Scroll::new(inner, *axis).add(self.rsc);
|
||||||
|
|||||||
@@ -16,6 +16,15 @@ impl Widget for Image {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Image {
|
||||||
|
/// One texture already uploaded, for a caller holding its handle: [`image`]
|
||||||
|
/// uploads what it is given, and several widgets showing one picture want
|
||||||
|
/// one upload and one slot between them.
|
||||||
|
pub fn new(handle: TextureHandle) -> Self {
|
||||||
|
Self { handle }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn image<State: UiRsc>(image: impl LoadableImage) -> impl WidgetFn<State, Image> {
|
pub fn image<State: UiRsc>(image: impl LoadableImage) -> impl WidgetFn<State, Image> {
|
||||||
let image = image.get_image().expect("Failed to load image");
|
let image = image.get_image().expect("Failed to load image");
|
||||||
move |state| Image {
|
move |state| Image {
|
||||||
|
|||||||
+20
-3
@@ -5,7 +5,9 @@
|
|||||||
//! and the oracle another. And reducing a plan has to end, or a shrinker
|
//! and the oracle another. And reducing a plan has to end, or a shrinker
|
||||||
//! searching for the smallest counterexample never returns.
|
//! searching for the smallest counterexample never returns.
|
||||||
|
|
||||||
use iris::random::{Edits, Kind, Plan, Rng, SpanEdit, plan};
|
use iris::harness::Harness;
|
||||||
|
use iris::prelude::*;
|
||||||
|
use iris::random::{Edits, Kind, Plan, Rng, SpanEdit, grow, plan};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
fn some_edits(seed: u64, of: &Plan) -> Edits {
|
fn some_edits(seed: u64, of: &Plan) -> Edits {
|
||||||
@@ -67,8 +69,6 @@ fn some_edits(seed: u64, of: &Plan) -> Edits {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use iris::prelude::*;
|
|
||||||
|
|
||||||
/// The two routes to an edited tree are one tree. `plan` resolves edits out
|
/// 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
|
/// 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
|
/// already exists, which is the only route a shrunk plan has, since no seed
|
||||||
@@ -137,3 +137,20 @@ fn reducing_a_plan_all_the_way_ends() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Every image in a tree is the same picture, and a handle is a reference to
|
||||||
|
/// the texture rather than a copy of it, so one upload and one slot serve all
|
||||||
|
/// of them however many a tree grows -- and the trees are grown in hundreds.
|
||||||
|
#[test]
|
||||||
|
fn a_tree_of_images_uploads_one_texture() {
|
||||||
|
let mut images = 0;
|
||||||
|
let mut tree = plan(1, 4, &Edits::default());
|
||||||
|
tree.walk_mut(&mut |p| images += (p.kind == Kind::Image) as usize);
|
||||||
|
assert!(images > 1, "a tree of {images} images tests nothing");
|
||||||
|
|
||||||
|
let mut h = Harness::new((900, 1200));
|
||||||
|
let (root, _) = grow(&mut h.rsc, 1, 4, &Edits::default());
|
||||||
|
h.state.root = Some(root);
|
||||||
|
h.frame();
|
||||||
|
assert_eq!(h.rsc.ui().textures.count(), 1);
|
||||||
|
}
|
||||||
@@ -9,6 +9,12 @@
|
|||||||
//! reached through a region node's own entry rather than through the offer
|
//! 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
|
//! 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.
|
//! it measured, rounded to a step below the line it measured there.
|
||||||
|
//!
|
||||||
|
//! Each says which seed it was shrunk from, of the generator as it stood when
|
||||||
|
//! it was found. Those numbers no longer grow those trees -- a seed names one
|
||||||
|
//! only while the generator draws the same things in the same order, and the
|
||||||
|
//! leaves have grown an image since -- so what is written out below is the
|
||||||
|
//! record of the case, and the seed is where it came from.
|
||||||
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
|||||||
+8
-4
@@ -25,10 +25,14 @@ fn depth() -> usize {
|
|||||||
env("IRIS_GENERATED_DEPTH", 4)
|
env("IRIS_GENERATED_DEPTH", 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The seeds the ordinary tests take. Eight that have never failed; 86,
|
/// The seeds the ordinary tests take: a corpus rather than a set of
|
||||||
/// which a `Scroll` fixed point once settled differently on; and 20, which
|
/// regression cases, since a seed names a tree only for as long as the
|
||||||
/// caught a locally redrawn widget being placed twice in the box its parent
|
/// generator draws the same things in the same order. Adding images to the
|
||||||
/// had already placed it in.
|
/// leaves moved every one of them, so 20 and 86 -- which once caught a widget
|
||||||
|
/// placed twice in a box its parent had already placed it in, and a `Scroll`
|
||||||
|
/// fixed point settling differently -- no longer grow those trees. Both
|
||||||
|
/// defects are pinned by the shrunk fixtures in `cases/unsettled.rs`, which
|
||||||
|
/// are trees rather than numbers.
|
||||||
const SEEDS: [u64; 10] = [1, 2, 3, 5, 8, 10, 13, 20, 86, 98];
|
const SEEDS: [u64; 10] = [1, 2, 3, 5, 8, 10, 13, 20, 86, 98];
|
||||||
|
|
||||||
fn check(seed: u64, depth: usize, case: Case) {
|
fn check(seed: u64, depth: usize, case: Case) {
|
||||||
|
|||||||
Reference in new issue
Block a user