Compare commits

...
Author SHA1 Message Date
iris-aiandClaude Opus 5 2dba90bd0f 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>
2026-09-20 04:50:31 -04:00
iris-aiandClaude Opus 5 b295c8b97a Read a leftover as a minimum where nothing divides it
A share under a parent that divides nothing is still a share: the pixels and
fraction beside it are taken first, the share fills whatever the box has left,
and where those are already longer than the box they overflow it exactly as
they would without the share. So the length is `max(box, px + rel*box)`, a
minimum the share imposes rather than an addition to what was asked for
(Bryan, 2026-09-20, generalising the same `max` he gave for `Scroll`'s content
length two days earlier).

A span does that. Measured at `77ed7a2`, a probe recording the box it is asked
in, in a 400 px window, under `.wrapper()` against a one-child span:

    rule                      nothing divides   a span divides
    leftover(1)                           400              400
    px(50) + leftover(1)                  400              400
    px(500) + leftover(1)                 400              500
    rel(0.5) + leftover(1)                400              400
    px(500), no leftover                  500              500

One row disagreed, and the same length without the share overflows fine
(drawn -50..450, its alignment centring it), so what swallowed the overflow was
the share. `LayoutLen::declared` refuses to answer for anything carrying
leftover weight, so the non-dividing path never learned the fixed part and fell
back to the offer.

Said as the place the parent gives rather than as a declaration, because that
is what the retained record already keeps: where the fixed part is the longer,
`widget_at` hands the child `fixed.as_desc().fills()` -- a box of that length,
placed by the child's alignment, its own rel base -- which is what a declared
length already comes to, and `active.placed` stores it, so a recomposed subtree
reads the same box without resolving anything again. A place that is already
the child's placement is skipped: a parent that divides has given the share
whatever it was owed, and re-placing a span's slot moved its child.

Which of two lengths is longer is a question in pixels, so it is one operation
with the crossing kept as a window range, and both callers now share it.
`Painter::longer_than` is that operation -- the span's room for the shares it
divides, and a share past the box it was given -- and it narrows this widget's
range where the span replaced it, since a comparison the framework makes on an
arbitrary parent's behalf is one more reason its drawing holds, not the only
one. A `SizeRule::Min` of `rel(1.0)` is the same operation again, which is what
this is (Bryan, 2026-09-20); when that lands it belongs on this path.

`a_share_is_a_minimum_wherever_nothing_divides_it` walks the table above and
holds the two parents to the same length; the crossing case is checked from
both sides, by a window that crosses it and by the rule itself crossing while
the window holds still. Both fail at `77ed7a2` with 400 where 500 is wanted. A
change of rule needs nothing to escalate it: the reported size is the rule
resolved, so the answer changes and the parent refuses its own drawing --
verified by writing the escalation, finding the tests pass without it, and
dropping it.

Format, clippy with and without layout-diagnostics, and the suite (134 + 19 +
13 + 4) are clean. The cold dump over 400 depth-5 trees is byte-identical to
`77ed7a2` across all 34,488 boxes, since no generated tree carries a share with
pixels beside it -- which the next commit changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 04:49:50 -04:00
iris-aiandClaude Opus 5 c2b8bf83de Let a rule beat a hint, and name marking a widget for redraw
A ninth sweep, over the part no earlier round named -- the widget vocabulary and
the builder methods, `Widgets`, the examples, the `util` additions and the
manifests -- and once more over `77ed7a2`, the eighth sweep's own commit and so
itself unreviewed.

A hint overrode a rule. `declared_lens` asked `rules[axis].declared()` first and
fell through to the widget's own `size_hint` whenever that answered `None` --
which it does for a share, since a share is not a declaration. So a widget
carrying `width(leftover(1))` and hinting a pixel length of its own was handed a
box of the hint, against the rule and against the comment inside the function:
"a hint still narrows the box where no rule does". `Painter::size_hint` spells
the same rule-else-hint step three hundred lines up and gets it right, with the
reason written on it; both read `Widgets::exact_len` now, and `declared_lens` is
the part of its answer that needs nobody to divide it. `Image` is the only
widget here whose hint is a declared length, and neither the tests nor the
generator builds one, so nothing in this repository could reach the difference
-- which is why the dump is unchanged and why the test builds a widget of its
own. It records the box it was asked in: 400 with the rule and 50 without, and
50 either way before this.

Marking a widget for redraw had no name. Twenty-one sites under `tests/` said it
as `widgets_mut().get_dyn_mut(id);` with the widget thrown away, five with a
`let _ =` in front, one with a comment explaining what the line was for, and one
wrapped in a local function called `mark`. `Widgets::mark_for_redraw` says it.
`revision_cost.rs` keeps the long spelling and now says why in place: it is
deliberately in the API subset an old worktree also has.

`assert_same_regions` could not see the defect the eighth sweep had just fixed.
It zips the warm and cold id lists, so a list naming one widget twice -- which
is what `width`, `sized` and `align` giving back their own argument produces --
compares fewer boxes than it lists and says nothing about it. It now rejects a
repeated id and two lists of different lengths, which also checks the nine
fixtures that round left alone: all eighteen cases pass.

Bare pairs where the framework has named ones. `random.rs`'s `Lens` and `Aligns`
were `[Option<LayoutLen>; 2]` and `[Option<AxisAlign>; 2]`, read as `[0]`/`[1]`
and zipped against a hand-written `[Axis::X, Axis::Y]`. They are `SizeRules` and
`Align`; `Align` took the `Index<Axis>` every other per-axis pair on this branch
has, and `RegionAlign::from` does the "an axis left out is centred" step two
rigs were spelling per axis. The three sites that wrote the axis pair out say
`Axis::BOTH`, which is what the rest of the layout code says.

`BothAxis<T>`, `AxisT`, `XAxis` and `YAxis` -- 45 lines with a const trait, two
marker types and three accessors -- have no user anywhere in the workspace. They
are the mechanism `impl_axis_index!` replaced, in the file this branch took
`Vec2::axis`/`axis_mut` out of. Deleted, which is a drive-by in a block the
branch was already rewriting; drop it if the scope matters more.

Smaller things, each in its own place: `Wrapper` arrived beside core's
`WidgetWrapper`, one word for a widget that wraps a child and for a dynamic
borrow guard, so the alias is gone and its two uses name `DynBorrower` -- which
is what they are. `Wrapper::new`, `Wrapper::empty` and its `Default` were three
names for one value, two of them unused. `Arena::get_mut` was the only
`pub(crate)` among `pub` siblings on a public type. `Selector` rounded the
pointer onto the pixel grid to do arithmetic on two values already there, losing
the precision the platform gave it for nothing; the step between the regions is
taken on the grid instead. And the two `debug` profile settings carry their
reason where the next reader looks rather than only in the commit that made
them, one of which was about renaming `rest`.

Format, clippy with and without layout-diagnostics, and the suite (132 + 19 + 13
+ 4) are clean. The cold dump over 400 depth-5 trees is byte-identical to
`77ed7a2` across all 34,488 boxes, and all three seed scans pass: 400 at depth 5
in 63.27s, 1,000 at depth 6 in 160.45s, 2,000 at depth 4 in 302.52s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 03:56:01 -04:00
iris-aiandClaude Opus 5 77ed7a24c0 Say how many widgets a shrunk fixture has, and share what tests repeat
An eighth sweep, over the part no earlier round named: the 6,300 lines of
tests, and once more over the seventh sweep's own commit, which was itself
unreviewed.

Four of the shrunk fuzz fixtures name one widget two or three times. `width`,
`sized` and `align` set a rule on the widget they are given and return its own
id -- only `pad` and `wrapper` make a new one -- so `let sized =
wrapped.width(76).add(..)` and the `let aligned = sized` beside it are three
names for one text. Each name then went into the list of ids the case compares
warm against cold, so a case that says it checks six boxes checks four, and
three doc comments quote that inflated count as the size of the tree the
shrinker reduced to. Measured: `plant` and `plant_fixed` list 6 and hold 4,
`plant_pair` lists 4 and holds 3, `plant_scrolled` lists 8 and holds 7. The
aliases are gone and the counts say what the fixtures build; each rebuilt
fixture was diffed against the old one, and both the widget slots and every
region are identical, for both settings of `swapped`.

`assert_same_regions` sits at the top of `unsettled.rs` and six tests call it.
Seven more spell its body out instead, byte for byte. They call it now, and it
is `#[track_caller]` so the panic names the case.

`tests/gpu/mod.rs` holds the adapter probe and the surface configuration that
`draw_cost` and `chain_cost` had a copy of each -- `config` identical, and the
probe identical but for the feature it asks for. The leak's justification lived
in one file with the other referring to it; it now sits on the thing it is
about. Shared through `#[path]`, the way `scenario/mod.rs` already is.

The mask a widget is clipped by was resolved in three places, two of them a
byte-identical closure. `mask_bounds` takes the slot rather than the widget,
because the third site deliberately reads the slot it saved before the frame:
that a redraw keeps the slot is what it is checking.

`Layered::_revision` was a field nothing reads, incremented to mark the widget
dirty. Two tests in the same file already do that with
`get_dyn_mut`, which is what the underscore was hiding.

`plan.rs` claimed every simplification is strictly smaller, and asserted `<=`.
Measured: 53 of one tree's 101 simplifications keep the widget count, since a
dropped alignment and a simpler leaf both do. The assertion is right and the
claim was not; the comment now gives the argument that does hold.

`generated.rs` said "Seven that have never failed" and "the nine the others
check" of a ten-seed array. The `should_panic` scroll test ended in an
`h.frame()` that cannot run, since `set_root` lays out and is where the panic
comes from. Two `drop(tree)` at the end of their own scope did nothing.

Format, clippy with and without layout-diagnostics, and the suite (131 + 19 +
13 + 4) are clean. The cold dump over 400 depth-5 trees is byte-identical to
f8aa0c5 across all 34,490 boxes. No library code changed, so the seed scans
have nothing to find. Both GPU rigs were rebuilt and run: chain cost +470% at
depth 64, draw cost ~4.4 us per layer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 03:18:18 -04:00
iris-aiandClaude Opus 5 f8aa0c5cdf Stop a scroll asking a question it has already answered
A seventh sweep, over the parts no earlier round named: the tree generator
and the scenario harness, `Fixed`, the headless rig, and once more over the
commit the sixth sweep left, which was itself unreviewed.

`Scroll`'s content box is `answer_px.max(container_len)`, so a scroll whose
content fits has nothing to scroll through and `update_amt` has already put
`amt` at zero. The test choosing between the viewport and a scrolled span
asked `amt != ZERO || content_len != container_len`, where the first
disjunct can never decide it -- the same defect `b7b8d09` removed from the
line above, one operand over. A `debug_assert` of the implication held
across the whole suite, including every scrolling test.

`Fixed::to_scale` and its private `shift_round` arrived on this branch with
no caller and never got one; the only thing that called either was the test
written for them.

`Len::align` wrote `Len` arithmetic out a component at a time, around an
`at.px` that is always zero, where `Len::scale` and the `Add`/`Sub` beside
it say the whole rule in two lines. `LayoutLen::without_leftover` took
`self` where the `apply_leftover` its own doc calls the opposite reading of
the same value takes `&self`.

`run-headless.sh --resize` changed the output's mode but not `out_w`/`out_h`,
which is the extent `replay-touch` scales a recording against -- so
`--resize` with `--replay` put every sample of the gesture somewhere else
and still finished like a run that worked. Both come from one function now.

The generator's plan/build split stranded a comment: "a row takes the height
it is given" describes the size rule `build` derives from `dir`, and it was
left above the `gap` draw, which is the one line it is not about and which
does consume randomness.

Format, clippy with and without layout-diagnostics, and the suite (131 + 19
+ 13 + 4) are clean. The cold dump over 400 depth-5 trees is byte-identical
to b7b8d09 across all 34,492 boxes, and all three seed scans pass: 400 at
depth 5 in 62.75s, 1,000 at depth 6 in 162.37s, 2,000 at depth 4 in 305.25s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 02:53:02 -04:00
iris-aiandClaude Opus 5 b7b8d09e40 Write a shared constant once, and stop a scroll placing its own content
Two findings from a sweep over the WGSL prelude and the position widgets,
scoped against upstream/main at ca2b4b2.

`module_source` already builds each shader's preamble from iris_core's own
constants, so the move-chain work's second copy of `MOVE_NONE` and
`CHAIN_LIMIT` -- under "keep in step with iris_core::CHAIN_LIMIT" -- asked a
reader by hand for what the mechanism beside it exists to do. Both are
injected now, with `MASK_NONE` beside them replacing a bare literal, and the
shader declares none of them.

`Scroll`'s `content_len` is never less than its box, so `slack` and the
`anchor` computed from it were always zero whatever the alignment: the
framework centres short content by placing the answer in the whole box, and
the comment credited arithmetic that could not have done it. The same belief
guarded the fits-in-the-box contract with `align == NEG`, so at the default
alignment -- the middle -- every box change redrew the scroll, measured as 1
widget against 0 at TOP_LEFT. `align` now has no reader at all.

`UiSpan::translated` and `UiRegion::translated` are reachable only from each
other and from nothing else.

Format, clippy with and without layout-diagnostics, and the 131-test suite
are clean. The cold dump over 400 depth-5 trees is byte-identical to
1096c31, and all three seed scans pass: 400 at depth 5 in 69.07s, 1,000 at
depth 6 in 169.29s, 2,000 at depth 4 in 300.75s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 02:09:41 -04:00
39 changed files with 640 additions and 525 deletions

No files matched your search

+6
View File
@@ -25,6 +25,12 @@ tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread", "time"]
[workspace] [workspace]
members = ["core", "macro", "rig-input"] members = ["core", "macro", "rig-input"]
# Full debug info was the bulk of what the linker wrote here and almost none of
# what anything read. `dev` keeps line tables and scopes, which is what stepping
# through an example wants; the tests keep the line tables alone, which is what
# a backtrace reads. Measured when the tests became one target: relinking them
# went from 9.8 s to 7.7 s with these, and target/ from 45 GB to 13 GB with the
# two changes together.
[profile.dev] [profile.dev]
debug = 1 debug = 1
-27
View File
@@ -131,14 +131,6 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
self.0 as f32 / Self::one().0 as f32 self.0 as f32 / Self::one().0 as f32
} }
/// 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 => self.0 << (TO - SHIFT),
false => shift_round(self.0 as i64, SHIFT - TO) as i32,
})
}
pub const fn add(self, rhs: Self) -> Self { pub const fn add(self, rhs: Self) -> Self {
Self(self.0.wrapping_add(rhs.0)) Self(self.0.wrapping_add(rhs.0))
} }
@@ -246,16 +238,6 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
} }
} }
/// Back to a single step, rounding halves away from zero so that a value and
/// its negation round to the same distance.
const fn shift_round(v: i64, bits: u32) -> i64 {
let half = (1i64 << bits) >> 1;
match v < 0 {
true => -((-v + half) >> bits),
false => (v + half) >> bits,
}
}
const fn div_round(num: i64, den: i64) -> i64 { const fn div_round(num: i64, den: i64) -> i64 {
let (q, rem) = (num / den, num % den); let (q, rem) = (num / den, num % den);
match rem.unsigned_abs() * 2 >= den.unsigned_abs() { match rem.unsigned_abs() * 2 >= den.unsigned_abs() {
@@ -531,15 +513,6 @@ mod tests {
assert_eq!(Px::from_f32(-1e12), Px::MIN); assert_eq!(Px::from_f32(-1e12), Px::MIN);
} }
#[test]
fn a_coarser_grid_rounds_and_a_finer_one_does_not() {
// A third, which neither grid holds exactly.
let third = Rel::ONE / Rel::from_int(3);
assert_eq!(third.to_scale::<6>(), Fixed::<6>::from_raw(21));
let coarse = Fixed::<6>::from_raw(21);
assert_eq!(coarse.to_scale::<24>().to_scale::<6>(), coarse);
}
#[test] #[test]
fn lerp_takes_the_fraction_as_the_receiver() { fn lerp_takes_the_fraction_as_the_receiver() {
let (from, to) = (Px::from_int(10), Px::from_int(20)); let (from, to) = (Px::from_int(10), Px::from_int(20));
+7 -4
View File
@@ -3,7 +3,7 @@ use crate::{Px, Rel};
use super::*; use super::*;
#[derive(Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub struct Align { pub struct Align {
pub x: Option<AxisAlign>, pub x: Option<AxisAlign>,
pub y: Option<AxisAlign>, pub y: Option<AxisAlign>,
@@ -151,13 +151,15 @@ impl Vec2 {
} }
impl Len { impl Len {
/// This length placed in the box it is measured in: the alignment names a
/// point along that box, and the two ends are that point less the part of
/// the length falling before it and plus the part falling after.
pub const fn align(&self, align: AxisAlign) -> UiSpan { pub const fn align(&self, align: AxisAlign) -> UiSpan {
let rel = align.rel(); let rel = align.rel();
let rest = Rel::ONE.sub(rel);
let at = Len::from_parts(rel, Px::ZERO); let at = Len::from_parts(rel, Px::ZERO);
UiSpan { UiSpan {
start: Len::from_parts(at.rel.sub(self.rel.mul(rel)), at.px.sub(self.px.mul(rel))), start: at - self.scale(rel),
end: Len::from_parts(at.rel.add(self.rel.mul(rest)), at.px.add(self.px.mul(rest))), end: at + self.scale(Rel::ONE.sub(rel)),
} }
} }
} }
@@ -212,3 +214,4 @@ impl RegionAlign {
} }
impl_axis_index!(RegionAlign => AxisAlign); impl_axis_index!(RegionAlign => AxisAlign);
impl_axis_index!(Align => Option<AxisAlign>);
-45
View File
@@ -71,50 +71,5 @@ impl Vec2 {
} }
} }
pub const trait AxisT {
fn get() -> Axis;
}
pub struct XAxis;
const impl AxisT for XAxis {
fn get() -> Axis {
Axis::X
}
}
pub struct YAxis;
const impl AxisT for YAxis {
fn get() -> Axis {
Axis::Y
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct BothAxis<T> {
pub x: T,
pub y: T,
}
impl<T> BothAxis<T> {
pub const fn axis<A: const AxisT>(&mut self) -> &mut T {
match A::get() {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
pub fn take_axis<A: const AxisT>(self) -> T {
match A::get() {
Axis::X => self.x,
Axis::Y => self.y,
}
}
pub fn axis_dyn(&mut self, axis: Axis) -> &mut T {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
}
impl_axis_index!({const SHIFT: u32} FixedVec2<SHIFT> => Fixed<SHIFT>); impl_axis_index!({const SHIFT: u32} FixedVec2<SHIFT> => Fixed<SHIFT>);
impl_axis_index!(Vec2 => f32); impl_axis_index!(Vec2 => f32);
+1 -1
View File
@@ -169,7 +169,7 @@ impl LayoutLen {
/// anyone not dividing a box between siblings, where a share is a claim /// anyone not dividing a box between siblings, where a share is a claim
/// on someone else's room rather than a length of its own. /// on someone else's room rather than a length of its own.
/// [`Self::apply_leftover`] is the opposite reading of the same value. /// [`Self::apply_leftover`] is the opposite reading of the same value.
pub const fn without_leftover(self) -> Len { pub const fn without_leftover(&self) -> Len {
Len::from_parts(self.rel, self.px) Len::from_parts(self.rel, self.px)
} }
-20
View File
@@ -281,15 +281,6 @@ impl UiSpan {
pub const fn len(&self) -> Len { pub const fn len(&self) -> Len {
self.end - self.start 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)] #[repr(C)]
@@ -300,17 +291,6 @@ pub struct UiRegion {
} }
impl 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 { pub const FULL: Self = Self {
x: UiSpan::FULL, x: UiSpan::FULL,
y: UiSpan::FULL, y: UiSpan::FULL,
-4
View File
@@ -133,10 +133,6 @@ impl TextBuffer {
} }
} }
pub fn new_empty() -> Self {
Self::new("")
}
pub fn text(&self) -> &str { pub fn text(&self) -> &str {
&self.text &self.text
} }
-4
View File
@@ -167,10 +167,6 @@ impl GlyphAtlas {
pub fn page_count(&self) -> u32 { pub fn page_count(&self) -> u32 {
self.pages.len() as u32 self.pages.len() as u32
} }
pub fn glyph_count(&self) -> usize {
self.entries.len()
}
} }
impl Page { impl Page {
+13 -4
View File
@@ -23,13 +23,22 @@ pub use primitive::*;
const PRELUDE: &str = include_str!("./shader/prelude.wgsl"); const PRELUDE: &str = include_str!("./shader/prelude.wgsl");
fn module_source(wgsl: &str) -> String { fn module_source(wgsl: &str) -> String {
// The steps come from the same constants the CPU counts in, rather than // Every number both sides count in, written once here rather than a
// a second copy of them written into the shader: a grid the two disagree // second time in the shader: a grid the two disagree about puts every
// about puts every coordinate somewhere else. // coordinate somewhere else, and a sentinel they disagree about makes one
// of them walk a chain from a slot the other says is not there.
format!( format!(
"const PX_STEP: f32 = 1.0 / {}.0;\nconst REL_STEP: f32 = 1.0 / {}.0;\n{PRELUDE}\n{wgsl}", "const PX_STEP: f32 = 1.0 / {}.0;\n\
const REL_STEP: f32 = 1.0 / {}.0;\n\
const MASK_NONE: u32 = {}u;\n\
const MOVE_NONE: u32 = {}u;\n\
const CHAIN_LIMIT: u32 = {}u;\n\
{PRELUDE}\n{wgsl}",
1u32 << crate::PX_SHIFT, 1u32 << crate::PX_SHIFT,
1u32 << crate::REL_SHIFT, 1u32 << crate::REL_SHIFT,
MaskIdx::NONE.idx(),
MoveIdx::NONE.idx(),
crate::CHAIN_LIMIT,
) )
} }
+6 -10
View File
@@ -26,9 +26,11 @@ struct MoveOffset {
parent: u32, parent: u32,
} }
// `PX_STEP` and `REL_STEP` are prepended from `iris_core`'s own constants: // `PX_STEP`, `REL_STEP`, `MASK_NONE`, `MOVE_NONE` and `CHAIN_LIMIT` are
// what it stores is a whole count of each, both powers of two, so decoding // prepended from `iris_core`'s own constants, so none of them is written
// is exact and the number here is the number the CPU decided. // twice. What the CPU stores is a whole count of each step, and both steps
// are powers of two, so decoding is exact and the number here is the number
// the CPU decided.
// Every coordinate the CPU decided is a whole count of `PX_STEP`, so one that // Every coordinate the CPU decided is a whole count of `PX_STEP`, so one that
// composes to within half a step of a pixel boundary is on that boundary and // composes to within half a step of a pixel boundary is on that boundary and
@@ -70,12 +72,6 @@ struct Region {
y: UiSpan, y: UiSpan,
} }
const MOVE_NONE: u32 = 4294967295u;
// Keep in step with `iris_core::CHAIN_LIMIT`. It bounds a malformed cycle
// rather than any real tree, and the CPU walk uses the same number so both
// resolve a deep one the same way.
const CHAIN_LIMIT: u32 = 64u;
// The same expression `Len::within` uses, in floats rather than on the // The same expression `Len::within` uses, in floats rather than on the
// CPU's grid: a move is resolved here so that scrolling a subtree writes one // CPU's grid: a move is resolved here so that scrolling a subtree writes one
// entry instead of walking it. What has to hold is that this agrees with // entry instead of walking it. What has to hold is that this agrees with
@@ -171,7 +167,7 @@ fn vs_main(
} }
fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> { fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> {
if in.mask_idx == 4294967295u { if in.mask_idx == MASK_NONE {
return color; return color;
} }
let mask = masks[in.mask_idx]; let mask = masks[in.mask_idx];
+108 -32
View File
@@ -187,10 +187,20 @@ impl<'a> Painter<'a> {
id: &'s StrongWidget<W>, id: &'s StrongWidget<W>,
place: impl Into<PlaceDesc>, place: impl Into<PlaceDesc>,
) -> DrawResult<'s, 'a, W> { ) -> DrawResult<'s, 'a, W> {
let place = self.resolve_rel_base(place.into()); let mut place = self.resolve_rel_base(place.into());
let region_node = self.rsc.widgets().is_region_node(id.id()); let region_node = self.rsc.widgets().is_region_node(id.id());
let declared = self.declared_lens(id);
let align = self.rsc.widgets().alignment(id.id()); let align = self.rsc.widgets().alignment(id.id());
// A share fills what the pixels and fraction beside it leave of the
// box and overflows where they are longer, which is the rule a span
// follows with one child. Only the overflow is a box of the child's
// own: a share that fits is the box it was given, which is what this
// place already says.
for axis in Axis::BOTH {
if let Some(len) = self.share_past_the_offer(id.id(), place, align, axis) {
place[axis] = len.as_desc().fills();
}
}
let declared = self.declared_lens(id);
let (rel_base, region) = let (rel_base, region) =
place.rel_base_and_region(self.region, self.rel_base, declared, align); place.rel_base_and_region(self.region, self.rel_base, declared, align);
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
@@ -291,27 +301,59 @@ impl<'a> Painter<'a> {
} }
} }
/// What a widget's rules declare its lengths to be, which whoever draws /// What a rule or a hint declares a widget's lengths to be, which whoever
/// it resolves into its rel base. Reading them depends on nothing -- the box /// draws it resolves into its rel base. Reading them depends on nothing -- the box
/// that comes of them is kept on the child, and `redraw` compares it /// that comes of them is kept on the child, and `redraw` compares it
/// there. /// there.
fn declared_lens<W: ?Sized>(&self, id: &StrongWidget<W>) -> Declared { fn declared_lens<W: ?Sized>(&self, id: &StrongWidget<W>) -> Declared {
self.rsc.widgets().declared_lens(id.id()) self.rsc.widgets().declared_lens(id.id())
} }
/// The box a child's own share asks for where that is longer than the box
/// `place` gives it, and nothing where the share fits.
///
/// A share is a length only to whoever divides one, and nothing divides a
/// box handed to one child: what is left of it after the pixels and the
/// fraction beside the share is what the share takes, so the length comes
/// to the whole box until those are longer than it and to them once they
/// are. Only that second case is a box this widget did not give, and the
/// crossing between them is a question in pixels, so this widget's drawing
/// holds for the windows on one side of it. Narrowed rather than stated,
/// because this widget may have read its own box as well, and a range it
/// pinned for that still holds.
fn share_past_the_offer(
&mut self,
id: WidgetId,
place: PlaceDesc,
align: RegionAlign,
axis: Axis,
) -> Option<Len> {
// A place that is the child's placement outright is a box its parent
// decided, and a parent that divides one has already given the share
// whatever it was owed. Only an offer -- a box with the answer still
// to be placed inside it -- is a box a share reads.
if place[axis].fills {
return None;
}
// A share with nothing beside it is the box whatever the box is, so
// there is no comparison to make and no range to keep for one.
let stated = self.rsc.widgets().exact_len(id, axis)?;
if stated.leftover == Weight::ZERO || stated.is_only_leftover() {
return None;
}
let fixed = stated
.without_leftover()
.within_len(place.base(axis, self.rel_base));
let offer = place.of(self.region, align)[axis].len();
self.longer_than(fixed, offer, axis).then_some(fixed)
}
/// What a child says its length is without being drawn, if it can say, /// What a child says its length is without being drawn, if it can say,
/// as the length its draw would report: a fraction in it is resolved /// as the length its draw would report: a fraction in it is resolved
/// against this widget's rel base, which is the rel base a child asked with /// against this widget's rel base, which is the rel base a child asked with
/// nothing narrowed gets. Asking counts as reading its size. /// nothing narrowed gets. Asking counts as reading its size.
pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<LayoutLen> { pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<LayoutLen> {
let widgets = self.rsc.widgets(); let hint = self.rsc.widgets().exact_len(id.id(), axis);
// A rule is the answer where there is one: it wins over whatever the
// widget would draw, so it has to win over what the widget says too.
let hint = widgets.size_rules(id.id())[axis].exact().or_else(|| {
widgets
.get_dyn(id.id())
.and_then(|widget| widget.size_hint(axis))
});
let rel_base = self.rel_base[axis]; let rel_base = self.rel_base[axis];
let resolved = hint.map(|hint| hint.within_len(rel_base)); let resolved = hint.map(|hint| hint.within_len(rel_base));
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
@@ -482,6 +524,40 @@ impl<'a> Painter<'a> {
len.to_px(window) len.to_px(window)
} }
/// Whether `len` is longer than `than`, kept as the windows that comparison
/// comes out the same way on: a drawing that took one of two lengths holds
/// where the same one is the longer, and nowhere else.
///
/// Which is longer is a question in pixels -- `rel(0.5)` is longer than 300
/// px at a box of 600 and shorter at 400 -- and it is asked of the
/// difference and answered back through that same difference, so the
/// boundary is the drawing's own rather than a second way of finding it.
/// Narrowed rather than stated, because whatever else this widget read
/// about the window is a reason its drawing holds where it does too.
///
/// This is the one operation a length that is the longer of two needs: the
/// room a container has left for the shares it divides, and a share that
/// overflows the box it was given because the pixels beside it are longer
/// than the box.
pub fn longer_than(&mut self, len: Len, than: Len, axis: Axis) -> bool {
let over = len - than;
let window = self.window[axis];
let longer = over.to_px(window) > Px::ZERO;
let side = match longer {
true => Px::STEP..=Px::MAX,
false => Px::MIN..=Px::ZERO,
};
let holds = Holds::from(side).through(over);
debug_assert!(
holds.contains(window),
"'{}' ({:?}) compared two lengths and kept a range without this window",
self.label(),
self.id
);
self.own[axis].window = self.own[axis].window.and(holds);
longer
}
/// The windows this drawing holds for, stated rather than taken: a /// The windows this drawing holds for, stated rather than taken: a
/// container that branched on a length in pixels says which side of the /// container that branched on a length in pixels says which side of the
/// boundary it was on, which is wider than the one window reading that /// boundary it was on, which is wider than the one window reading that
@@ -647,24 +723,27 @@ impl Painter<'_> {
} }
impl Widgets { impl Widgets {
/// What a widget's box is where a rule or its own hint says so outright. /// What says a widget's length on one axis without drawing it, if anything
pub(super) fn declared_lens(&self, id: WidgetId) -> Declared { /// does. A rule is the answer where there is one: it wins over whatever the
let rules = self.size_rules(id); /// widget would draw, so it has to win over what the widget says too -- a
let widget = self.get_dyn(id); /// share included, since a share is a length only to whoever divides one,
Declared::from_axes(|axis| { /// and that is the parent rather than this widget.
rules[axis].declared().or_else(|| { fn exact_len(&self, id: WidgetId, axis: Axis) -> Option<LayoutLen> {
// A hint still narrows the box where no rule does, which is self.size_rules(id)[axis].exact().or_else(|| {
// how a widget with a natural pixel size -- an image, a gap // A hint still narrows the box where no rule does, which is how a
// -- gets that size rather than the whole offer. That is the // widget with a natural pixel size -- an image, a gap -- gets that
// offer's business rather than a declaration's, and this // size rather than the whole offer. That is the offer's business
// falls away once a widget occupies its reported size inside // rather than a declaration's, and this falls away once a widget
// the box it was offered. // occupies its reported size inside the box it was offered.
widget self.get_dyn(id)?.size_hint(axis)
.and_then(|widget| widget.size_hint(axis))
.and_then(|len| len.declared())
})
}) })
} }
/// What a widget's box is where a rule or its own hint gives one outright,
/// rather than a share for whoever draws it to divide.
pub(super) fn declared_lens(&self, id: WidgetId) -> Declared {
Declared::from_axes(|axis| self.exact_len(id, axis)?.declared())
}
} }
impl LayoutLen { impl LayoutLen {
@@ -729,10 +808,7 @@ impl PlaceDesc {
let mut rel_base = parent_rel_base; let mut rel_base = parent_rel_base;
let mut region = given; let mut region = given;
for axis in Axis::BOTH { for axis in Axis::BOTH {
let base = match self[axis].rel_base { let base = self.base(axis, parent_rel_base);
RelBase::Len(len) => len,
RelBase::Inherit | RelBase::WithRegion => parent_rel_base[axis],
};
let len = declared[axis] let len = declared[axis]
.map(|len| len.within_len(base)) .map(|len| len.within_len(base))
.unwrap_or(base); .unwrap_or(base);
+11 -1
View File
@@ -1,5 +1,5 @@
use crate::util::impl_axis_index; use crate::util::impl_axis_index;
use crate::{Axis, AxisAlign, Len, PrimitiveHandle, RegionAlign, UiRegion, UiSpan}; use crate::{Axis, AxisAlign, Len, PrimitiveHandle, RegionAlign, UiRegion, UiSpan, UiVec2};
/// How a child's region along one axis comes from the region of the widget /// How a child's region along one axis comes from the region of the widget
/// asking, and what its fractions are of. /// asking, and what its fractions are of.
@@ -123,6 +123,16 @@ impl PlaceDesc {
self self
} }
/// What a child's fractions on one axis are of, as a length of the
/// window: a length this place names, or the rel base of the widget
/// giving it, which is `parent_rel_base`.
pub(super) fn base(&self, axis: Axis, parent_rel_base: UiVec2) -> Len {
match self[axis].rel_base {
RelBase::Len(len) => len,
RelBase::Inherit | RelBase::WithRegion => parent_rel_base[axis],
}
}
/// The box each axis names, in the coordinates `own` is in. /// The box each axis names, in the coordinates `own` is in.
pub fn of(self, own: UiRegion, align: RegionAlign) -> UiRegion { pub fn of(self, own: UiRegion, align: RegionAlign) -> UiRegion {
UiRegion::new(self.x.of(own.x, align.x), self.y.of(own.y, align.y)) UiRegion::new(self.x.of(own.x, align.x), self.y.of(own.y, align.y))
+1 -1
View File
@@ -35,7 +35,7 @@ impl<T, I: IdNum> Arena<T, I> {
self.data[i] self.data[i]
} }
pub(crate) fn get_mut(&mut self, id: Id<I>) -> &mut T { pub fn get_mut(&mut self, id: Id<I>) -> &mut T {
&mut self.data[id.idx()] &mut self.data[id.idx()]
} }
} }
+11 -5
View File
@@ -30,6 +30,14 @@ impl Widgets {
!self.needs_redraw.is_empty() !self.needs_redraw.is_empty()
} }
/// Marks this widget for the next frame to draw again, with nothing about
/// it changed. Taking a widget mutably marks it too, which is the ordinary
/// content-change signal; this is for a change the borrow cannot express,
/// and for asking for the same tree over again.
pub fn mark_for_redraw(&mut self, id: impl IdLike) {
self.needs_redraw.insert(id.id());
}
pub fn get_dyn(&self, id: WidgetId) -> Option<&dyn Widget> { pub fn get_dyn(&self, id: WidgetId) -> Option<&dyn Widget> {
Some(self.vec.get(id)?.widget.as_ref()) Some(self.vec.get(id)?.widget.as_ref())
} }
@@ -41,14 +49,14 @@ impl Widgets {
/// get_dyn but dynamic borrow checking of widgets /// get_dyn but dynamic borrow checking of widgets
/// lets you do recursive (tree) operations, like the painter does /// lets you do recursive (tree) operations, like the painter does
pub(crate) fn get_dyn_dynamic<'a>(&self, id: WidgetId) -> WidgetWrapper<'a> { pub(crate) fn get_dyn_dynamic<'a>(&self, id: WidgetId) -> DynBorrower<'a, dyn Widget> {
// SAFETY: must guarantee no other mutable references to this widget exist // SAFETY: must guarantee no other mutable references to this widget exist
// done through the borrow variable // done through the borrow variable
let data = unsafe { forget_mut(to_mut(self.vec.get(id).unwrap())) }; let data = unsafe { forget_mut(to_mut(self.vec.get(id).unwrap())) };
if data.borrowed { if data.borrowed {
panic!("tried to mutably borrow the same widget twice"); panic!("tried to mutably borrow the same widget twice");
} }
WidgetWrapper::new(data.widget.as_mut(), &mut data.borrowed) DynBorrower::new(data.widget.as_mut(), &mut data.borrowed)
} }
pub fn get<I: IdLike>(&self, id: &I) -> Option<&I::Widget> pub fn get<I: IdLike>(&self, id: &I) -> Option<&I::Widget>
@@ -154,7 +162,7 @@ impl Widgets {
self.needs_redraw.insert(id); self.needs_redraw.insert(id);
} }
/// Both axes at once, for a caller holding a pair. /// Both axes at once.
pub fn set_size_rules( pub fn set_size_rules(
&mut self, &mut self,
id: impl IdLike, id: impl IdLike,
@@ -188,8 +196,6 @@ impl Default for Widgets {
} }
} }
pub type WidgetWrapper<'a> = DynBorrower<'a, dyn Widget>;
impl<I: IdLike> std::ops::Index<I> for Widgets impl<I: IdLike> std::ops::Index<I> for Widgets
where where
I::Widget: Sized + Widget, I::Widget: Sized + Widget,
+11 -6
View File
@@ -106,11 +106,16 @@ export WAYLAND_DISPLAY
echo "run-headless: $WAYLAND_DISPLAY (sway $(swaymsg -t get_version --raw | sed -n 's/.*"human_readable":"\([^"]*\)".*/\1/p'))" >&2 echo "run-headless: $WAYLAND_DISPLAY (sway $(swaymsg -t get_version --raw | sed -n 's/.*"human_readable":"\([^"]*\)".*/\1/p'))" >&2
swaymsg output HEADLESS-1 mode "$mode" >/dev/null # The extent `replay-touch` positions against, so a script's coordinates are
# The extent `replay-touch` positions against, so a script's coordinates # the output's own pixels. Set beside every mode change, since a gesture
# are the output's own pixels. # scaled against a mode the output no longer has lands somewhere else and
out_w=${mode%x*} # still looks like a run that worked.
out_h=${mode#*x}; out_h=${out_h%@*} set_mode() {
swaymsg output HEADLESS-1 mode "$1" >/dev/null
out_w=${1%x*}
out_h=${1#*x}; out_h=${out_h%@*}
}
set_mode "$mode"
# Built before the app starts, so a compile error is not reported as a # Built before the app starts, so a compile error is not reported as a
# window that failed to move. # window that failed to move.
@@ -149,7 +154,7 @@ while [ $i -lt "$((seconds * 2))" ]; do
done done
if [ -n "$resize" ] && kill -0 "$pid" 2>/dev/null; then if [ -n "$resize" ] && kill -0 "$pid" 2>/dev/null; then
swaymsg output HEADLESS-1 mode "$resize" >/dev/null set_mode "$resize"
echo "run-headless: resized to $resize" >&2 echo "run-headless: resized to $resize" >&2
sleep 2 sleep 2
fi fi
Binary file not shown.

After

Width:  |  Height:  |  Size: 191 B

+4 -3
View File
@@ -15,9 +15,10 @@ where
let region = ctx.data.render.window_region(&id).unwrap(); let region = ctx.data.render.window_region(&id).unwrap();
let id_pos = region.top_left; let id_pos = region.top_left;
let container_pos = ctx.data.render.window_region(&container).unwrap().top_left; let container_pos = ctx.data.render.window_region(&container).unwrap().top_left;
// The pointer arrives from the platform in floats; everything // The two regions are on the grid and the pointer is not, so the
// it is compared against is on the grid. // step between them is taken there and the pointer keeps the
let pos = (PxVec2::from_f32(ctx.data.pos) + container_pos - id_pos).to_f32(); // precision the platform gave it.
let pos = ctx.data.pos + (container_pos - id_pos).to_f32();
let size = region.size().to_f32(); let size = region.size().to_f32();
select( select(
rsc, rsc,
+54 -29
View File
@@ -8,23 +8,16 @@
use crate::prelude::*; use crate::prelude::*;
use std::collections::HashMap; use std::collections::HashMap;
/// The declared lengths of one widget carrying a size rule, by axis.
pub type Lens = [Option<LayoutLen>; 2];
/// Where one widget carrying an alignment sits, by axis. `None` uses the
/// centered default.
pub type Aligns = [Option<AxisAlign>; 2];
/// What a test changes between two trees grown from the same seed, so the /// 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. /// warm one can be mutated and the cold one grown that way to begin with.
#[derive(Default)] #[derive(Default)]
pub struct Edits { pub struct Edits {
/// Declared sizes, by the order the rules were put on. /// Declared sizes, by the order the rules were put on.
pub sizes: HashMap<usize, Lens>, pub sizes: HashMap<usize, SizeRules>,
/// Which children a span has, by the order the spans were made. /// Which children a span has, by the order the spans were made.
pub spans: HashMap<usize, SpanEdit>, pub spans: HashMap<usize, SpanEdit>,
/// Alignments, by the order they were put on. /// Alignments, by the order they were put on.
pub aligns: HashMap<usize, Aligns>, pub aligns: HashMap<usize, Align>,
/// Which widgets own a movable region, by the order they were offered /// Which widgets own a movable region, by the order they were offered
/// one. Region nodes change what a move writes and how deep a primitive's /// 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. /// chain is, so a tree that never grows one leaves both untested.
@@ -176,9 +169,11 @@ pub struct Plan {
/// it one and the offer is taken or declined; a second offer to the same /// 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 /// widget is dropped, because two rules on one widget would settle in the
/// order they were applied rather than in grow order. /// order they were applied rather than in grow order.
pub size: Option<Lens>, pub size: Option<SizeRules>,
/// The alignment it carries, under the same one-offer rule. /// The alignment it carries, under the same one-offer rule. An axis left
pub align: Option<Aligns>, /// out takes the centered default, which is what [`RegionAlign`] reads it
/// as.
pub align: Option<Align>,
/// Whether it was offered a movable region of its own and what it /// 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 /// answered. `Some(false)` is an offer declined, which still uses up the
/// one offer, where `None` is an offer never made. /// one offer, where `None` is an offer never made.
@@ -195,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 {
@@ -448,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,
}), }),
@@ -632,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;
@@ -651,7 +652,7 @@ impl Sow<'_> {
} }
} }
fn align(&mut self) -> Aligns { fn align(&mut self) -> Align {
let axis = |s: &mut Self| match s.rng.below(4) { let axis = |s: &mut Self| match s.rng.below(4) {
0 => None, 0 => None,
1 => Some(AxisAlign::NEG), 1 => Some(AxisAlign::NEG),
@@ -661,15 +662,21 @@ impl Sow<'_> {
let (x, y) = (axis(self), axis(self)); let (x, y) = (axis(self), axis(self));
// Aligning on neither axis leaves the branch unexercised. // Aligning on neither axis leaves the branch unexercised.
match x.is_none() && y.is_none() { match x.is_none() && y.is_none() {
true => [Some(AxisAlign::CENTER), y], true => Align {
false => [x, y], x: Some(AxisAlign::CENTER),
y,
},
false => Align { x, y },
} }
} }
/// A declared size over half the tree, kept where a test can change it. /// A declared size over half the tree, kept where a test can change it.
fn sized(&mut self, inner: &mut Plan) { fn sized(&mut self, inner: &mut Plan) {
let take = self.rng.chance(); let take = self.rng.chance();
let lens = [self.len(), self.len()]; let lens = SizeRules {
x: self.len().into(),
y: self.len().into(),
};
if !take || inner.size.is_some() { if !take || inner.size.is_some() {
return; return;
} }
@@ -773,10 +780,6 @@ impl Sow<'_> {
self.spans += 1; self.spans += 1;
let edit = self.edits.spans.get(&idx).cloned().unwrap_or_default(); let edit = self.edits.spans.get(&idx).cloned().unwrap_or_default();
let dir = self.rng.below(4); 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 gap = self.rng.below(3) as i32 * 4;
let grown: Vec<usize> = (0..children.len()).collect(); let grown: Vec<usize> = (0..children.len()).collect();
let order = span_edited(&grown, children.len(), spares.len(), &edit); let order = span_edited(&grown, children.len(), spares.len(), &edit);
@@ -796,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)
@@ -804,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> {
@@ -811,16 +819,14 @@ impl<Rsc: UiRsc + 'static> Build<'_, Rsc> {
let built = self.kind(&plan.kind); let built = self.kind(&plan.kind);
let id = built.id(); let id = built.id();
if let Some(lens) = plan.size { if let Some(lens) = plan.size {
self.rsc self.rsc.ui_mut().widgets.set_size_rules(id, lens.x, lens.y);
.ui_mut()
.widgets
.set_size_rules(id, lens[0], lens[1]);
self.tree.sized.push(id); self.tree.sized.push(id);
} }
if let Some(align) = plan.align { if let Some(align) = plan.align {
let resolved = RegionAlign::from(align);
let widgets = &mut self.rsc.ui_mut().widgets; let widgets = &mut self.rsc.ui_mut().widgets;
for (axis, align) in [Axis::X, Axis::Y].into_iter().zip(align) { for axis in Axis::BOTH {
widgets.set_alignment(id, axis, align.unwrap_or_default()); widgets.set_alignment(id, axis, resolved[axis]);
} }
self.tree.aligned.push(id); self.tree.aligned.push(id);
} }
@@ -831,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),
@@ -839,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);
@@ -915,6 +936,10 @@ impl<Rsc: UiRsc + 'static> Build<'_, Rsc> {
gap: Px::from_int(*gap), gap: Px::from_int(*gap),
} }
.add(self.rsc); .add(self.rsc);
// A row takes the height it is given rather than its tallest
// child, which is a rule beside the span rather than anything
// it draws. Derived from `dir` rather than stored, so a plan
// that says the direction says this too.
if dir.axis == Axis::X { if dir.axis == Axis::X {
self.rsc self.rsc
.widgets_mut() .widgets_mut()
+9
View File
@@ -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 {
+23 -22
View File
@@ -24,36 +24,37 @@ impl Widget for Scroll {
self.amt = self.content_len - self.container_len; self.amt = self.content_len - self.container_len;
} }
self.update_amt(); self.update_amt();
let align = painter.alignment()[self.axis]; // Reading the box in pixels above holds this drawing to that one
// Content of a fixed length that fits sits at the start of any box it // length, so these two say where it holds more widely.
// fits in -- but only anchored there. Anywhere else it is a part of //
// the room left over, so it moves with every length the box takes and // Content of a fixed length that fits is handed the whole box below,
// the drawing holds for that length alone. One scrolled part way sits // and nothing here reads the box again, so every longer box gives the
// where it is until the box shrinks past what is left of it. Kept to // same drawing: it holds from the length the content needs upwards,
// the end, it moves with every length. // and shrinking past that is what changes it. Where it sits in a box
// longer than itself is not this widget's to say -- placing its
// answer in the whole box is its own alignment, and that placement is
// a fraction of the box, so it holds at every length too.
//
// One scrolled part way sits where it is until the box shrinks past
// what is left of it. Kept to the end, it moves with every length.
let answer_is_px = answer_len.is_px(); let answer_is_px = answer_len.is_px();
if answer_is_px && self.content_len <= self.container_len && align == AxisAlign::NEG { if answer_is_px && self.content_len <= self.container_len {
painter.holds(self.axis, answer_px..=Px::MAX); painter.holds(self.axis, answer_px..=Px::MAX);
} else if answer_is_px && !self.snap_end { } else if answer_is_px && !self.snap_end {
let left = self.content_len - self.amt; let left = self.content_len - self.amt;
painter.holds(self.axis, Px::MIN..=left); painter.holds(self.axis, Px::MIN..=left);
} }
// Content shorter than the viewport has room to sit in, and where it // Content that fills the viewport is the viewport, and is handed back
// sits is this widget's own alignment -- the same property that would // as it came -- it has nothing to scroll through, so the clamp above
// have placed the whole scroll in a box longer than it. // has already put `amt` at zero. Writing the same box as its own
let slack = (self.container_len - self.content_len).max(Px::ZERO); // length in pixels is the same box in another form, and the two do
let anchor = slack.mul(align.rel()); // not round alike: a part centred in `rel 1` lands a step from one
// Content that fills the viewport and has not been scrolled is the // centred in `px 900`, since halving a difference is not halving each
// viewport, and is handed back as it came. Writing the same box as // part of it.
// its own length in pixels is the same box in another form, and the let content = match self.content_len > self.container_len {
// two do not round alike: a part centred in `rel 1` lands a step from
// one centred in `px 900`, since halving a difference is not halving
// each part of it.
let moved = anchor != Px::ZERO || self.amt != Px::ZERO;
let content = match moved || self.content_len != self.container_len {
true => { true => {
let start = Len::from_parts(Rel::ZERO, anchor - self.amt); let start = Len::from_parts(Rel::ZERO, -self.amt);
UiSpan::new(start, start.offset(self.content_len)).shifted_desc() UiSpan::new(start, start.offset(self.content_len)).shifted_desc()
} }
false => PlaceDescAxis::WHOLE, false => PlaceDescAxis::WHOLE,
+7 -17
View File
@@ -52,25 +52,15 @@ impl Widget for Span {
// What is left for the shares to divide: the row less everything // What is left for the shares to divide: the row less everything
// fixed, as a length of the rel base rather than a number of pixels. // fixed, as a length of the rel base rather than a number of pixels.
let room = row - total.without_leftover(); let all_fixed = total.without_leftover();
// Whether anything is left over is a question in pixels: `rel(0.5)` let room = row - all_fixed;
// beside 300 px is full at 600 and overfull at 400. Asked of `room` // The three cases a rounded division needed -- the fixed parts
// itself, and answered back through the same expression, so the
// boundary is the drawing's own and not a second way of finding it:
// the three cases a rounded division needed -- the fixed parts
// growing slower than the box, faster, or exactly with it -- are the // growing slower than the box, faster, or exactly with it -- are the
// sign of `room.rel`, which `through` already reads. What the // sign of `room.rel`, which the range `longer_than` keeps already
// generated oracle checks is the consequence, since which children // reads. What the generated oracle checks is the consequence, since
// exist at all turns on this. // which children exist at all turns on this.
let any_leftover = total.leftover > Weight::ZERO; let any_leftover = total.leftover > Weight::ZERO;
let has_room = any_leftover && painter.to_px(room, axis) > Px::ZERO; let has_room = any_leftover && painter.longer_than(row, all_fixed, axis);
if any_leftover {
let holds = match has_room {
true => Holds::from(Px::STEP..=Px::MAX),
false => Holds::from(Px::MIN..=Px::ZERO),
};
painter.window_holds(axis, holds.through(room));
}
// Across itself a span is as long as its longest child -- unless a // Across itself a span is as long as its longest child -- unless a
// rule beside it gives that length outright, and then reading them // rule beside it gives that length outright, and then reading them
+4 -6
View File
@@ -321,12 +321,10 @@ impl<'a> TextEditCtx<'a> {
let old = (self.text.view.buf.text().to_string(), self.text.selection); let old = (self.text.view.buf.text().to_string(), self.text.selection);
let mut undo = false; let mut undo = false;
let res = self.apply_event_inner(event, modifiers, &mut undo); let res = self.apply_event_inner(event, modifiers, &mut undo);
if undo { if undo && let Some((old, selection)) = self.text.history.pop() {
if let Some((old, selection)) = self.text.history.pop() { self.set(&old);
self.set(&old); self.text.selection = selection;
self.text.selection = selection; self.clamp_selection_to_layout();
self.clamp_selection_to_layout();
}
} else if self.text.view.buf.text() != old.0 { } else if self.text.view.buf.text() != old.0 {
self.text.history.push(old); self.text.history.push(old);
} }
+2 -2
View File
@@ -19,8 +19,8 @@ widget_trait! {
move |state| { move |state| {
let id = self.add(state); let id = self.add(state);
let widgets = &mut state.ui_mut().widgets; let widgets = &mut state.ui_mut().widgets;
for (axis, align) in [(Axis::X, align.x), (Axis::Y, align.y)] { for axis in Axis::BOTH {
if let Some(align) = align { if let Some(align) = align[axis] {
widgets.set_alignment(id, axis, align); widgets.set_alignment(id, axis, align);
} }
} }
+2 -11
View File
@@ -9,6 +9,7 @@ use std::marker::Unsize;
/// ///
/// Its child is optional so it can also be the swappable slot a tab bar /// Its child is optional so it can also be the swappable slot a tab bar
/// needs, which is what it was written for. /// needs, which is what it was written for.
#[derive(Default)]
pub struct Wrapper { pub struct Wrapper {
pub inner: Option<StrongWidget>, pub inner: Option<StrongWidget>,
} }
@@ -26,11 +27,7 @@ impl Wrapper {
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self::default()
} }
pub fn empty() -> Self {
Self {
inner: Default::default(),
}
}
pub fn set<W: ?Sized + Unsize<dyn Widget>>(&mut self, to: StrongWidget<W>) { pub fn set<W: ?Sized + Unsize<dyn Widget>>(&mut self, to: StrongWidget<W>) {
self.inner = Some(to) self.inner = Some(to)
} }
@@ -42,9 +39,3 @@ impl Wrapper {
self.inner.replace(to) self.inner.replace(to)
} }
} }
impl Default for Wrapper {
fn default() -> Self {
Self::empty()
}
}
+3 -3
View File
@@ -69,8 +69,8 @@ fn a_branch_taken_on_a_measurement_holds_across_repaints() {
assert_ne!(first, (false, false), "threshold {threshold}: neither drew"); assert_ne!(first, (false, false), "threshold {threshold}: neither drew");
for frame in 0..4 { for frame in 0..4 {
h.rsc.widgets_mut().get_dyn_mut(wide); h.rsc.widgets_mut().mark_for_redraw(wide);
h.rsc.widgets_mut().get_dyn_mut(narrow); h.rsc.widgets_mut().mark_for_redraw(narrow);
h.frame(); h.frame();
assert_eq!( assert_eq!(
taken(&h, wide, narrow), taken(&h, wide, narrow),
@@ -88,7 +88,7 @@ fn a_branch_taken_on_a_measurement_is_the_one_a_cold_start_takes() {
let (wide, narrow) = plant(&mut warm, threshold); let (wide, narrow) = plant(&mut warm, threshold);
warm.resize((640, 480)); warm.resize((640, 480));
warm.frame(); warm.frame();
warm.rsc.widgets_mut().get_dyn_mut(wide); warm.rsc.widgets_mut().mark_for_redraw(wide);
warm.frame(); warm.frame();
let mut cold = Harness::new((640, 480)); let mut cold = Harness::new((640, 480));
+1 -1
View File
@@ -18,7 +18,7 @@ fn a_wrapping_text_in_a_span_settles_on_one_width() {
let r = h.region(&t.id()).unwrap(); let r = h.region(&t.id()).unwrap();
widths.push(r.bot_right.x - r.top_left.x); widths.push(r.bot_right.x - r.top_left.x);
// Redrawing it changes nothing about the state, so nothing may move. // Redrawing it changes nothing about the state, so nothing may move.
h.rsc.widgets_mut().get_dyn_mut(t.id()); h.rsc.widgets_mut().mark_for_redraw(t.id());
h.frame(); h.frame();
} }
println!("widths over six frames: {widths:?}"); println!("widths over six frames: {widths:?}");
+101
View File
@@ -1,5 +1,7 @@
//! Where a frame puts things, with no window to put them in. //! Where a frame puts things, with no window to put them in.
use std::{cell::Cell, rc::Rc};
use iris::harness::{Harness, assert_corners}; use iris::harness::{Harness, assert_corners};
use iris::prelude::*; use iris::prelude::*;
@@ -219,6 +221,105 @@ fn an_empty_widget_takes_a_share_of_a_span() {
assert_corners!(h, right, (300, 0), (400, 200)); assert_corners!(h, right, (300, 0), (400, 200));
} }
/// A widget with a natural pixel size, like an image, which records the box
/// it was asked in so a test can see which length decided it.
struct NaturalSize {
len: f32,
asked: Rc<Cell<f32>>,
}
impl Widget for NaturalSize {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.asked.set(painter.px_len(Axis::X).to_f32());
Size::px(Vec2::new(self.len, self.len))
}
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
Some(LayoutLen::px(self.len))
}
}
/// A rule wins over what the widget says about itself, and a share is a rule:
/// it is a length only to whoever divides one, and nobody here does, so the
/// widget is asked in the whole box rather than in the size it asked for.
#[test]
fn a_share_rule_beats_the_widgets_own_pixel_size() {
let mut h = Harness::new((400, 200));
let asked = Rc::new(Cell::new(0.0));
let natural = NaturalSize {
len: 50.0,
asked: asked.clone(),
}
.add(&mut h.rsc);
h.set_root(natural.wrapper());
assert_eq!(asked.get(), 50.0, "its hint gives it its own size");
h.set_len(natural, Axis::X, LayoutLen::LEFTOVER);
h.frame();
assert_eq!(asked.get(), 400.0, "the share is all of the box");
}
/// A share with pixels or a fraction beside it is the longer of the two: it
/// fills what they leave of the box and overflows the box where they are
/// longer than it. A parent that divides nothing gives the same length as a
/// span with one child, because in both there is nobody else to divide with.
#[test]
fn a_share_is_a_minimum_wherever_nothing_divides_it() {
let asked = |rule: LayoutLen, in_a_span: bool| {
let mut h = Harness::new((400, 200));
let probe = rect(Color::RED).add(&mut h.rsc);
h.set_len(probe, Axis::X, rule);
match in_a_span {
true => h.set_root((probe,).span(Dir::RIGHT)),
false => h.set_root(probe.wrapper()),
}
h.region(&probe).unwrap().size().x
};
for (rule, want) in [
(LayoutLen::LEFTOVER, 400),
(LayoutLen::px(50) + LayoutLen::LEFTOVER, 400),
(LayoutLen::px(500) + LayoutLen::LEFTOVER, 500),
(LayoutLen::rel(0.5) + LayoutLen::LEFTOVER, 400),
(LayoutLen::px(500), 500),
] {
let want = Px::from_int(want);
assert_eq!(asked(rule, false), want, "{rule:?} where nothing divides");
assert_eq!(asked(rule, true), want, "{rule:?} in a span");
}
}
/// Which of the two is longer is a question in pixels, so the box is decided
/// again wherever the answer can change: a window that crosses the length the
/// pixels ask for, and the rule itself crossing it while the window holds
/// still. The first is a range the drawing holds for; the second cannot be
/// seen in what the widget declares, since a share declares nothing either
/// way, so it reaches the parent as a length only the parent can resolve.
#[test]
fn a_share_past_the_box_is_decided_again_on_either_side_of_the_crossing() {
let mut h = Harness::new((400, 200));
let probe = rect(Color::RED).add(&mut h.rsc);
h.set_len(probe, Axis::X, LayoutLen::px(500) + LayoutLen::LEFTOVER);
h.set_root(probe.wrapper());
assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(500));
h.resize((900, 200));
h.frame();
assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(900));
h.resize((400, 200));
h.frame();
assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(500));
h.set_len(probe, Axis::X, LayoutLen::px(50) + LayoutLen::LEFTOVER);
h.frame();
assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(400));
h.set_len(probe, Axis::X, LayoutLen::px(500) + LayoutLen::LEFTOVER);
h.frame();
assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(500));
}
#[test] #[test]
fn a_child_drawn_twice_moves_once() { fn a_child_drawn_twice_moves_once() {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
+44 -9
View File
@@ -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 {
@@ -25,11 +27,27 @@ fn some_edits(seed: u64, of: &Plan) -> Edits {
Edits { Edits {
sizes: pick(sized, &mut rng) sizes: pick(sized, &mut rng)
.into_iter() .into_iter()
.map(|i| (i, [Some(LayoutLen::LEFTOVER), None])) .map(|i| {
(
i,
SizeRules {
x: SizeRule::Exact(LayoutLen::LEFTOVER),
y: SizeRule::Free,
},
)
})
.collect(), .collect(),
aligns: pick(aligned, &mut rng) aligns: pick(aligned, &mut rng)
.into_iter() .into_iter()
.map(|i| (i, [Some(AxisAlign::POS), None])) .map(|i| {
(
i,
Align {
x: Some(AxisAlign::POS),
y: None,
},
)
})
.collect(), .collect(),
nodes: pick(nodes, &mut rng) nodes: pick(nodes, &mut rng)
.into_iter() .into_iter()
@@ -51,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
@@ -70,11 +86,13 @@ fn editing_a_plan_is_growing_one_with_those_edits() {
} }
} }
/// Every simplification is strictly smaller, so taking them in turn reaches a /// No simplification is larger, which is the half of "the shrinker stops" a
/// fixed point instead of circling. A shrinker that can return to a tree it /// widget count can see. Most are not smaller either -- a dropped alignment
/// has already tried does not stop. /// and a simpler leaf both keep the count -- so what rules out circling is
/// that those are one-way too: a `Some` becomes a `None`, and a kind steps
/// down a ladder with no way back up.
#[test] #[test]
fn every_simplification_of_a_plan_is_smaller_than_it() { fn no_simplification_of_a_plan_is_larger_than_it() {
for seed in 1..=60 { for seed in 1..=60 {
let tree = plan(seed, 4, &Edits::default()); let tree = plan(seed, 4, &Edits::default());
let mut queue = vec![tree]; let mut queue = vec![tree];
@@ -119,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);
}
+29 -39
View File
@@ -45,7 +45,6 @@ fn counted(h: &mut Harness, size: Size, reads_box: bool) -> (WeakWidget<Counted>
struct Layered { struct Layered {
children: [StrongWidget<Rect>; 2], children: [StrongWidget<Rect>; 2],
_revision: usize,
} }
impl Widget for Layered { impl Widget for Layered {
@@ -65,14 +64,10 @@ fn a_redrawn_layered_widget_keeps_the_layer_it_was_entered_on() {
rect(Color::RED).add_strong(&mut h.rsc), rect(Color::RED).add_strong(&mut h.rsc),
rect(Color::BLUE).add_strong(&mut h.rsc), rect(Color::BLUE).add_strong(&mut h.rsc),
]; ];
let root = Layered { let root = Layered { children }.add(&mut h.rsc);
children,
_revision: 0,
}
.add(&mut h.rsc);
h.set_root(root); h.set_root(root);
h.rsc[root]._revision += 1; h.rsc.widgets_mut().mark_for_redraw(root.id());
h.frame(); h.frame();
let label = h.rsc.widgets().label(root.id()); let label = h.rsc.widgets().label(root.id());
@@ -181,9 +176,9 @@ fn a_repaint_that_keeps_its_size_does_not_relay_out() {
h.set_root((first, second).span(Dir::RIGHT)); h.set_root((first, second).span(Dir::RIGHT));
let settled = draws.get(); let settled = draws.get();
// Taking mutable access is the ordinary content-change signal. This // Marked with nothing about it changed, and it reports the same size
// widget returns the same size, so the parent has nothing to lay out. // either way, so the parent has nothing to lay out.
let _ = h.rsc.widgets_mut().get_dyn_mut(first.id()); h.rsc.widgets_mut().mark_for_redraw(first.id());
h.frame(); h.frame();
assert_eq!(draws.get(), settled + 1); assert_eq!(draws.get(), settled + 1);
@@ -198,7 +193,7 @@ fn a_span_child_survives_the_next_frame() {
let bottom = rect(Color::BLUE).height(120).add(&mut h.rsc); let bottom = rect(Color::BLUE).height(120).add(&mut h.rsc);
h.set_root((top, bottom).span(Dir::DOWN)); h.set_root((top, bottom).span(Dir::DOWN));
h.rsc.widgets_mut().get_dyn_mut(top.id()); h.rsc.widgets_mut().mark_for_redraw(top.id());
h.frame(); h.frame();
assert_corners!(h, top, (0, 0), (400, 80)); assert_corners!(h, top, (0, 0), (400, 80));
@@ -660,7 +655,7 @@ fn a_masked_widget_redrawn_on_its_own_sets_its_mask_again() {
let masked = inner.masked().add(&mut h.rsc); let masked = inner.masked().add(&mut h.rsc);
let other = rect(Color::RED).width(100).add(&mut h.rsc); let other = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((other, masked).span(Dir::RIGHT)); h.set_root((other, masked).span(Dir::RIGHT));
h.rsc.widgets_mut().get_dyn_mut(masked.id()); h.rsc.widgets_mut().mark_for_redraw(masked.id());
h.frame(); h.frame();
assert_corners!(h, inner, (100, 0), (400, 200)); assert_corners!(h, inner, (100, 0), (400, 200));
} }
@@ -774,6 +769,17 @@ fn a_subtree_that_changed_parents_settles_at_the_depth_it_moved_to() {
); );
} }
/// Where a mask slot clips, in window pixels: the region it holds, carried
/// through whatever move entry it hangs from. Taken by slot rather than by
/// widget, so a test can name the slot it expects a redraw to keep.
fn mask_bounds(h: &Harness, mask: MaskIdx) -> PixelRegion {
let mask = &h.rsc.ui().masks[mask.idx()];
h.render
.moves
.resolve(mask.move_idx, mask.region)
.to_px(h.render.output_size())
}
fn primitive_bounds(h: &Harness, id: WidgetId) -> Vec<PixelRegion> { fn primitive_bounds(h: &Harness, id: WidgetId) -> Vec<PixelRegion> {
h.render.active[&id] h.render.active[&id]
.primitives .primitives
@@ -919,15 +925,10 @@ fn resizing_a_fixed_frame_recomposes_its_contents_without_drawing_them() {
primitive_bounds(&warm, leaf.id()), primitive_bounds(&warm, leaf.id()),
primitive_bounds(&cold, other.id()) primitive_bounds(&cold, other.id())
); );
let mask = |h: &Harness, id: WidgetId| { assert_eq!(
let active = &h.render.active[&id]; mask_bounds(&warm, warm.render.active[&leaf.id()].mask),
let mask = &h.rsc.ui().masks[active.mask.idx()]; mask_bounds(&cold, cold.render.active[&other.id()].mask)
h.render );
.moves
.resolve(mask.move_idx, mask.region)
.to_px(h.render.output_size())
};
assert_eq!(mask(&warm, leaf.id()), mask(&cold, other.id()));
} }
} }
@@ -994,7 +995,7 @@ fn glyph_origins_compose_identically_when_drawn_and_when_retained() {
assert_eq!(draws.get(), before); assert_eq!(draws.get(), before);
let retained = primitive_bounds(&h, text.id()); let retained = primitive_bounds(&h, text.id());
assert!(!retained.is_empty()); assert!(!retained.is_empty());
let _ = h.rsc.widgets_mut().get_dyn_mut(text.id()); h.rsc.widgets_mut().mark_for_redraw(text.id());
h.frame(); h.frame();
assert!(draws.get() > before); assert!(draws.get() > before);
assert_eq!(retained, primitive_bounds(&h, text.id())); assert_eq!(retained, primitive_bounds(&h, text.id()));
@@ -1184,15 +1185,10 @@ fn padding_and_stack_boxes_follow_the_region_without_drawing_again() {
assert_eq!(warm.region(&a), cold.region(&b)); assert_eq!(warm.region(&a), cold.region(&b));
assert_eq!(primitive_bounds(&warm, a), primitive_bounds(&cold, b)); assert_eq!(primitive_bounds(&warm, a), primitive_bounds(&cold, b));
} }
let mask = |h: &Harness, id: WidgetId| { assert_eq!(
let active = &h.render.active[&id]; mask_bounds(&warm, warm.render.active[&leaf.id()].mask),
let mask = &h.rsc.ui().masks[active.mask.idx()]; mask_bounds(&cold, cold.render.active[&other.id()].mask)
h.render );
.moves
.resolve(mask.move_idx, mask.region)
.to_px(h.render.output_size())
};
assert_eq!(mask(&warm, leaf.id()), mask(&cold, other.id()));
} }
} }
} }
@@ -1374,20 +1370,14 @@ fn a_redrawn_mask_keeps_reused_primitives_clipped_when_it_moves() {
h.set_root((first, masked).span(Dir::DOWN)); h.set_root((first, masked).span(Dir::DOWN));
let mask = h.render.active[&masked.id()].mask; let mask = h.render.active[&masked.id()].mask;
let settled = draws.get(); let settled = draws.get();
h.rsc.widgets_mut().get_dyn_mut(masked.id()); h.rsc.widgets_mut().mark_for_redraw(masked.id());
h.frame(); h.frame();
assert_eq!(primitive_masks(&h, inner.id()), vec![mask]); assert_eq!(primitive_masks(&h, inner.id()), vec![mask]);
assert_eq!(draws.get(), settled, "a mask repaint must reuse its child"); assert_eq!(draws.get(), settled, "a mask repaint must reuse its child");
assert_eq!(h.render.active[&masked.id()].mask, mask); assert_eq!(h.render.active[&masked.id()].mask, mask);
h.set_len(first, Axis::Y, 10); h.set_len(first, Axis::Y, 10);
h.frame(); h.frame();
let clip = h.rsc.ui().masks[mask.idx()]; assert_eq!(mask_bounds(&h, mask), h.region(&masked).unwrap());
let clip = h
.render
.moves
.resolve(clip.move_idx, clip.region)
.to_px(h.render.output_size());
assert_eq!(clip, h.region(&masked).unwrap());
assert_corners!(h, inner, (0, 10), (400, 200)); assert_corners!(h, inner, (0, 10), (400, 200));
} }
} }
+2 -2
View File
@@ -113,7 +113,7 @@ fn wrapping_content_beside_a_fixed_length_is_stable_warm_and_cold() {
let mut warm = Harness::new((900, 300)); let mut warm = Harness::new((900, 300));
let (text, content) = plant(&mut warm); let (text, content) = plant(&mut warm);
warm.rsc.widgets_mut().get_dyn_mut(text); warm.rsc.widgets_mut().mark_for_redraw(text);
warm.frame(); warm.frame();
let mut cold = Harness::new((900, 300)); let mut cold = Harness::new((900, 300));
@@ -142,8 +142,8 @@ fn a_clipping_widget_reporting_more_than_its_box_is_caught() {
let mut h = Harness::new((100, 100)); let mut h = Harness::new((100, 100));
let tall = rect(Color::RED).height(400).add_strong(&mut h.rsc); let tall = rect(Color::RED).height(400).add_strong(&mut h.rsc);
let clipper = Clipper(tall).add(&mut h.rsc); let clipper = Clipper(tall).add(&mut h.rsc);
// `set_root` lays the tree out, so this is where it is caught.
h.set_root(clipper); h.set_root(clipper);
h.frame();
} }
/// Content that fits sits in the viewport, not in a box of the window's /// Content that fits sits in the viewport, not in a box of the window's
+59 -102
View File
@@ -9,17 +9,45 @@
//! 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 iris::harness::Harness; use iris::harness::Harness;
use iris::prelude::*; use iris::prelude::*;
use iris::random::Branch; use iris::random::Branch;
/// Every widget in the same place warm as cold, reported all at once: which
/// of a dozen boxes moved is the whole of what a shrunk case has to say.
///
/// A list that names one widget twice is an error rather than a redundant
/// check. `width`, `sized` and `align` give back the widget they were handed,
/// so a fixture built through them can name one text three times, and then a
/// case comparing six boxes compares four and says nothing about it. One
/// fixture builds both lists, so checking the warm one checks both.
#[track_caller]
fn assert_same_regions( fn assert_same_regions(
warm: &Harness, warm: &Harness,
warm_ids: &[WidgetId], warm_ids: &[WidgetId],
cold: &Harness, cold: &Harness,
cold_ids: &[WidgetId], cold_ids: &[WidgetId],
) { ) {
assert_eq!(
warm_ids.len(),
cold_ids.len(),
"the warm and cold fixtures list different widgets"
);
let named: HashSet<&WidgetId> = warm_ids.iter().collect();
assert_eq!(
named.len(),
warm_ids.len(),
"a widget is listed twice: {warm_ids:?}"
);
let mut wrong = Vec::new(); let mut wrong = Vec::new();
for (i, (&w, &c)) in warm_ids.iter().zip(cold_ids).enumerate() { for (i, (&w, &c)) in warm_ids.iter().zip(cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c)); let (got, want) = (warm.region(&w), cold.region(&c));
@@ -92,7 +120,7 @@ fn repainting_a_stack_uses_the_box_its_sizing_child_decided() {
let mut warm = Harness::new((900, 1200)); let mut warm = Harness::new((900, 1200));
let ids = plant_stack_in_its_sizing_childs_box(&mut warm); let ids = plant_stack_in_its_sizing_childs_box(&mut warm);
for &id in &ids { for &id in &ids {
warm.rsc.widgets_mut().get_dyn_mut(id); warm.rsc.widgets_mut().mark_for_redraw(id);
} }
warm.frame(); warm.frame();
@@ -188,36 +216,31 @@ fn reordering_nested_spans_keeps_the_answer_from_the_decided_box() {
assert_same_regions(&warm, &ids, &cold, &cold_ids); assert_same_regions(&warm, &ids, &cold, &cold_ids);
} }
/// Six widgets, shrunk from a 402-widget tree the fuzzer found. Nothing about /// Four 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 /// the tree changes -- every widget is marked for redraw and the frame is
/// taken again -- so no box may move, and a warm frame has to land where a /// taken again -- so no box may move, and a warm frame has to land where a
/// cold one does. /// cold one does.
fn plant(h: &mut Harness) -> Vec<WidgetId> { fn plant(h: &mut Harness) -> Vec<WidgetId> {
let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc); let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc);
let wrapped = wtext("Wrapping shapes").size(16).wrap(true).add(&mut h.rsc); let wrapped = wtext("Wrapping shapes")
let sized = wrapped.width(76).add(&mut h.rsc); .size(16)
let aligned = sized; .wrap(true)
.width(76)
.add(&mut h.rsc);
h.rsc h.rsc
.widgets_mut() .widgets_mut()
.set_alignment(sized, Axis::X, AxisAlign::POS); .set_alignment(wrapped, Axis::X, AxisAlign::POS);
h.rsc h.rsc
.widgets_mut() .widgets_mut()
.set_alignment(sized, Axis::Y, AxisAlign::POS); .set_alignment(wrapped, Axis::Y, AxisAlign::POS);
let stack = Stack { let stack = Stack {
children: vec![plain.add_strong(&mut h.rsc), aligned.add_strong(&mut h.rsc)], children: vec![plain.add_strong(&mut h.rsc), wrapped.add_strong(&mut h.rsc)],
size: StackSize::Child(0), size: StackSize::Child(0),
} }
.add(&mut h.rsc); .add(&mut h.rsc);
let root = (stack,).span(Dir::RIGHT).add(&mut h.rsc); let root = (stack,).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root(root); h.set_root(root);
vec![ vec![plain.id(), wrapped.id(), stack.id(), root.id()]
plain.id(),
wrapped.id(),
sized.id(),
aligned.id(),
stack.id(),
root.id(),
]
} }
/// The first frame does not reach the layout a second one does, so "cold" is /// The first frame does not reach the layout a second one does, so "cold" is
@@ -230,7 +253,7 @@ fn one_frame_is_enough() {
let first = h.region(&ids[1]).unwrap(); let first = h.region(&ids[1]).unwrap();
for _ in 0..3 { for _ in 0..3 {
for &id in &ids { for &id in &ids {
h.rsc.widgets_mut().get_dyn_mut(id); h.rsc.widgets_mut().mark_for_redraw(id);
} }
h.frame(); h.frame();
} }
@@ -252,46 +275,30 @@ fn repainting_everything_moves_nothing() {
let mut warm = Harness::new((640, 900)); let mut warm = Harness::new((640, 900));
let ids = plant(&mut warm); let ids = plant(&mut warm);
for &id in &ids { for &id in &ids {
warm.rsc.widgets_mut().get_dyn_mut(id); warm.rsc.widgets_mut().mark_for_redraw(id);
} }
warm.frame(); warm.frame();
let mut cold = Harness::new((640, 900)); let mut cold = Harness::new((640, 900));
let cold_ids = plant(&mut cold); let cold_ids = plant(&mut cold);
let mut wrong = Vec::new(); assert_same_regions(&warm, &ids, &cold, &cold_ids);
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"));
} }
/// Six widgets, shrunk from 905. Everything inside the declared 189x176 box /// Four widgets, shrunk from 905. Everything inside the declared 189x176 box
/// is the same size whatever the output is, so a resize may not change any of /// is the same size whatever the output is, so a resize may not change any of
/// it -- but the text comes out 3.92px narrower warm than cold. /// it -- but the text comes out 3.92px narrower warm than cold.
fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> { fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> {
let words = "Wrapping shapes one source into as many lines as the box leaves"; let words = "Wrapping shapes one source into as many lines as the box leaves";
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc); let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
let aligned = text;
h.rsc h.rsc
.widgets_mut() .widgets_mut()
.set_alignment(text, Axis::X, AxisAlign::NEG); .set_alignment(text, Axis::X, AxisAlign::NEG);
let inner = (aligned,).span(Dir::RIGHT).add(&mut h.rsc); let inner = (text,).span(Dir::RIGHT).sized((189, 176)).add(&mut h.rsc);
let sized = inner.sized((189, 176)).add(&mut h.rsc);
let filler = rect(Color::RED).add(&mut h.rsc); let filler = rect(Color::RED).add(&mut h.rsc);
let root = (filler, sized).span(Dir::RIGHT).add(&mut h.rsc); let root = (filler, inner).span(Dir::RIGHT).add(&mut h.rsc);
h.state.root = Some(root.add_strong(&mut h.rsc)); h.state.root = Some(root.add_strong(&mut h.rsc));
vec![ vec![text.id(), inner.id(), filler.id(), root.id()]
text.id(),
aligned.id(),
inner.id(),
sized.id(),
filler.id(),
root.id(),
]
} }
#[test] #[test]
@@ -306,17 +313,10 @@ fn a_resize_does_not_reach_inside_a_box_of_declared_pixels() {
let cold_ids = plant_fixed(&mut cold); let cold_ids = plant_fixed(&mut cold);
cold.frame(); cold.frame();
let mut wrong = Vec::new(); assert_same_regions(&warm, &ids, &cold, &cold_ids);
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"));
} }
/// Four widgets, shrunk from 486. A span's two children are swapped: warm by /// Three widgets, shrunk from 486. A span's two children are swapped: warm by
/// moving them, cold by growing them that way. Same widgets, same sizes, one /// moving them, cold by growing them that way. Same widgets, same sizes, one
/// ends up 29.9px from where the other does. /// ends up 29.9px from where the other does.
fn plant_pair(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, WeakWidget<Span>) { fn plant_pair(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, WeakWidget<Span>) {
@@ -340,16 +340,11 @@ fn plant_pair(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, WeakWidget<Span
gap: Px::ZERO, gap: Px::ZERO,
} }
.add(&mut h.rsc); .add(&mut h.rsc);
let span_handle = span;
let aligned = span;
h.rsc h.rsc
.widgets_mut() .widgets_mut()
.set_alignment(span, Axis::X, AxisAlign::CENTER); .set_alignment(span, Axis::X, AxisAlign::CENTER);
h.state.root = Some(aligned.add_strong(&mut h.rsc)); h.state.root = Some(span.add_strong(&mut h.rsc));
( (vec![wrapped.id(), plain.id(), span.id()], span)
vec![wrapped.id(), plain.id(), span.id(), aligned.id()],
span_handle,
)
} }
#[test] #[test]
@@ -364,17 +359,10 @@ fn swapping_two_children_lands_where_growing_them_that_way_does() {
let (cold_ids, _) = plant_pair(&mut cold, true); let (cold_ids, _) = plant_pair(&mut cold, true);
cold.frame(); cold.frame();
let mut wrong = Vec::new(); assert_same_regions(&warm, &ids, &cold, &cold_ids);
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"));
} }
/// Eight widgets, shrunk from 80. The scroll decides how wide to make its /// Seven widgets, shrunk from 80. The scroll decides how wide to make its
/// content from what the content says, and hands that box down through a /// content from what the content says, and hands that box down through a
/// pass-through; the span under it was given that box once, so nothing at its /// pass-through; the span under it was given that box once, so nothing at its
/// own edge says the box was its own answer. /// own edge says the box was its own answer.
@@ -393,8 +381,7 @@ fn plant_scrolled(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget
gap: Px::ZERO, gap: Px::ZERO,
} }
.add(&mut h.rsc); .add(&mut h.rsc);
let block = rect(Color::RED).add(&mut h.rsc); let fixed = rect(Color::RED).width(87).add(&mut h.rsc);
let fixed = block.width(87).add(&mut h.rsc);
let mut outer_children: Vec<StrongWidget> = let mut outer_children: Vec<StrongWidget> =
vec![fixed.add_strong(&mut h.rsc), inner.add_strong(&mut h.rsc)]; vec![fixed.add_strong(&mut h.rsc), inner.add_strong(&mut h.rsc)];
if swapped { if swapped {
@@ -416,7 +403,6 @@ fn plant_scrolled(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget
text.id(), text.id(),
filler.id(), filler.id(),
inner.id(), inner.id(),
block.id(),
fixed.id(), fixed.id(),
outer.id(), outer.id(),
through.id(), through.id(),
@@ -440,14 +426,7 @@ fn a_span_given_the_box_its_answer_decided_matches_a_cold_layout() {
let (cold_ids, _) = plant_scrolled(&mut cold, true); let (cold_ids, _) = plant_scrolled(&mut cold, true);
cold.frame(); cold.frame();
let mut wrong = Vec::new(); assert_same_regions(&warm, &ids, &cold, &cold_ids);
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"));
} }
/// Reports a width derived from the box it is asked in. Reading through the /// Reports a width derived from the box it is asked in. Reading through the
@@ -471,11 +450,10 @@ impl Widget for Wider {
fn plant_wider(h: &mut Harness, extra: f32) -> (WeakWidget<Wider>, WidgetId) { fn plant_wider(h: &mut Harness, extra: f32) -> (WeakWidget<Wider>, WidgetId) {
let content = Wider { extra }.add(&mut h.rsc); let content = Wider { extra }.add(&mut h.rsc);
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc); let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
let root = scroll;
h.rsc h.rsc
.widgets_mut() .widgets_mut()
.set_alignment(scroll, Axis::X, AxisAlign::NEG); .set_alignment(scroll, Axis::X, AxisAlign::NEG);
h.set_root(root); h.set_root(scroll);
(content, scroll.id()) (content, scroll.id())
} }
@@ -567,14 +545,7 @@ fn a_box_that_only_rounds_past_its_fixed_children_leaves_nothing_over() {
let (cold_ids, _) = plant_boundary(&mut cold, true); let (cold_ids, _) = plant_boundary(&mut cold, true);
cold.frame(); cold.frame();
let mut wrong = Vec::new(); assert_same_regions(&warm, &ids, &cold, &cold_ids);
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"));
} }
/// Five widgets, shrunk by `tests/shrink.rs` from the 277 the oracle's seed /// Five widgets, shrunk by `tests/shrink.rs` from the 277 the oracle's seed
@@ -616,20 +587,13 @@ fn plant_nested_scrolls(h: &mut Harness) -> Vec<WidgetId> {
fn redrawing_one_widget_does_not_move_what_scrolls_around_it() { fn redrawing_one_widget_does_not_move_what_scrolls_around_it() {
let mut warm = Harness::new((900, 1200)); let mut warm = Harness::new((900, 1200));
let ids = plant_nested_scrolls(&mut warm); let ids = plant_nested_scrolls(&mut warm);
warm.rsc.widgets_mut().get_dyn_mut(ids[0]); warm.rsc.widgets_mut().mark_for_redraw(ids[0]);
warm.frame(); warm.frame();
let mut cold = Harness::new((900, 1200)); let mut cold = Harness::new((900, 1200));
let cold_ids = plant_nested_scrolls(&mut cold); let cold_ids = plant_nested_scrolls(&mut cold);
let mut wrong = Vec::new(); assert_same_regions(&warm, &ids, &cold, &cold_ids);
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 /// Ten widgets, of the shape `tests/shrink.rs` reduces the oracle's seed 220
@@ -720,14 +684,7 @@ fn a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered() {
let (cold_ids, _) = plant_under_a_node(&mut cold, true); let (cold_ids, _) = plant_under_a_node(&mut cold, true);
cold.frame(); cold.frame();
let mut wrong = Vec::new(); assert_same_regions(&warm, &ids, &cold, &cold_ids);
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 \ const PARAGRAPH: &str = "Wrapping shapes one source into as many lines as the \
+5 -29
View File
@@ -11,8 +11,6 @@
//! The instances are two pixels wide so that vertex work dominates; a chain //! The instances are two pixels wide so that vertex work dominates; a chain
//! walk that does not show up against small quads will not show up against //! walk that does not show up against small quads will not show up against
//! anything. //! anything.
//!
//! The instance is leaked deliberately, for the reason `draw_cost.rs` gives.
use iris::prelude::*; use iris::prelude::*;
use iris_core::{ use iris_core::{
@@ -21,6 +19,9 @@ use iris_core::{
}; };
use wgpu::{Color as GpuColor, *}; use wgpu::{Color as GpuColor, *};
#[path = "gpu/mod.rs"]
mod gpu;
const SIZE: u32 = 1024; const SIZE: u32 = 1024;
const INSTANCES: usize = 200_000; const INSTANCES: usize = 200_000;
const FRAMES: u32 = 20; const FRAMES: u32 = 20;
@@ -29,18 +30,7 @@ const FRAMES: u32 = 20;
const BATCHES: u32 = 8; const BATCHES: u32 = 8;
fn gpu() -> Option<(Device, Queue, f32)> { fn gpu() -> Option<(Device, Queue, f32)> {
let all = Instance::new(InstanceDescriptor::new_without_display_handle()); let adapter = gpu::adapter()?;
let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default()))
{
Ok(_) => all,
Err(_) => Instance::new(InstanceDescriptor {
backends: Backends::GL,
..InstanceDescriptor::new_without_display_handle()
}),
};
let instance: &'static Instance = Box::leak(Box::new(instance));
let adapter =
pollster::block_on(instance.request_adapter(&RequestAdapterOptions::default())).ok()?;
if !adapter.features().contains(Features::TIMESTAMP_QUERY) { if !adapter.features().contains(Features::TIMESTAMP_QUERY) {
println!("no timestamp queries on {:?}", adapter.get_info().name); println!("no timestamp queries on {:?}", adapter.get_info().name);
return None; return None;
@@ -55,20 +45,6 @@ fn gpu() -> Option<(Device, Queue, f32)> {
Some((device, queue, period)) Some((device, queue, period))
} }
fn config(format: TextureFormat) -> SurfaceConfiguration {
SurfaceConfiguration {
usage: TextureUsages::RENDER_ATTACHMENT,
format,
color_space: SurfaceColorSpace::Auto,
width: SIZE,
height: SIZE,
present_mode: PresentMode::Fifo,
desired_maximum_frame_latency: 2,
alpha_mode: CompositeAlphaMode::Auto,
view_formats: vec![],
}
}
/// A chain `depth` slots long, and instances that all resolve through its end. /// A chain `depth` slots long, and instances that all resolve through its end.
fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize) { fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize) {
let kind = ui.primitives.kind::<RectPrimitive>(); let kind = ui.primitives.kind::<RectPrimitive>();
@@ -103,7 +79,7 @@ fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize) {
/// Nanoseconds the pass took on the GPU, best of `BATCHES`. /// Nanoseconds the pass took on the GPU, best of `BATCHES`.
fn pass_cost(device: &Device, queue: &Queue, period: f32, depth: usize) -> f64 { fn pass_cost(device: &Device, queue: &Queue, period: f32, depth: usize) -> f64 {
let format = TextureFormat::Bgra8Unorm; let format = TextureFormat::Bgra8Unorm;
let mut node = UiRenderNode::new(device, &config(format)); let mut node = UiRenderNode::new(device, &gpu::config(format, SIZE));
let mut ui = UiData::default(); let mut ui = UiData::default();
let mut render = UiRenderState::new(); let mut render = UiRenderState::new();
fill(&mut ui, &mut render, depth); fill(&mut ui, &mut render, depth);
+5 -34
View File
@@ -13,10 +13,6 @@
//! That is how `PrimitiveRender` was measured against a match in the renderer: //! That is how `PrimitiveRender` was measured against a match in the renderer:
//! 6 instructions per list drawn, against the ~5,400 wgpu spends recording //! 6 instructions per list drawn, against the ~5,400 wgpu spends recording
//! one. //! one.
//!
//! The instance is leaked deliberately. A Vulkan loader may unload the driver
//! when the last one drops, which can fault as a thread that used it exits --
//! and every test runs on a spawned thread.
use std::time::Instant; use std::time::Instant;
@@ -27,6 +23,9 @@ use iris_core::{
}; };
use wgpu::{Color as GpuColor, *}; use wgpu::{Color as GpuColor, *};
#[path = "gpu/mod.rs"]
mod gpu;
const SIZE: u32 = 1024; const SIZE: u32 = 1024;
const FRAMES: u32 = 200; const FRAMES: u32 = 200;
/// Reported as the best of this many batches, since the mean moves by more /// Reported as the best of this many batches, since the mean moves by more
@@ -34,39 +33,11 @@ const FRAMES: u32 = 200;
const BATCHES: u32 = 8; const BATCHES: u32 = 8;
fn gpu() -> Option<(Device, Queue)> { fn gpu() -> Option<(Device, Queue)> {
// Probed rather than assumed: there may be no Vulkan adapter, and GL is let adapter = gpu::adapter()?;
// what is left when there is not.
let all = Instance::new(InstanceDescriptor::new_without_display_handle());
let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default()))
{
Ok(_) => all,
Err(_) => Instance::new(InstanceDescriptor {
backends: Backends::GL,
..InstanceDescriptor::new_without_display_handle()
}),
};
// Leaked rather than dropped: see the note at the top of the file.
let instance: &'static Instance = Box::leak(Box::new(instance));
let adapter =
pollster::block_on(instance.request_adapter(&RequestAdapterOptions::default())).ok()?;
println!("adapter: {:?}", adapter.get_info()); println!("adapter: {:?}", adapter.get_info());
pollster::block_on(adapter.request_device(&DeviceDescriptor::default())).ok() pollster::block_on(adapter.request_device(&DeviceDescriptor::default())).ok()
} }
fn config(format: TextureFormat) -> SurfaceConfiguration {
SurfaceConfiguration {
usage: TextureUsages::RENDER_ATTACHMENT,
format,
color_space: SurfaceColorSpace::Auto,
width: SIZE,
height: SIZE,
present_mode: PresentMode::Fifo,
desired_maximum_frame_latency: 2,
alpha_mode: CompositeAlphaMode::Auto,
view_formats: vec![],
}
}
/// Every layer draws all three primitives, so the renderer takes a different /// Every layer draws all three primitives, so the renderer takes a different
/// path for each list it walks -- which is the case a single-primitive layer /// path for each list it walks -- which is the case a single-primitive layer
/// would never exercise. Images are bound per instance, so there are few. /// would never exercise. Images are bound per instance, so there are few.
@@ -136,7 +107,7 @@ fn fill(
fn frame_cost(device: &Device, queue: &Queue, layers: usize, per_layer: usize) -> f64 { fn frame_cost(device: &Device, queue: &Queue, layers: usize, per_layer: usize) -> f64 {
let format = TextureFormat::Bgra8Unorm; let format = TextureFormat::Bgra8Unorm;
let mut node = UiRenderNode::new(device, &config(format)); let mut node = UiRenderNode::new(device, &gpu::config(format, SIZE));
let mut ui = UiData::default(); let mut ui = UiData::default();
let mut render = UiRenderState::new(); let mut render = UiRenderState::new();
let _handles = fill(&mut ui, &mut render, layers, per_layer); let _handles = fill(&mut ui, &mut render, layers, per_layer);
+9 -5
View File
@@ -25,10 +25,14 @@ fn depth() -> usize {
env("IRIS_GENERATED_DEPTH", 4) env("IRIS_GENERATED_DEPTH", 4)
} }
/// The seeds the ordinary tests take. Seven 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) {
@@ -104,7 +108,7 @@ fn adding_and_removing_span_children_lands_where_growing_it_that_way_would() {
} }
#[test] #[test]
#[ignore = "as many seeds as it is asked for, rather than the nine the others check"] #[ignore = "as many seeds as it is asked for, rather than the ten the others check"]
fn a_long_run_of_seeds_agrees() { fn a_long_run_of_seeds_agrees() {
let depth = depth(); let depth = depth();
let seeds: Vec<u64> = match std::env::var("IRIS_GENERATED_SEED") let seeds: Vec<u64> = match std::env::var("IRIS_GENERATED_SEED")
+40
View File
@@ -0,0 +1,40 @@
//! The adapter and the surface configuration the GPU measurement rigs share,
//! so the two cannot probe for a device in two different ways.
use wgpu::*;
/// An adapter on whatever this machine has, or `None` where there is none.
///
/// Probed rather than assumed: there may be no Vulkan adapter, and GL is what
/// is left when there is not.
///
/// The instance is leaked deliberately. A Vulkan loader may unload the driver
/// when the last one drops, which can fault as a thread that used it exits --
/// and every test runs on a spawned thread.
pub fn adapter() -> Option<Adapter> {
let all = Instance::new(InstanceDescriptor::new_without_display_handle());
let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default()))
{
Ok(_) => all,
Err(_) => Instance::new(InstanceDescriptor {
backends: Backends::GL,
..InstanceDescriptor::new_without_display_handle()
}),
};
let instance: &'static Instance = Box::leak(Box::new(instance));
pollster::block_on(instance.request_adapter(&RequestAdapterOptions::default())).ok()
}
pub fn config(format: TextureFormat, size: u32) -> SurfaceConfiguration {
SurfaceConfiguration {
usage: TextureUsages::RENDER_ATTACHMENT,
format,
color_space: SurfaceColorSpace::Auto,
width: size,
height: size,
present_mode: PresentMode::Fifo,
desired_maximum_frame_latency: 2,
alpha_mode: CompositeAlphaMode::Auto,
view_formats: vec![],
}
}
+28 -6
View File
@@ -22,6 +22,30 @@ use std::time::Instant;
const OUTPUT: (f32, f32) = (1920.0, 1200.0); const OUTPUT: (f32, f32) = (1920.0, 1200.0);
/// A scroll whose content fits is the same drawing in every box it still
/// fits in, so a longer or shorter one relays out nothing. Where the content
/// sits in that box is decided by placing its answer in the whole of it,
/// which is a fraction of the box and holds at every length -- so the
/// contract must not turn on the alignment. It did, and at the default
/// alignment, which is the middle, every box change redrew the scroll.
#[cfg(feature = "layout-diagnostics")]
#[test]
fn a_fitting_scroll_holds_for_every_box_its_content_fits_in() {
use iris::core::layout_diagnostics as diag;
for align in [Align::TOP_LEFT, Align::CENTER, Align::BOT_RIGHT] {
let mut harness = Harness::new((400, 200));
let inner = rect(Color::RED).height(50).add(&mut harness.rsc);
harness.set_root(inner.scrollable().align(align));
harness.frame();
let _ = diag::take();
// Still far longer than the 50 the content needs.
harness.resize((400, 180));
harness.frame();
assert_eq!(diag::take().distinct_widgets(), 0, "{align:?}");
}
}
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
#[test] #[test]
fn a_selected_widget_retains_its_layout_events() { fn a_selected_widget_retains_its_layout_events() {
@@ -37,8 +61,8 @@ fn a_selected_widget_retains_its_layout_events() {
diagnostics::trace_widget(leaf.id()); diagnostics::trace_widget(leaf.id());
let _ = diagnostics::take(); let _ = diagnostics::take();
let _ = harness.rsc.widgets_mut().get_dyn_mut(root.id()); harness.rsc.widgets_mut().mark_for_redraw(root.id());
let _ = harness.rsc.widgets_mut().get_dyn_mut(leaf.id()); harness.rsc.widgets_mut().mark_for_redraw(leaf.id());
harness.frame(); harness.frame();
let report = diagnostics::take(); let report = diagnostics::take();
@@ -195,7 +219,6 @@ fn layout_cost() {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
let _ = iris::core::layout_diagnostics::take(); let _ = iris::core::layout_diagnostics::take();
run("cold", 1, &mut harness, |_, _| {}); run("cold", 1, &mut harness, |_, _| {});
drop(tree);
} }
if selected("repaint") { if selected("repaint") {
@@ -203,7 +226,7 @@ fn layout_cost() {
trace_selected(&tree); trace_selected(&tree);
let leaf = tree.ids[0]; let leaf = tree.ids[0];
run("repaint", frames, &mut harness, move |harness, _| { run("repaint", frames, &mut harness, move |harness, _| {
let _ = harness.rsc.widgets_mut().get_dyn_mut(leaf); harness.rsc.widgets_mut().mark_for_redraw(leaf);
}); });
} }
@@ -218,7 +241,7 @@ fn layout_cost() {
println!("marking {} of {} widgets", dirty.len(), tree.ids.len()); println!("marking {} of {} widgets", dirty.len(), tree.ids.len());
run("many", frames, &mut harness, move |harness, _| { run("many", frames, &mut harness, move |harness, _| {
for &id in &dirty { for &id in &dirty {
harness.rsc.widgets_mut().get_dyn_mut(id); harness.rsc.widgets_mut().mark_for_redraw(id);
} }
}); });
} }
@@ -251,6 +274,5 @@ fn layout_cost() {
run("resize", frames, &mut harness, |harness, frame| { run("resize", frames, &mut harness, |harness, frame| {
harness.resize((OUTPUT.0 - ((frame + 1) % 2) as f32 * 8.0, OUTPUT.1)); harness.resize((OUTPUT.0 - ((frame + 1) % 2) as f32 * 8.0, OUTPUT.1));
}); });
drop(tree);
} }
} }
+3 -1
View File
@@ -194,7 +194,9 @@ fn text_memory() {
h.frame(); h.frame();
} }
report("after 40 resizes"); report("after 40 resizes");
// Settled: the output holds still and one leaf repaints per frame. // Settled: the output holds still and one leaf repaints per frame. Marked
// by taking it mutably because the revision at the top of this file has no
// `mark_for_redraw`, and the same source has to build against both.
for _ in 0..10 { for _ in 0..10 {
let _ = h.rsc.widgets_mut().get_dyn_mut(paragraphs[0]); let _ = h.rsc.widgets_mut().get_dyn_mut(paragraphs[0]);
h.frame(); h.frame();
+16 -11
View File
@@ -13,7 +13,7 @@
use iris::harness::Harness; use iris::harness::Harness;
use iris::prelude::*; use iris::prelude::*;
use iris::random::{Aligns, Edits, Kind, Lens, Plan, Rng, SpanEdit, Tree, build}; use iris::random::{Edits, Kind, Plan, Rng, SpanEdit, Tree, build};
use std::collections::HashMap; use std::collections::HashMap;
/// A seed per thread but one, since a seed grows, lays out and drops its tree /// A seed per thread but one, since a seed grows, lays out and drops its tree
@@ -190,7 +190,7 @@ impl Case {
fn mark(warm: &mut Harness, tree: &Tree, step: usize) { fn mark(warm: &mut Harness, tree: &Tree, step: usize) {
for &id in tree.ids.iter().step_by(step) { for &id in tree.ids.iter().step_by(step) {
warm.rsc.widgets_mut().get_dyn_mut(id); warm.rsc.widgets_mut().mark_for_redraw(id);
} }
} }
@@ -198,27 +198,32 @@ fn a_len(rng: &mut Rng) -> Option<LayoutLen> {
Some(LayoutLen::px(20.0 + rng.below(180) as f32)) Some(LayoutLen::px(20.0 + rng.below(180) as f32))
} }
fn resize_one(warm: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Lens { fn resize_one(warm: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> SizeRules {
let lens = [a_len(rng), a_len(rng)]; let lens = SizeRules {
x: a_len(rng).into(),
y: a_len(rng).into(),
};
warm.rsc warm.rsc
.widgets_mut() .widgets_mut()
.set_size_rules(tree.sized[idx], lens[0], lens[1]); .set_size_rules(tree.sized[idx], lens.x, lens.y);
lens lens
} }
fn realign_one(warm: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Aligns { fn realign_one(warm: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Align {
let side = |rng: &mut Rng| match rng.below(4) { let side = |rng: &mut Rng| match rng.below(4) {
0 => None, 0 => None,
1 => Some(AxisAlign::NEG), 1 => Some(AxisAlign::NEG),
2 => Some(AxisAlign::CENTER), 2 => Some(AxisAlign::CENTER),
_ => Some(AxisAlign::POS), _ => Some(AxisAlign::POS),
}; };
let align = [side(rng), side(rng)]; let align = Align {
x: side(rng),
y: side(rng),
};
let id = tree.aligned[idx]; let id = tree.aligned[idx];
for (axis, align) in [Axis::X, Axis::Y].into_iter().zip(align) { let taken = RegionAlign::from(align);
warm.rsc for axis in Axis::BOTH {
.widgets_mut() warm.rsc.widgets_mut().set_alignment(id, axis, taken[axis]);
.set_alignment(id, axis, align.unwrap_or_default());
} }
align align
} }
+15 -29
View File
@@ -1,5 +1,5 @@
//! Traces the six-widget tree in `unsettled.rs`, to see what box its text is //! Traces the four-widget trees in `unsettled.rs`, to see what box their text
//! actually drawn in on a first frame against a settled one. //! is actually drawn in on a first frame against a settled one.
#![cfg(feature = "layout-diagnostics")] #![cfg(feature = "layout-diagnostics")]
@@ -9,30 +9,25 @@ use iris::prelude::*;
fn plant(h: &mut Harness) -> Vec<WidgetId> { fn plant(h: &mut Harness) -> Vec<WidgetId> {
let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc); let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc);
let wrapped = wtext("Wrapping shapes").size(16).wrap(true).add(&mut h.rsc); let wrapped = wtext("Wrapping shapes")
let sized = wrapped.width(76).add(&mut h.rsc); .size(16)
let aligned = sized; .wrap(true)
.width(76)
.add(&mut h.rsc);
h.rsc h.rsc
.widgets_mut() .widgets_mut()
.set_alignment(sized, Axis::X, AxisAlign::POS); .set_alignment(wrapped, Axis::X, AxisAlign::POS);
h.rsc h.rsc
.widgets_mut() .widgets_mut()
.set_alignment(sized, Axis::Y, AxisAlign::POS); .set_alignment(wrapped, Axis::Y, AxisAlign::POS);
let stack = Stack { let stack = Stack {
children: vec![plain.add_strong(&mut h.rsc), aligned.add_strong(&mut h.rsc)], children: vec![plain.add_strong(&mut h.rsc), wrapped.add_strong(&mut h.rsc)],
size: StackSize::Child(0), size: StackSize::Child(0),
} }
.add(&mut h.rsc); .add(&mut h.rsc);
let root = (stack,).span(Dir::RIGHT).add(&mut h.rsc); let root = (stack,).span(Dir::RIGHT).add(&mut h.rsc);
h.state.root = Some(root.add_strong(&mut h.rsc)); h.state.root = Some(root.add_strong(&mut h.rsc));
vec![ vec![plain.id(), wrapped.id(), stack.id(), root.id()]
plain.id(),
wrapped.id(),
sized.id(),
aligned.id(),
stack.id(),
root.id(),
]
} }
fn dump(label: &str, report: &diag::Report, text: WidgetId) { fn dump(label: &str, report: &diag::Report, text: WidgetId) {
@@ -81,7 +76,7 @@ fn what_box_the_text_is_drawn_in() {
for _ in 0..2 { for _ in 0..2 {
for &id in &ids { for &id in &ids {
h.rsc.widgets_mut().get_dyn_mut(id); h.rsc.widgets_mut().mark_for_redraw(id);
} }
let _ = diag::take(); let _ = diag::take();
h.frame(); h.frame();
@@ -93,23 +88,14 @@ fn what_box_the_text_is_drawn_in() {
fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> { fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> {
let words = "Wrapping shapes one source into as many lines as the box leaves"; let words = "Wrapping shapes one source into as many lines as the box leaves";
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc); let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
let aligned = text;
h.rsc h.rsc
.widgets_mut() .widgets_mut()
.set_alignment(text, Axis::X, AxisAlign::NEG); .set_alignment(text, Axis::X, AxisAlign::NEG);
let inner = (aligned,).span(Dir::RIGHT).add(&mut h.rsc); let inner = (text,).span(Dir::RIGHT).sized((189, 176)).add(&mut h.rsc);
let sized = inner.sized((189, 176)).add(&mut h.rsc);
let filler = rect(Color::RED).add(&mut h.rsc); let filler = rect(Color::RED).add(&mut h.rsc);
let root = (filler, sized).span(Dir::RIGHT).add(&mut h.rsc); let root = (filler, inner).span(Dir::RIGHT).add(&mut h.rsc);
h.state.root = Some(root.add_strong(&mut h.rsc)); h.state.root = Some(root.add_strong(&mut h.rsc));
vec![ vec![text.id(), inner.id(), filler.id(), root.id()]
text.id(),
aligned.id(),
inner.id(),
sized.id(),
filler.id(),
root.id(),
]
} }
#[test] #[test]