Compare commits
base: iris-ai/iris:f9423855e11c365a92fa026ff3cae30fe6df70b2
iris-ai/iris:main
iris-ai/iris:layout/one-ask
iris-ai/iris:layout/bounds
iris-ai/iris:app-pin
iris/iris:main
iris/iris:work
iris/iris:remove_size_attempt
iris/iris:atomics
iris/iris:tuple_experiment
iris/iris:tuple_attempt
iris-ai/iris:archive/2026-09-19/wip/inset
iris-ai/iris:archive/2026-09-19/wip/step3-experiment
iris-ai/iris:archive/2026-09-19/wip/local-reask
iris-ai/iris:archive/2026-09-19/split/18-position-chain
iris-ai/iris:archive/2026-09-19/wip/stack-fraction-twice
iris-ai/iris:archive/2026-09-19/wip/padding-outset-and-inset
iris-ai/iris:archive/2026-09-19/split/17-headless-rig
iris-ai/iris:archive/2026-09-19/split/16-draw-size
iris-ai/iris:archive/2026-09-19/split/12-pointer-routing
iris-ai/iris:archive/2026-09-19/split/15-harness
iris-ai/iris:archive/2026-09-19/split/14-setsize
iris-ai/iris:archive/2026-09-19/split/13-wgpu30
iris-ai/iris:archive/2026-09-19/split/11-texture-bindings
iris-ai/iris:archive/2026-09-19/split/09-parley-text-clean
iris-ai/iris:archive/2026-09-19/split/08-parley-text
..
compare: iris-ai/iris:archive/2026-09-19/split/16-draw-size
iris-ai/iris:layout/one-ask
iris-ai/iris:layout/bounds
iris-ai/iris:main
iris-ai/iris:app-pin
iris/iris:main
iris/iris:work
iris/iris:remove_size_attempt
iris/iris:atomics
iris/iris:tuple_experiment
iris/iris:tuple_attempt
iris-ai/iris:archive/2026-09-19/wip/inset
iris-ai/iris:archive/2026-09-19/wip/step3-experiment
iris-ai/iris:archive/2026-09-19/wip/local-reask
iris-ai/iris:archive/2026-09-19/split/18-position-chain
iris-ai/iris:archive/2026-09-19/wip/stack-fraction-twice
iris-ai/iris:archive/2026-09-19/wip/padding-outset-and-inset
iris-ai/iris:archive/2026-09-19/split/17-headless-rig
iris-ai/iris:archive/2026-09-19/split/16-draw-size
iris-ai/iris:archive/2026-09-19/split/12-pointer-routing
iris-ai/iris:archive/2026-09-19/split/15-harness
iris-ai/iris:archive/2026-09-19/split/14-setsize
iris-ai/iris:archive/2026-09-19/split/13-wgpu30
iris-ai/iris:archive/2026-09-19/split/11-texture-bindings
iris-ai/iris:archive/2026-09-19/split/09-parley-text-clean
iris-ai/iris:archive/2026-09-19/split/08-parley-text
7
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
46547563c1 |
Take a strong widget to depend on, and use the question mark
`depend_on_size` took a bare `WidgetId`, which any id at all satisfies. It takes a `&StrongWidget` now, so `DrawResult` carries the handle it was drawn from rather than an id copied out of it and nothing can claim a dependency on a widget it does not hold. `UiSpan::outside` matched on a pair of `Option`s where `?` says it. It was written that way while the method was still `const`, and it is not. No call site changed. Checked: fmt, clippy and 35 tests; `tabs`, `view`, `minimal` and `text` render unchanged, and the live sway resize round trip still matches a cold start at each size. |
||
|
|
53e61289f5 |
Say when a drawing cannot be taken out of its box, rather than guessing
`lerp_inv` returns `Option`. Inverting a lerp over a range of zero length has no one answer, and `div_or`'s fallback picked one: the start of the range. Measured, that is not a wrong number so much as a plausible one -- taking a part out of a box fixed at the top of the window hands back exactly what went in, and out of a box fixed at the middle hands back the parent's own `rel` of 0.5 as if it were the child's fraction. Either way the caller cannot tell that nothing was recovered, which is the defect; the previous commit's claim that it "returns a rel of 0" is right only for the first case. `UiScalar::outside` and `UiSpan::outside` follow it to `Option`, and `Remap` is the answer at the region level: `new` says whether a drawing in one box can be put in another and `apply` then cannot fail, so `try_reuse` asks once for a whole subtree and `reusable` goes back to reporting only what the widget claims. That replaces the predicate the last commit put in `reusable`, which stated the same rule in a second place. `DivOr` existed only for the fallback and is gone, along with `UiRegion::outside` and `UiVec2::outside`, which had no callers once `Remap` owned the operation. `UiRegion::axis` took `&mut self` to return a shared reference; `Remap::new` needs it on a shared one. This does not redraw less. `Remap::new` refuses exactly what the predicate refused; what it buys is one statement of the rule and a remap that cannot half-apply. Counted on a resize, with the old wipe-everything for comparison: 20 padded rows, 101 widgets 101 draws -> 0 the `tabs` example 73 draws -> 0 the `text` example 49 draws -> 48 So the saving is whole where a resize does not change any widget's size, and nil in `text`, where both paragraphs rewrap to a different height and the relayout that forces reaches the root. The last commit's message oversold that case. Checked: fmt, clippy and 35 tests. `tabs` (with the image replay), `view` and `minimal` still byte-identical to `upstream/main`, `text` unchanged, and the live sway resize round trip still matches a cold start at each size. |
||
|
|
984f482a7f |
Move a resized drawing instead of redrawing it
A region is a fraction of the output plus an offset and the shader resolves it against the window every frame, so a resize already moves the whole drawing without the CPU. Wiping the tree and drawing it again was throwing that away. `Painter::output_size` and `px_size` now record that a widget read pixels, the way reading a child's size records a dependency on it, and a resize marks only those. In the `text` example that is the two wrapping paragraphs out of forty-odd widgets; everything else keeps its drawing and the window uniform puts it in the right place. Two defects the change surfaced, both of which made a resize land somewhere a cold start would not: `redraw` climbed to the highest reader of the changed widget and drew from there, trusting that draw to reach back down. It does not: an intermediate whose own box has not changed is reused as it stands and the draw stops there. Everything between the two is now marked as well, which is the only thing that stops the reuse. Not resize-specific -- `a_change_two_levels_under_its_reader_still_reaches_it` fails on the mutation path too. `mov` cannot stretch a drawing out of a box with no relative extent. `UiScalar::within` puts a part into such a box as a plain offset from its start, and `lerp_inv`'s divide-by-zero fallback then returns a rel of 0 rather than saying it cannot invert, so the remap silently leaves the drawing its old size. `OnResize::Scale` now only reuses across a length change when the old box had a relative extent. The underlying loss belongs to the position chain, which separates the drawn box from the offered one; until then this is the honest predicate. Checked: fmt, clippy and 33 tests. `tabs` (with the image replay), `view`, `minimal` still byte-identical to `upstream/main`, and `text` unchanged at 1920x1200 and 900x1200. Driven live under the GPU as well: started at 1920x1200, resized to 900x1200 and back through sway, and each screenshot matches a cold start at that size byte for byte. |
||
|
|
9520996623 |
Return the size from draw, and fold placing back into drawing
Review response. `Painter::set_size` is gone: `Widget::draw` returns the `Size` instead, so a widget that does not say what it used cannot compile rather than panicking at the widget that forgot. That also settles setting it twice -- a branch that learns something late just returns a different value. `Painter::place` and `UiRenderState::place` are gone too. `draw_inner` already tried to reuse an active widget's drawing before redrawing it, so `place` was `widget_within` with its own bookkeeping bolted on; drawing a child a second time now *is* how a parent puts it where it belongs, and a child is deduplicated in `children` because listing one twice would move it twice. The unification also drops `place`'s use of `ActiveData::layer`, which is the layer a widget's own `child_layer()` left the painter on rather than the layer it was drawn into. What `place` did unconditionally and `widget_within` did not is record the size dependency, so `Painter::size_hint` now records one: reading a child's length to lay out around it is reading its size, whether it came from a draw or from a hint. `tests/retained.rs` has a parent that only ever reads the hint, which is the case no existing widget exercises. `()` sizes itself `Size::default()` -- rest -- rather than zero, so it is a gap that takes an even share of a span; `WidgetPtr` with nothing in it does the same, since it is the same situation. `Widget::on_resize`'s default body said `Translate` while the enum's `#[default]` said `Redraw`; it now defers to the enum. `was` is `old` throughout, the `OnResize` variant comments are gone, and so are two empty `impl` blocks. Checked: fmt, clippy and 27 tests across the workspace; `tabs` on each of its five tabs, and `tabs` with a replay that adds two images, all byte-identical to `upstream/main`; `view` and `minimal` likewise; and the `text` example rendered at 1920x1200 and 900x1200 to see the paragraph reflow and its container follow. |
||
|
|
b108645240 |
Say what may be done to a drawing, and default to nothing
`SizeDependence::{None, Internal, External}` becomes
`OnResize::{Scale, Translate, Redraw}`, which says what the retained path
may do rather than leaving the reader to work it out from a dependency.
`Redraw` is now the default, and that is the substance of this rather
than the naming. `Translate` was, and nothing opted into it: `SetSize`
reports one size and hands its child the whole box, so its pixels change
with the box and carrying them stretched a 100x100 rect across half the
window. A default that is only right for widgets that happen to qualify
is the same fault as an unchecked reuse flag.
`Translate` still does nothing, and now for the reason rather than the
one I gave before: `mov` translates perfectly well, but `ActiveData`'s
`region` is both the box a widget was given and the box its primitives
occupy, and `mov` remaps out of it. Keeping a drawing at its old size
while the box grows leaves those two disagreeing, and the next move
stretches it. Separating them is what the offset chain does.
Caught by rendering `tabs` against `main` rather than by a test, which is
the argument for keeping that check in the loop.
|
||
|
|
ec012c4552 |
Text depends on its box for where it sits, not only for shaping
Alignment was not accounted for. Glyphs anchored to the start of an axis stay where they are when that extent changes, but the default is `CENTER_LEFT`: a label in a box that grows taller has to re-centre, even though its shaping is untouched. Saying `Internal` there would keep a drawing that belongs somewhere else once the retained path can act on it. It costs nothing today, since neither `Internal` nor `External` reuses anything yet. It would be silent when the move chain lands, which is why the classification wants to be right while it is being written rather than when something starts trusting it. |
||
|
|
f192f75b25 |
Size a widget while drawing it, not in a pass of its own
`desired_width`/`desired_height`, `WidgetAxisFns`, `SizeCtx` and the size cache are gone. A widget states what it used with `Painter::set_size` while it draws, and `Painter::widget` hands back a `DrawResult` whose `size()` both reads the child and records that this widget's size depends on it. Reading nothing keeps the parent independent of what the child came to. `Span` is what the change is for. It takes each child's `size_hint` where there is one, draws only the children that cannot answer, allocates the flexible space, then places everything -- which deletes `desired_ortho`, whose own comment said it "literally copies draw ... which makes this slow and not cool". Invalidation follows the dependency edges the draw recorded: a widget that needs redrawing hands off to the highest ancestor that read its size, instead of re-running a measurement to find out whether anything changed. `Widget::size_dependence(axis)` says how much of its box a widget's drawing depends on -- none of it, its own extent, or the whole box -- so the retained path can keep a drawing and write a new box into it. Asked per axis, because wrapped text depends on the width it is offered and not on the height. `Internal` does not yet buy more than `External`: keeping a drawing when only the room around it changed is a translation, which waits for the move chain. `tests/retained.rs` covers the second frame rather than the first, which is where the bugs were: a placed child that was not recorded as one got pruned as departed on the next draw. `examples/text.rs` is new, since wrapping was the one thing here with no way to see it on its own. |