layout/one-ask
9
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
aeb60e50f5 |
Say rel base, and give containers back a box to hand over
`frame` named a length, not a rectangle, which was the one word in the layout vocabulary that lied about its own shape. It is `rel_base`: what a fraction a widget declares or reports is a fraction of. Three API changes with it, all for containers that do one simple thing: - `widget_within(id, region)` returns, taking a box in the widget's own coordinates and deriving the child's rel base from it. `Offset` and `Pad` are one call each again. `Offset` also stops reading `region_len`, which pinned its drawing to a box length it does not care about. - `place_at` takes the rel base, returns the answer, and asks the child where there is no answer to re-express. Which of the two happens is the painter's to work out, so `Span`'s second pass is one call and its `drawn_across` bookkeeping is gone. - `Part::All` is a `Part::WHOLE` constant rather than a variant, since it was exactly `Of(UiSpan::FULL)` and bought a separate arm in two matches. Measured at 0.07% of instructions retired against 0.04% run-to-run noise. Cold layout is byte-identical to `84dad21` over 400 depth-5 trees. |
||
|
|
5642f2010a |
Say region and placement, not extent
The split box was named `region` and `placement` on 2026-09-17; `frame` came back as a length and survived, `extent` did not. It stayed as the name for both halves, distinguished only by prose: `draw_at` bound the caller's `part` to a parameter called `extent`, and `ActiveData` held two `UiRegion`s that `draw_at` wrote `part: extent` from. The box a parent asks a widget in is now the region, and where its drawing ends up is its placement. `Painter`'s four holds accumulators become the one `LayoutHolds` they were assembled into, which also drops the name mapping between them. The cold dump of 400 depth-5 trees is byte-identical across the change. |
||
|
|
84dad211f5 | Avoid repeated plan generation and unused diagnostics in layout fuzzers | ||
|
|
a30971e4c5 |
Call the record's boxes what they are
The offer names are from the protocol before this one, where a widget was drawn twice and the record had to say which drawing was the question. It is asked once now, so offer_part is the part it was asked in, offer_place the place it was asked at, and place where its drawing was put: part, asked and placed. LayoutHolds::frame is a range on the window since the frame became a length of one, and the frame's own entry is the frame_len pin beside it, so it is window; Painter::frame_own goes with it. answers_at had one caller and said less than the line that replaces it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1512d8418b |
Make the frame a length of the window and the box a region
There is one coordinate unit, the window. Every box in the tree is a region in window units and a widget's frame is a length in the same units, which is only what fractions resolve against, so the box need not be the frame and padding can take from both without either becoming the other. A region node's entry is a translation -- a rel 1 region anchored where its box starts -- rather than a box, so nothing composes a frame back up a chain and a node that moves is one entry write. Padding is then an inset of both: its pixels come off the frame, so rel(1.0) under it fills the padded widget rather than overflowing it, and off the box, so what is drawn sits inside. A length a container decides for a child's frame is a length of the window like everything else here -- a row's slot, padding's frame less its pixels, or the box a stack's sizing child decided, which arrives as Part::Sized -- because a slot of a row is not a fraction of anything the row can name, the same reason a node entry is a translation. A declaration is a fraction of whichever of those reached it, and is the only one that also places the box. Frame validity is a pin beside the box's, not a range: a range of window pixels cannot say which frame an answer is a fraction of, since two frames are different lengths at the same window size. A widget pins its frame by reading it or by being answered with it under a fractional rule, and the pin composes up wherever a length of this frame is what reached the child. Also here, because the diagnosis needed them: the shrinker reports the shrunk tree's own divergence with each level's frame, ask, box and size warm against cold, and there is a size-resize case -- a change and then a resize, the order that shows an answer kept as a fraction of the wrong length, which every other case compares at the window it was made at. Three defects the reports found, each pinned: a rule changed over two pads relocated the column under them instead of dividing it again (seed 59, depth 5, resize-size), a share inside padding had the padding taken off twice, and a root resolved its own rule twice. fmt and clippy clean with and without layout-diagnostics, 121 suite, 20 core, 11 generated, the 400-seed depth-5 shrinker over all sixteen cases. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
2ed5503717 |
Recompose retained frames exactly and preserve text width validity
Keep each widget's original local frame and replay the same composition order on reuse. Remove inverse region remapping, including its fixed-frame fallback that forced otherwise valid subtrees to draw again. Require exact pixel-region equality in the shared generated oracle. Check primitive and mask geometry as well as draw reuse when fixed frames resize. Publish text's retained line-break range, with no upper bound when there are no soft breaks, and cover widening, explicit newlines, and empty text. Compared with |
||
|
|
32542d0c0b |
Thread a box in pixels down the draw, one multiply from its parent's
A box in pixels was composed back up the move chain, on a grid fine enough that the walk rounded once, while a widget's offer was threaded down through its ancestors' offers. Two routes to one length, which is what `Holds::through` allowed for -- and the offer's route broke at a region node. `offered_region` fell back to `UiRegion::FULL` there, and `redraw` resolved that against the node's slot entry, which holds the box its parent *placed* the node in. Under a `Scroll` that is as long as the content rather than the viewport, so everything below was re-asked at a width its own answer had produced and the old answer confirmed itself: shrinker seed 220 on `reorder` left a widget 290px out. `ActiveData` now keeps a widget's box as lengths of its parent's box -- `given_len`, and `offer_len` for the box it was first asked about -- and `DrawInfo` carries the pixel lengths, threaded down one `Len::to_px` at a time: the box its parent gave it, then the part of that box its own answer placed the drawing in, which `placed_lens` states once for both `placed_box` and the walk. `Painter::px_size` and `px_len` read that value, and `UiRenderState::asked_px` takes the same steps back up the parent chain where a local redraw starts part-way down the tree. Neither chain has a coordinate frame in it, so neither can break at a region node, and warm and cold reach every length by the same expression. Three things follow. `Holds::through` is the exact preimage of `px + floor(rel * box)` -- two divisions, no allowance, the whole of a box mapping back to itself. A local redraw asks in the box its parent gave it and only where that box is as long as the offer, which retires `redraw`'s third ask and the region-node exception beside it; `draw_inner` places the answer inside that box itself. And symbolic regions are left to the GPU, hit testing and remaps, where `Moves::resolve` is the only walk: `wide.rs`, `Moves::compose`, `Moves::size_of`, `px_of`, `px_region`, `offered_region` and `slot_wide` are gone, 252 lines of `core/` net. `px` is deliberately not stored beside those lengths. A resize every widget's `Holds` admits redraws nothing, so a stored pixel length would be stale on every widget in the tree with nothing on it to say so, and refreshing it costs a walk down every reused subtree on the resize path. Instructions:u, medians of 21 runs, seed 1 at depth 8: | phase | before | after | | | --- | ---: | ---: | ---: | | `cold`, 200 frames | 313.1M | 312.9M | -0.04% | | `resize` | 408.1M | 405.6M | -0.61% | | `many` | 1,924M | 1,756M | -8.75% | | `scroll` | 357.3M | 323.4M | -9.49% | | `repaint` | 363.3M | 315.4M | -13.18% | `cold` and `resize` have all twenty-five work counters identical, so those two rows say the draw path costs the same threaded as composed. The other three do less work: `repaint` goes from 23 draw requests and 13 widget draws a frame to 1 and 1, `scroll` from 20 and 11 to 8 and 2, `many` from 273 and 186 to 207 and 157. Primitive writes are unmoved in every phase. Verified: `view`, `minimal`, `random`, `tabs` and `text` render byte-identical at 1920x1200 against `5b78002`, as does the `tabs` touch replay before and after the gesture, and a live resize of `random` to 1280x800 is identical both to the old head's and to a cold render at that size. The oracle passes 100 seeds in release and 120 in debug -- the debug run is the one that exercises the `Holds` assertion -- and the fifteen shrinker cases pass at 400 seeds of depth 5 and 1000 of depth 6. Seed 220 is `unsettled::a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered`, which needs both halves of this to fail: the old chain with the old allowance passes it, and the old chain with the exact preimage does not. `AGREE_STEPS` stays 2. One step passes the 100-seed oracle and fails the 400-seed shrinker on `resize-size` by 0.002 px, so what is left there is the resize path re-expressing a part as a fraction of a box that changed length, not a length reached two ways. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
98d4e98a29 |
Describe a tree before building it, so a failing seed can be reduced
The oracle grew its trees from a seed and the shrinker grew its own, with every scenario written out on each side. So a failure the oracle found could not be handed to the shrinker: there was no tree to pass it, only a seed, and a seed cannot be made smaller. The shrinker could only grow its own trees and hope to meet the same shape, which it does not -- 20,000 of its trees never reproduced what the oracle's seed 18 shows at depth 6. `iris::random` now answers with a `Plan`: `plan(seed, depth, &edits)` draws one out of the random stream and `build(rsc, &plan)` makes the widgets, where `grow` did both at once. Every draw happens in the order it always has, so a seed still means the tree it meant -- checked by running the oracle at 1000 seeds of depth 6 before and after and getting the same three failures with the same boxes. `Plan::smaller` reduces one, `Plan::edited` applies an `Edits` to a tree that already exists, and `tests/scenario/` holds the fifteen cases both rigs now run over the same trees. A span keeps the order it holds its children in apart from the children themselves, so detaching, attaching and reordering leave the widgets made in the same order and two builds still line up index for index. `Tree::detached` is gone: `Spanned::spares` is everything made for a span that it does not hold, which is what both of those were. `tests/cases/plan.rs` pins the three properties the rest rests on: editing a plan is growing one with those edits, every simplification is smaller than what it came from, and reducing ends. The second caught this change's own defect, where dropping a side of a `Branch` duplicated another and grew the tree by four widgets. What it found, on its first run: `SHRINK_SEED=18 SHRINK_DEPTH=6 SHRINK_CASE=repaint-some` reduces 277 widgets to 5. A scroll inside a scroll, the inner one owning a movable region, and only the text at the bottom marked for redraw -- and the span lands 24px out, which is exactly the sized child's height. `git bisect` names `95fb4f9`, where `Masked` began reporting its box rather than its inner's size, so what the outer scroll is told its content measures now depends on whether the inner subtree was redrawn this frame. `tests/cases/unsettled.rs` has it written out, ignored until it is fixed. Checked: fmt, clippy over all targets with -D warnings, the workspace tests (79 + 11 + 15, one ignored for the defect above), and the 100-seed oracle over all fifteen cases at depth 4. The shrinker at 400 seeds of depth 5 now fails, which it did not before running the oracle's trees and cases: seeds 2 and 288 on region-node and 174 and 175 on repaint-some are unreduced leads. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |