Plan the transparent-frames layout protocol
Decided with Bryan on 2026-09-18: a child's frame is forwarded through spans, stacks and scrolls and narrowed only by what is decided from above, so rel is never a fraction of a self-sized box; placements are parts of the parent's extent in frame units from its start, so a moved extent re-places its children exactly; the placement pin becomes a symbolic length; and a local redraw asks at the offer placement without deferring. Lists the steps in order with the check for each and the failures that mean stop rather than patch. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
cb4557f1bc
commit
2bee0b4b53
1 file changed
+316
-8
+316
-8
@@ -27,6 +27,317 @@ traced on 2026-09-17 and is not what the earlier sections say; read **Where
|
|||||||
the `many` gap actually comes from** before anything else in this document
|
the `many` gap actually comes from** before anything else in this document
|
||||||
about performance, and take the earlier sections' explanations as history.
|
about performance, and take the earlier sections' explanations as history.
|
||||||
|
|
||||||
|
**The current work is the plan in the next section**, decided with Bryan on
|
||||||
|
2026-09-18. It is what the next agent does, on top of `34cafb6`, and it
|
||||||
|
supersedes the "Frame and extent" sections' description of `Span`, `Pad`,
|
||||||
|
`Stack`, the placement pin and `measure_len` wherever they differ.
|
||||||
|
|
||||||
|
## Plan: transparent frames (2026-09-18)
|
||||||
|
|
||||||
|
Decided with Bryan on 2026-09-18 after the trace below. The mechanism was
|
||||||
|
checked against the code of `34cafb6` and against the two counterexamples
|
||||||
|
on `wip/local-reask`; the pieces that were in doubt are called out. Do the
|
||||||
|
steps in order and run each step's check before the next. **If a check fails
|
||||||
|
and the fix that suggests itself is a tolerance, a pin, a deferral, a second
|
||||||
|
layout method, or a special case in `Span`, stop and report instead**: those
|
||||||
|
are exactly the patches that have been made around this design before, and
|
||||||
|
each one made the next problem harder to see.
|
||||||
|
|
||||||
|
### Decided by Bryan
|
||||||
|
|
||||||
|
- **Containers are transparent.** The frame a widget's fractions are of is
|
||||||
|
forwarded from its parent through a span, a stack and a scroll unchanged,
|
||||||
|
and through an inset narrowed by its margins. Any number of nested spans
|
||||||
|
lay out against one frame. The reason: `px` already passes through, `rel`
|
||||||
|
should behave the same way, and `leftover` is already the way to say
|
||||||
|
"fill the containing widget", so `rel(1.0)` meaning that too would be two
|
||||||
|
ways to say one thing.
|
||||||
|
- A frame is narrowed only by what is decided from above: a declared length
|
||||||
|
on the widget (`.width(rel(0.5))` on a span narrows its children's frame
|
||||||
|
too), an inset's margins, the root. A box that is an *answer* (a row's
|
||||||
|
height, a stack sized by a child) is never anything's frame.
|
||||||
|
- `rel` overflows on purpose when it sums past one or has pixels beside it.
|
||||||
|
- `Inset` and `Outset` take `px`, `rel` and `leftover` margins. Outset adds
|
||||||
|
its margins to the child's report and moves the child in; Inset draws the
|
||||||
|
child in a frame with the margins subtracted and reports the child's size
|
||||||
|
plus what it subtracted. An inset resolves what it subtracts, so `px` and
|
||||||
|
`rel` margins narrow the frame symbolically and a `leftover` margin is
|
||||||
|
resolved in pixels from the extent after the child answers, like a span's
|
||||||
|
shares. A general `Pad` would outset pixels and inset `rel` and
|
||||||
|
`leftover`. **Do not build these yet**; make them a few lines each to
|
||||||
|
write.
|
||||||
|
- Along its own axis a span places a child whose length is known in pixels
|
||||||
|
at its final slot while measuring, so it is drawn once, and moves a child
|
||||||
|
that is in the wrong place, `px` or `rel`, by translation rather than
|
||||||
|
drawing it again. Across itself it may still re-place by the answer.
|
||||||
|
- Layers may later be tied to another widget (a popup near an anchor). A
|
||||||
|
position is never defined by two widgets; positions compose up the tree.
|
||||||
|
|
||||||
|
### The protocol
|
||||||
|
|
||||||
|
Two boxes per widget, both in the *parent's frame coordinates*:
|
||||||
|
|
||||||
|
- **frame** -- what a declared or reported fraction is a fraction of.
|
||||||
|
`UiRegion::FULL` for a transparent parent; narrowed by `ask_box` for a
|
||||||
|
declared length and by an inset. Its *length* is the same on every ask of
|
||||||
|
the widget, which is the property the whole retained model rests on.
|
||||||
|
- **extent** -- where the drawing goes. Given by the parent as a `Place`
|
||||||
|
per axis, **relative to the parent's extent start, in frame units**:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Where a child goes along one axis, as a part of this widget's extent.
|
||||||
|
/// Spans are frame lengths from the extent's start, so a span's slot is
|
||||||
|
/// `from..start` and a moved extent re-places every child by re-adding
|
||||||
|
/// its start, exactly. `None` is the whole extent.
|
||||||
|
pub enum Place {
|
||||||
|
/// The child's answer, aligned inside the part by the child's alignment.
|
||||||
|
Within(Option<UiSpan>),
|
||||||
|
/// Exactly the part; the answer is not placed inside it again.
|
||||||
|
Fill(Option<UiSpan>),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The widget's `draw` sees only its extent: `px_len`/`px_size` are the
|
||||||
|
extent's pixels and narrow the extent range, `holds` widens it. Primitives
|
||||||
|
and masks are written in extent coordinates (`FULL` is the extent); there is
|
||||||
|
no `DrawRegion` and no frame-coordinate primitive. A container reads
|
||||||
|
`extent_len() -> UiVec2`, the extent's *symbolic* length in frame units,
|
||||||
|
which pins its drawing to that length and to nothing about the start. A
|
||||||
|
span that divides room also reads `frame_px_len(axis)` (today's
|
||||||
|
`region_px_len`) and widens through `frame_holds` (today's `region_holds`),
|
||||||
|
which narrow the frame range. `placement()`, `region()`, `box_of`,
|
||||||
|
`measure_len`, `widget_within`, `DrawRegion` and `ExtentPlacement` go.
|
||||||
|
|
||||||
|
The parent side is one call, with a shorthand:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub fn widget_at<'s, W: ?Sized>(
|
||||||
|
&'s mut self,
|
||||||
|
id: &'s StrongWidget<W>,
|
||||||
|
frame: UiRegion, // in this widget's frame coordinates; FULL forwards it
|
||||||
|
place: [Place; 2], // relative to this widget's extent start, in frame units
|
||||||
|
) -> DrawResult<'s, 'a, W>;
|
||||||
|
|
||||||
|
/// The transparent default: the frame as given, the answer aligned in the extent.
|
||||||
|
pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> {
|
||||||
|
self.widget_at(id, UiRegion::FULL, [Place::Within(None); 2])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`Span`, measuring and placing, with the row being its own extent:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
let axis = self.dir.axis;
|
||||||
|
let far = painter.extent_len().axis(axis); // symbolic; pins the length
|
||||||
|
let along = |from: Len, to: Len| match self.dir.sign {
|
||||||
|
Sign::Pos => UiSpan::new(from, to),
|
||||||
|
Sign::Neg => UiSpan::new(far - to, far - from),
|
||||||
|
};
|
||||||
|
let across = Place::Within(None);
|
||||||
|
// Measure. A child whose length is already known -- a rule, a hint --
|
||||||
|
// with no share before it is placed at its slot here and never again.
|
||||||
|
let mut cursor = Len::ZERO;
|
||||||
|
let mut shares_before = false;
|
||||||
|
let mut lens = Vec::with_capacity(self.children.len());
|
||||||
|
for child in &self.children {
|
||||||
|
let known = painter.size_hint(child, axis).filter(|_| !shares_before);
|
||||||
|
let place = match known {
|
||||||
|
Some(len) if len.leftover == Weight::ZERO => {
|
||||||
|
Place::Fill(Some(along(cursor, cursor + Len::from_parts(len.rel, len.px))))
|
||||||
|
}
|
||||||
|
_ => Place::Within(Some(along(cursor, far))),
|
||||||
|
};
|
||||||
|
let len = painter.widget_at(child, UiRegion::FULL, axis.pair(place, across)).len(axis);
|
||||||
|
shares_before |= len.leftover != Weight::ZERO;
|
||||||
|
cursor += Len::from_parts(len.rel, len.px + self.gap);
|
||||||
|
lens.push(len);
|
||||||
|
}
|
||||||
|
// total, room = far - fixed, the shares decision through frame_px_len and
|
||||||
|
// frame_holds(through(room)): unchanged from today.
|
||||||
|
// Place. A child already at its slot is an exact reuse; one whose slot
|
||||||
|
// moved is recomposed, since its length did not change.
|
||||||
|
for (child, len) in self.children.iter().zip(&lens) {
|
||||||
|
// undraw a share with nothing to share, accumulate fixed/taken, then:
|
||||||
|
painter.widget_at(child, UiRegion::FULL, axis.pair(Place::Fill(Some(along(from, start))), across));
|
||||||
|
}
|
||||||
|
Size::from_axis(axis, total, ortho)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Across itself a span reports `leftover` if any child does; otherwise the
|
||||||
|
longest child in *frame pixels* among `px` and `rel` children, reporting
|
||||||
|
that child's own `Len`. Comparing in pixels is safe here because the frame
|
||||||
|
is decided from above and nothing feeds back; the read is a `frame_px_len`
|
||||||
|
and narrows the frame range, and at the crossover both candidates are the
|
||||||
|
same number of pixels, so the drawing is the same on either side of it.
|
||||||
|
|
||||||
|
The shapes the rest of the containers take (write them, they are short):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Stack: the sizing child takes the whole extent, the rest are aligned in it.
|
||||||
|
painter.widget_at(child, UiRegion::FULL, [Place::Fill(None); 2]).size() // sizing
|
||||||
|
painter.widget_at(child, UiRegion::FULL, [Place::Within(None); 2]); // others
|
||||||
|
// Scroll: viewport is the extent; content is a pixel box offset by the scroll.
|
||||||
|
painter.widget_at(&self.inner, UiRegion::FULL,
|
||||||
|
self.axis.pair(Place::Fill(Some(UiSpan::px(anchor - amt, anchor - amt + content_len))), Place::Fill(None)));
|
||||||
|
// Inset with px/rel margins m: a narrowed frame and a narrowed extent.
|
||||||
|
let frame = m.narrow(UiRegion::FULL); // Len subtraction, exact
|
||||||
|
let len = painter.extent_len();
|
||||||
|
painter.widget_at(child, frame, [Place::Within(Some(UiSpan::new(m.lead.x, len.x - m.trail.x))), /* y */]).size()
|
||||||
|
+ m.total() // report child plus margins
|
||||||
|
// Outset: the same placement with the frame forwarded (UiRegion::FULL).
|
||||||
|
// Leftover margins: measure the child with Within(None), divide the room
|
||||||
|
// left in the extent by weight as a span does, then place with Fill.
|
||||||
|
```
|
||||||
|
|
||||||
|
A report is still `LayoutLen { px, rel, leftover }` per axis. A fraction in
|
||||||
|
it is of the reporting widget's frame, so through a transparent parent it
|
||||||
|
passes unchanged and through a narrowing parent (declared, inset) it is
|
||||||
|
composed by `within_len(frame.len())` as `in_parent_frame` does today. A
|
||||||
|
stack sized by a child that reports `rel(0.5)` therefore reports `rel(0.5)`,
|
||||||
|
is placed at half the frame, and hands its child the whole extent while the
|
||||||
|
child's frame is still the full one: the fraction is applied once.
|
||||||
|
|
||||||
|
### Retained state and reuse
|
||||||
|
|
||||||
|
Per widget (`ActiveData`), replacing `region`/`placement`/`given_region`/
|
||||||
|
`offer_len`/`offer_placement`:
|
||||||
|
|
||||||
|
- `frame: UiRegion` in the parent's frame coordinates, and `place: [Place; 2]`
|
||||||
|
as last given; `offer_place: [Place; 2]` from the first ask of the
|
||||||
|
parent's draw at the offer. There is no `offer_frame`: the frame's length
|
||||||
|
is the same on every ask, and `redraw` asserts it in debug.
|
||||||
|
- `frame_abs`, `extent_abs`: the two composed into `parent_move`
|
||||||
|
coordinates, for writing primitives and for `window_region`; rewritten by
|
||||||
|
recomposition.
|
||||||
|
- `answer: Option<(Size, LayoutHolds)>` from the offer ask; `holds:
|
||||||
|
LayoutHolds` for the drawing, where
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct LayoutHolds {
|
||||||
|
pub frame: [Holds; 2], // frame pixel lengths
|
||||||
|
pub extent: [Holds; 2], // extent pixel lengths
|
||||||
|
pub extent_len: [Option<Len>; 2], // symbolic extent length, where read
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `placement: Option<UiRegion>` pin is gone. Nothing may depend on where
|
||||||
|
an extent starts.
|
||||||
|
|
||||||
|
- primitives and the mask retained in extent-local coordinates, as now.
|
||||||
|
|
||||||
|
**Reuse** of a drawing at an ask: same layer, parent move and region-node
|
||||||
|
choice; frame pixels inside `frame`; the extent resolved from `place`
|
||||||
|
(`Fill` is the part; `Within` is the answer aligned in the part, or the part
|
||||||
|
where the answer fills) has pixels inside `extent` and, where pinned, the
|
||||||
|
same symbolic length. A frame that moved recomposes the subtree from
|
||||||
|
retained local coordinates (today's `recompose_subtree`). An extent that
|
||||||
|
moved **re-places every child** through its retained `place`
|
||||||
|
(`extent_abs.start + place`, then the child's own reuse test), stopping at
|
||||||
|
region nodes; this is today's `reposition` over `extent_children`,
|
||||||
|
generalised to all children because every child is now placed relative to
|
||||||
|
the extent. A child whose reuse fails there is drawn again at its retained
|
||||||
|
place. Along a span this is what moves `px` and `rel` children whose slots
|
||||||
|
shifted: the span's redraw re-issues `Fill(from..start)` with the same
|
||||||
|
lengths and different starts, and the child recomposes.
|
||||||
|
|
||||||
|
**A `Within` ask does not place the answer immediately.** The child draws
|
||||||
|
in the whole part, and the aligned answer box is applied by the next ask of
|
||||||
|
that child in the same parent draw, or, for a child not asked again, by a
|
||||||
|
pass at the end of the parent's draw over `children`. That is today's
|
||||||
|
`measure_len` rule made the rule for every open axis; it is what keeps a
|
||||||
|
span child at one draw plus one recomposition rather than two
|
||||||
|
recompositions.
|
||||||
|
|
||||||
|
**Dependencies composed into the parent** (today's `in_parent`): a child's
|
||||||
|
`frame` range goes through the child's frame length into the parent's frame
|
||||||
|
range. A child's `extent` range goes into the parent's *frame* range through
|
||||||
|
the part's length where the part is a span (a frame length), and into the
|
||||||
|
parent's *extent* range where the part is `None` (the whole extent). Answer
|
||||||
|
dependencies come only from children whose answer was read, drawing
|
||||||
|
dependencies from every child drawn, as today. Pins do not compose: a child
|
||||||
|
pinned on its symbolic length is checked when it is re-placed.
|
||||||
|
|
||||||
|
**Local redraw** (`redraw`): no deferral on lengths. Draw at `offer_place`
|
||||||
|
with the drawing left there where the given place differs (measuring),
|
||||||
|
compare answer and holds with what was retained, keep a still-covering old
|
||||||
|
guarantee as today, mark the parent only on a change, then place at the
|
||||||
|
given `place` (a reuse where the holds admit it). Deferral remains only for
|
||||||
|
a changed declared length or alignment and for an undrawn widget. The
|
||||||
|
`given_px != offered_px` branch and `offer_len` are deleted, not disabled.
|
||||||
|
|
||||||
|
### Steps, each with its check
|
||||||
|
|
||||||
|
Work on a branch from `34cafb6` in `/home/bob/repos/iris-layout-experiment`
|
||||||
|
(its `target` is its own; the baseline's is `target-own`). The checks are
|
||||||
|
`cargo test --workspace` in debug, `cargo test --release --test generated`
|
||||||
|
(fast oracle), and after steps 4, 6 and 7 the long ones:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
SHRINK_CASE=all SHRINK_SEEDS=400 SHRINK_DEPTH=5 cargo test --release --test shrink -- --ignored --nocapture
|
||||||
|
IRIS_GENERATED_SEEDS=1000 IRIS_GENERATED_DEPTH=6 cargo test --release --test generated -- --ignored a_long_run_of_seeds_agrees
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **`Place` and `widget_at(child, frame, [Place; 2])`**, with `widget` as
|
||||||
|
the shorthand, replacing `widget_at`/`widget_within`/`widget`/
|
||||||
|
`measure_len`. Internally keep `draw_inner` but make `DrawInfo` carry
|
||||||
|
`frame` and `place`; delete `DrawRegion`, `ExtentPlacement`,
|
||||||
|
`reads_placement`, `region()`, `placement()`, `box_of`. Add
|
||||||
|
`extent_len()`, rename `region_px_len`/`region_holds` to `frame_*`.
|
||||||
|
Check: it compiles with the call sites moved in step 2; no test yet.
|
||||||
|
2. **Call sites**: `Span` as above (without the known-length shortcut yet),
|
||||||
|
`Stack`, `Scroll`, `Pad` (as an inset by pixels, unchanged behaviour),
|
||||||
|
`Masked`, `Branch` in `random.rs`, `Text` (`glyphs` in extent
|
||||||
|
coordinates), the examples. Check: suite and fast oracle. Expected to
|
||||||
|
pass with `Span` reading `extent_len` on both axes and pinning it.
|
||||||
|
3. **Re-place every child on an extent move**, generalising `reposition`;
|
||||||
|
delete `extent_children`. Check: suite, fast oracle,
|
||||||
|
`retained::a_span_ruled_across_itself_moves_its_child_without_redrawing_it`
|
||||||
|
and a new test: a row whose first child grows by a pixel rule moves the
|
||||||
|
two after it without drawing them (count draws with the `Counted` widget
|
||||||
|
in `tests/cases/retained.rs`), once with a `px` second child and once
|
||||||
|
with a `rel` one.
|
||||||
|
4. **The symbolic-length pin replaces the placement pin**; `LayoutHolds`
|
||||||
|
as above. Check: suite, fast oracle, shrinker at 400/5, and the rig's
|
||||||
|
`many` at seed 13, depth 8 (`IRIS_SEED=13 IRIS_DEPTH=8 IRIS_PHASE=many`):
|
||||||
|
"reuse outside: the placement it was pinned to" is gone as a counter and
|
||||||
|
distinct widgets a frame should already be well under `34cafb6`'s 508.
|
||||||
|
5. **Lazy `Within`** with the end-of-draw pass. Check: suite;
|
||||||
|
`retained::a_span_does_not_place_its_measurement_before_assigning_the_childs_slot`
|
||||||
|
still counts three draws.
|
||||||
|
6. **`redraw` without the deferral**, as above; delete `offer_len` and the
|
||||||
|
`given_px != offered_px` branch. Check: suite, fast oracle, shrinker,
|
||||||
|
oracle at 1000/6 -- this is the step `wip/local-reask` failed at seeds
|
||||||
|
532 and 398, and both must pass now because the frame no longer changes
|
||||||
|
under the widget. If either still fails, shrink it and report; do not
|
||||||
|
restore the deferral. Then the rig: `many` at seeds 1 and 13, depth 8,
|
||||||
|
against `e44dea3` in `/home/bob/repos/iris-layout-baseline`
|
||||||
|
(`CARGO_TARGET_DIR=$PWD/target-own`). Expect distinct widgets near
|
||||||
|
`e44dea3`'s 95 and 159 and no deferral at all.
|
||||||
|
7. **Span's known-length shortcut and the cross-axis report** as written.
|
||||||
|
New tests: a `px` child after a `px` child is drawn once on a cold
|
||||||
|
layout and never on repaint; nested rows two deep with `rel(0.5)` give
|
||||||
|
half the *root* (or half a declared `.width(rel(0.5))` ancestor) wherever
|
||||||
|
the child sits; a row with a `rel(0.5)`-tall child reports `rel(0.5)`
|
||||||
|
across. Check: everything, including the long runs and the five
|
||||||
|
reference renders (`view`, `minimal`, `random`, `tabs`, `text`) against
|
||||||
|
`34cafb6` -- expect `random` and `tabs` to move where nested spans or
|
||||||
|
span heights change meaning, and look at them rather than diffing.
|
||||||
|
8. **Prove `Inset`/`Outset` are easy**: write them as test widgets in
|
||||||
|
`tests/cases/layout.rs` with `px`, `rel` and `leftover` margins, a dozen
|
||||||
|
lines each, and assert the child's box; do not add them to the crate.
|
||||||
|
9. Measure all six phases against `e44dea3` with the uninstrumented rig,
|
||||||
|
record the table here, and update the "Retained-layout invariants" list:
|
||||||
|
delete the bullets about `reports_of`, `decided`, `dirty_size_under`,
|
||||||
|
the placement pin and the offer-length deferral, and add the frame rule.
|
||||||
|
|
||||||
|
Expected against `e44dea3` at seed 1 and 13, depth 8: `size` and `scroll`
|
||||||
|
keep their wins; `many` within a few tens of percent, from the container
|
||||||
|
body running at the offer placement and at the placed one; `resize` at or
|
||||||
|
below its 13 draws. A `many` result above 2x is a sign something above was
|
||||||
|
not done as written, not a reason for a new mechanism.
|
||||||
|
|
||||||
**Widen one fuzzer axis at a time, and record which.** Seeds **1121** and
|
**Widen one fuzzer axis at a time, and record which.** Seeds **1121** and
|
||||||
**1839** at **depth 4** failed on `ea6dbae` and on every commit before it,
|
**1839** at **depth 4** failed on `ea6dbae` and on every commit before it,
|
||||||
and nothing in the routine verification reached them: the fast oracle takes
|
and nothing in the routine verification reached them: the fast oracle takes
|
||||||
@@ -1229,14 +1540,11 @@ In order, from the review above and Bryan's steer (2026-09-17):
|
|||||||
`a0693ac` and `e44dea3`; see the two sections above. The re-parenting
|
`a0693ac` and `e44dea3`; see the two sections above. The re-parenting
|
||||||
half turned out to be two defects rather than the predicted one, and
|
half turned out to be two defects rather than the predicted one, and
|
||||||
neither was the `depth()` assertion.
|
neither was the `depth()` assertion.
|
||||||
2. **Frame and extent**, as written above. This is the fundamental change
|
2. **Transparent frames**, the plan near the top of this document. This is
|
||||||
and comes before anything built on the placing ask. It lands the two
|
the fundamental change and comes before anything built on the placing
|
||||||
parked branches' tests (the stack test unchanged, `Inset`/`Outset` with
|
ask; it subsumes the parked `Inset`/`Outset` and stack-fraction branches
|
||||||
the rename and the `.pad()` audit), flips
|
(their tests land with step 7 and 8) and the `wip/local-reask` branch,
|
||||||
`a_span_reads_a_child_report_as_a_fraction_of_the_row`, and deletes
|
which is superseded and should be deleted once step 6 passes.
|
||||||
`box_of`, `reports_of`'s composition in `in_parent_frame`, and the
|
|
||||||
`through(lens)` translation in `draw_inner`. Run the long fuzzers and
|
|
||||||
the render set once for it.
|
|
||||||
3. Write `ActiveData::answer` in one place.
|
3. Write `ActiveData::answer` in one place.
|
||||||
4. Keep the `DrawInfo` on `ActiveData`; delete the copied fields and the
|
4. Keep the `DrawInfo` on `ActiveData`; delete the copied fields and the
|
||||||
reconstruction in `redraw`.
|
reconstruction in `redraw`.
|
||||||
|
|||||||
Reference in new issue
Block a user