The length kind that asks for a part of what is left once the fixed
lengths are taken is called leftover: Len::leftover(2), Len::LEFTOVER,
Size::LEFTOVER, Len::leftover the field, and apply_leftover. It says
what it is where "rest" reads as "the remainder of the list" as often as
"the remaining space", and every agent who has touched this has reached
for a third word for it.
Locals called rest that meant a region or a widget are renamed with it,
since the word now names something else.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SetSize took its declared length out of UiRegion::FULL, which is the box
it was already given, so a span that had sized that box from the same
hint had the fraction taken twice: .width(rel(0.5)) in a 400-wide span
drew its child 100 wide. It held under a Pad or the root, which do not
honour a hint, and hid under px, where 200 of a 200-wide box is all of
it. The text example was 111,923 pixels from upstream/main because of
it.
Whoever draws a widget now takes its declared length, in its own box,
which is what a fraction of one means, and is the identity for a caller
that already reserved the space. rest is not taken: a share of what is
left over is only a length to the widget dividing one, so it passes up
in the size as it does out of a span. SetSize keeps only what it
declares.
A declared length is then part of the box its parent decided, so
changing one has to redraw the parent; the lengths resolved into a box
are kept beside it and compared. Assuming instead that any dirty widget
which declares a length needs its parent costs 17% of a frame that
dirties 130 of 260 widgets, and buys nothing.
All five reference renders, the resize render and the image replay are
byte-identical to upstream/main, the 100-seed sweep passes, and the
resize fixture is 1.286 ms against 1.289 before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A local redraw drew a dirty widget in the box it was placed in. When a reader
decided that box from the widget's own answer -- an aligned span sized to its
children, a text at the tail of a row, a scroll's content -- the old answer is a
fixed point of measuring there whatever the content now says, so the layout had
two stable answers and which one it reached depended on the tree's history.
`tests/unsettled.rs` has the two shrunk cases: the four-widget aligned span,
and a scroll placing a pass-through `SetSize` in a box the content decided,
where the span under it was placed once and nothing at its own edge said so.
`ActiveData::offered_px` keeps the pixel size of the box the parent first asked
about the child in, whether through `known_len` or a first `place`, beside `px`,
the box it drew against. A dirty widget whose size reads an axis on which some
reader up its chain gave what it read a box other than the one it asked in is
not drawn locally: the chain is marked and the parent of the highest such
placement draws, since above it every box is a constraint rather than an
answer. The walk goes up the whole reader chain because a pass-through hands a
derived box down unchanged.
`Scroll` read its box's length for the clamp through `px_len`, which records
the reported size as depending on it, and it does not: its size is its
content's. That made every scroll tick a size question asked in a derived box,
at 34x the instructions. `Painter::px_len_for_draw` is the read that records
nothing. Instructions per frame on the depth-8 rig against the previous head:
`many` at 32 dirty 0.66M to 0.74M, at 130 dirty 27.7M to 26.5M, `resize` 15.8M
to 15.0M, `scroll`, `repaint` and `size` unchanged. The shrinking fuzzer passes
200 trees at depth 7 in all four cases, the hundred-seed sweep passes, and the
five reference renders and the resize render are byte-identical.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A resize had its own mechanism: `reads_output` recorded that a widget had
looked at the output, `update` scanned every active widget for one whose
`output_px` had moved, marked it and its whole reader chain, and
`resize_marks` kept those marks from counting as content dirtiness --
while `on_resize` answered the same question for every other box. Two
answers to "does this drawing survive its box changing length", and the
one that applied to the window ignored what the widget had declared.
With the output held as the root of the chain there is one question. A
resize offers the root widget its box again, `try_reuse` answers per axis
from `on_resize`, and `redraws_under` prices the subtree. Gone with it:
`reads_output`, `resized`, `resize_marks`, the scan, the eager reader
marking, and the shallowest-first branch in `redraw_updates`, which only
existed because resize marking worked differently -- the settle loop now
has one order.
Two things this needed. An unslotted widget may be reused when only its
parent's box changed length: it has nothing of its own to write, and what
it drew is a fraction of that box, so the slot above it already carries
the change. And `root_readers` holds the widgets whose size came from the
output rather than their own box -- `MaxSize` -- since no box of theirs
need have changed; they are marked per axis, from a set kept as they draw
rather than by scanning.
`a_resize_does_not_redraw_what_the_shader_can_move` now says `Scale`,
which is what it was always describing, and `a_resize_redraws_what_does
_not_scale` is its other half. `ReadsWidth` declares `Scale` across the
axis it does not read, so per-axis precision comes from the widget rather
than from which output axis it happened to touch.
Resize phase, seed 1 depth 8: 6.45M instructions per frame to 5.84M.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`dp` is coming, and then `abs` says which of the two it is not. The
component has always been a pixel count, so the name that admits it is
the one that leaves room for a second unit beside it.
Mechanical: the field on `Len` and `UiScalar`, their constructors,
`to_abs`/`get_abs`, the matching WGSL struct member and the locals
composing it. Field order and types are unchanged, so the `Pod` layout
the shader reads is the same bytes. `f32::abs` is untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Choosing which dirty widget to settle next asked every one of them how
deep it was, and answering meant walking its ancestry to the root. At
130 of 260 widgets dirty that was 25.8% of the frame -- more than laying
out or rendering.
A widget's depth is known where it is drawn: its parent's plus one. So
`Painter` carries it and `ActiveData` keeps it, and the choice reads a
field. Being reused counts as being visited, so the two reuse paths keep
it current too; only a subtree nothing looked at can hold an old one,
and nothing under an unvisited subtree is being ordered.
The order is unchanged, so nothing about the layout is: the five
reference renders and the resize render are byte-identical. What the
carried depth might get wrong is itself, so `depth` asserts it against
the ancestry in debug builds, and the hundred-seed sweep passes with
those assertions on -- including the reshuffles, which are what move a
widget to another parent.
Same load, 1000 frames, 130 of 260 dirty: 8.16M instructions per frame
to 7.14M, median 0.813 ms to 0.639, and the choosing from 25.8% of the
frame to 4.7%. What is left of it is iterating the dirty set itself,
which a `HashSet` walks by capacity rather than by length.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A container measures a child by drawing it in a box it may not keep, so
one layout asks a text for a dozen widths and comes back to widths it
has already had -- the hottest text in the depth-8 tree draws 32 times.
Each ask re-ran the shaper, because the two caches in front of it held
one entry each and a trial width alternating with a final width evicts
the answer about to be wanted again. `perf record` put 63% of a resize
frame in text and 0.9% in `draw_inner`.
So keep more than one: a bounded store of shapings on `TextData`, keyed
by the text, the attrs and the width, holding the parley layout and the
glyphs placed from it. Bounding the store rather than each buffer is
what keeps it a fixed cost -- +4 MB on a tree of 4,000 texts, which is
19 MB less than the code before #16 holds after the same resizes.
`TextBuffer` now holds the glyphs of the shaping it is drawn as, which
is where `TextView::tex` was. That leaves one place to invalidate rather
than two, so the `MutDetect` flags on a view's text and attrs have no
reader and go, along with the `buf.changed = true` after every edit.
On a 40-row tree of distinct random paragraphs, 500 resize frames:
124.2M instructions per frame before, 17.7M after, and 45.9M when the
width never repeats. The five reference renders and the resize render
are byte-identical, and the 100-seed sweep passes.
`tests/revision_cost.rs` is that tree, written in the API subset
`43ce8c7` shares so the same source measures the code this replaced.
Report the worst frame and p99 beside the median, since a stutter is
what somebody sees. Count glyph placements, and count a text render per
ask rather than per shaping, so the store cannot hide how many times a
layout drew the same text.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reading this back, three things claim something they do not do.
`OnResize::Translate` is returned by `TextView::on_resize` under a
comment weighing anchored glyphs against reshaping ones, but nothing
consumes it: `try_reuse` asks only whether the answer is `Scale`, so a
widget saying `Translate` is redrawn. Say so on the variant, since the
comment beside it reads as a description of behaviour.
`depend_on_size(child, false)` and `depend_on_size(child, true)` are the
difference between a hint, which is context-free, and a size the child
produced by drawing, which carries every pixel axis the child read. That
is the subtlest rule in the file and it was spelled as a bool; give the
two cases their names.
`draw_started` is the record of what has drawn during the pass under
way, and it worked only because `redraw` removes an id before asking
about it -- nothing emptied the set, so it accumulated the id of every
widget ever drawn, including ones long gone. Empty it with the pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A widget's region is now held in the coordinates of the slot it draws in
rather than the window's, and `Painter::place` is how a container asks for a
slot: it draws a child it decides the box of and may decide again. Everything
under that slot is a fraction of its box, so placing the child a second time
is one entry to write whether it moved or changed length. A child drawn any
other way has no slot and shares its nearest ancestor's.
That is what keeps the chain short. `chain_cost` measured depth as the cost
-- free to 8, +42.6% at 16 -- and a slot per widget put a transcript's glyphs
past that for nothing, since almost every slot was zero. `Span`, `Aligned`
and `Scroll` are the containers that re-place a child after drawing it, and
`tests/layout.rs` pins that four widgets between a span and a leaf leave the
leaf's chain one deep.
`UiRegion::stretch`, `UiRegion::stretchable` and `UiScalar::stretch` are
gone. Nothing is inverted any more: a box that changed length is written to
its slot, and the descendants recompose against it in the shader. That also
retires the case the guard existed for, where a fixed length has no fraction
to recover -- `tests/layout.rs` now stretches a 40-tall row on its other
axis, which `stretchable` refused outright.
What still walks the CPU is deciding who must draw again, which no chain can
answer: `mark_resized` descends from the widget whose box changed and marks
anything whose own box changed length and whose drawing reads it. A part of
a box with no relative extent on an axis is a fixed length, and composing
into it leaves none either, so the walk stops where a length did not change
-- an 80-wide child in a widened row is not redrawn though it says `Redraw`.
`Span`, `Pad`, `Stack`, `Offset`, `Aligned`, `SetSize` and `LayerOffset` say
`Scale`: each places in fractions and offsets of its own box and none reads
the box's pixel length. `Scroll` and `MaxSize` do read pixels and stay
`Redraw`.
45 tests pass, five of them new. Render verification comes after the CPU
side, per the owner.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The plumbing for O(1) subtree movement (LAYOUT.md §2), with every slot
still at zero, so this changes no pixels and the next commit can change
behaviour against a known-good picture.
Every active widget owns a slot in `UiData::moves`: a translation in
physical pixels and the slot it is relative to. A primitive instance and
a mask each name one, and `prelude.wgsl` walks the chain and adds the
accumulated delta. A mask resolves its own chain rather than the drawn
primitive's, so a stationary viewport can clip content that moves inside
it. `CHAIN_LIMIT` is stated on both sides; it bounds a malformed cycle
rather than any real tree.
A slot outlives any one `ActiveData`, because a redraw replaces that
while the widget's children go on pointing at the slot, so it lives in
`UiRenderState::moves` keyed by widget and is retired when the widget
stops being drawn. `MoveIdx` is its own type rather than another
`Id<u32>`: it sits beside `MaskIdx` in an instance and the two must not
be swappable.
`Vec2` is now `repr(align(8))`, which is WGSL's alignment for a
`vec2<f32>`, so a GPU struct holding one is laid out the way its shader
reads it without saying so itself -- `GlyphPrimitive` no longer states
its own alignment, and `MoveOffset` never has to. Both keep a manual
`unsafe impl Pod`, since the trailing padding that alignment introduces
is what `derive(Pod)` refuses. `WindowUniform` holds the `Vec2` its
shader has always called `dim` rather than two loose floats, which was
the last place the two sides described the same bytes differently.
Checked: fmt, clippy and 40 tests. `tabs` (with the image replay),
`view` and `minimal` render byte-identical to `upstream/main`, and
`text` is unchanged.
Replaces the bindless `binding_array<texture_2d<f32>>` the renderer bound every texture through. That array needs `VK_EXT_descriptor_indexing`, which a real share of Android GPUs lack, so the old shape did not run there at all.
The two things being bound want opposite treatment, so they are now split:
- **Glyph atlas pages become layers of one `texture_2d_array`.** A glyph primitive carries a `layer` instead of a view/sampler index pair. A layer index is an ordinary sampling operand, so this needs nothing beyond plain Vulkan 1.0 / GLES. Growing the atlas recreates the array with headroom and `copy_texture_to_texture`s the old layers across, no readback.
- **A standalone image gets its own texture and its own bind group,** and draws in its own call. It no longer needs a per-instance entry in `PrimitiveData`: the bind group has already picked the texture.
`Primitives` keeps images in a list of their own as a result, with `PrimitiveChange::is_image` naming which list a renumbering belongs to -- the two have independent index spaces, so `(layer, inst_idx)` alone would collide between them.
Two notes on judgement calls, since this slice was rebuilt on top of `main` rather than transplanted:
- The source version renamed `GlyphEntry::is_colored` to `is_color` and added a second `IS_COLOR` flag constant beside the existing `GlyphEntry::IS_COLORED`. Both dropped: #10's naming and its `flags()` are kept, and UVs stay `Vec2` rather than going back to `[f32; 2]`.
- `ImageGpu` no longer holds the `Texture` behind its view, which removes an `#[allow(dead_code)]`. A `TextureView` keeps its own reference to the texture, checked by rendering rather than assumed -- see below.
### Verification
```
cargo fmt --all --check
cargo clippy --workspace --all-targets --locked -- -D warnings
cargo test --workspace --locked
```
All clean; the 4 text-edit tests pass. The only clippy output is the pre-existing future-incompatibility notice about `naga`/`wgpu`/`winit`.
Because this is a rendering change, it was also run for real rather than only compiled. The `tabs` example was rendered on this machine's GPU -- Venus onto an RX 7900 XT, confirmed from the loaded ICD (`libvulkan_virtio.so` on `/dev/dri/renderD128`) rather than assumed, since a failed Vulkan init here silently falls back to llvmpipe and would make the screenshots meaningless.
Screenshots before and after the change are **byte-identical** (same md5) in two scenes: the default tab, which exercises text (the atlas path) and rects, and the image tab with a standalone image pushed at startup, which exercises the per-image bind group. The image-tab scene needed a temporary local edit to the example to push the image without a click; that edit is not part of this branch. The same comparison, re-run after dropping the `Texture` field, is still byte-identical -- which is the check that the view alone keeps it alive.
---------
Co-authored-by: iris <2+iris@noreply.localhost>
Reviewed-on: iris/iris#11
Reviewed-by: iris <2+iris@noreply.localhost>
Co-authored-by: AIris <4+iris-ai@noreply.localhost>
Replace the cosmic-text path with Parley layout and Swash rasterization, backed by shared glyph-atlas pages. Shaping, editing, rasterization, and glyph rendering move together because they share the text buffer and rendered-glyph types; splitting them further would require a temporary renderer that is immediately removed.
This is reconstructed rather than replayed from the extraction history. It also fixes issues found during review:
- texture binding changes remain set when an atlas patch follows a new page
- pressing an empty field places a caret and accepts input
- selection motion delegates collapse behavior to Parley
- character deletion follows logical clusters rather than visual neighbors
- the unused root-level Swash dependency is omitted
Four public-behavior integration tests live in `tests/text_edit.rs`: empty-field input, multibyte IME preedit replacement, UTF-8-safe backspace, and selection replacement. The old twelve-test inline block and implementation-restating cases are omitted.
Every added source comment was manually reviewed. Comments that narrated implementation or history were removed; retained comments document cache/rasterization keys, GPU upload constraints, focus representation, bidi geometry, or IME semantics.
Known limitation: atlas pages currently grow without eviction. Each page is 4 MiB on CPU and GPU. An arbitrary cap would leave cached rendered-text UVs pointing at reused glyph slots, so bounding this safely needs a later generation/invalidation change.
This changes public text types and signatures. GPU glyph rendering is covered by compilation rather than a live-surface test.
Verified with:
- `cargo fmt --all --check`
- `cargo clippy --workspace --all-targets -- -D warnings`
- `cargo test --workspace` (four integration tests pass)
Cargo still reports inherited future-incompatibility notices for existing wgpu/winit dependencies; there are no current clippy warnings.
---------
Co-authored-by: iris <2+iris@noreply.localhost>
Reviewed-on: iris/iris#10
Reviewed-by: iris <2+iris@noreply.localhost>
Co-authored-by: AIris <4+iris-ai@noreply.localhost>