`mov` accumulates a delta onto the slot and `reposition` overwrote it, and both legitimately land on one widget in one frame: `List::place`'s Bottom-known branch offers a row a same-size box that has moved (`mov`), then corrects the placement inside it when the row's cached height no longer matches what the row reports (`reposition`). That is what a wrapped transcript row hit, and what the `move_applied == ZERO` debug assert was standing in for -- an assert against a case that happens is not a guarantee, it is a crash. The slot means `move_applied + repositioned` now, both halves recorded on `ActiveData`, so `reposition` adds the move rather than dropping it and stays idempotent. The assert it replaces is a `debug_assert_eq!` that the slot still holds that sum on entry -- i.e. that nothing but those two ever wrote it. Test: `a_widget_moved_by_its_parent_and_then_placed_inside_it_lands_at_the_placement`, which draws the child at the offered position (-100px) rather than the placement (100px) without the fix. Verified against the `.wrap(true)` repro from docs/IRIS_TODO.md (draws correctly, no panic) and an emulator bench run with assertions live.
70 lines
3.7 KiB
Rust
70 lines
3.7 KiB
Rust
use crate::{
|
|
LayerId, MaskIdx, MoveIdx, PrimitiveHandle, Size, TextureHandle, UiRegion, WidgetId, util::Vec2,
|
|
};
|
|
|
|
/// important non rendering data for retained drawing
|
|
#[derive(Debug)]
|
|
pub struct ActiveData {
|
|
pub id: WidgetId,
|
|
pub region: UiRegion,
|
|
pub parent: Option<WidgetId>,
|
|
pub textures: Vec<TextureHandle>,
|
|
pub primitives: Vec<PrimitiveHandle>,
|
|
pub children: Vec<WidgetId>,
|
|
/// The mask this widget was drawn **under** (its parent's), not the
|
|
/// one it set for itself -- see `own_mask` for that.
|
|
pub mask: MaskIdx,
|
|
/// The mask slot this widget allocated for *itself* with
|
|
/// `Painter::set_mask`, or `MaskIdx::NONE`. Kept across redraws and
|
|
/// rewritten in place, the way `move_slot` is: a `Masked` that pushed
|
|
/// a fresh slot each draw left every already-drawn descendant --
|
|
/// which `draw_inner`'s unchanged-region fast path does not revisit --
|
|
/// clipping to the *old* slot's region, so a composer whose bar had
|
|
/// since been placed at the bottom of the screen was still being
|
|
/// clipped to a box at the top of it and drew nothing (measured
|
|
/// 2026-09-06: four mask entries live, none of them the widget's
|
|
/// current region). Its path out is the `undraw` branch of
|
|
/// `UiRenderState::remove`, which drops the self-ownership ref taken
|
|
/// when the slot was allocated.
|
|
pub own_mask: MaskIdx,
|
|
pub layer: LayerId,
|
|
/// What `Widget::draw` returned the last time this widget was actually
|
|
/// drawn -- read by a parent placing this widget again without
|
|
/// redrawing it, replacing `Cache.size`'s old role. See LAYOUT.md
|
|
/// section 5.
|
|
pub size: Size,
|
|
/// This widget's slot in `UiData::move_offsets`, assigned on its first
|
|
/// draw and kept for the rest of its life (redraws reuse it in place
|
|
/// so a retained child's `parent` link never goes stale). See
|
|
/// LAYOUT.md section 2.
|
|
pub move_slot: MoveIdx,
|
|
/// How much of this widget's own `move_slot` delta is already folded
|
|
/// into `region` above, in window pixels. The two mechanisms that
|
|
/// write that slot disagree about this and cannot be told apart from
|
|
/// the slot alone: `UiRenderState::mov` shifts `region` and the delta
|
|
/// together (the *offered* region genuinely moved), while
|
|
/// `Painter::reposition` writes only the delta (`region` stays the
|
|
/// offered box and the delta says where inside it the content was
|
|
/// placed). So anything that wants the widget's real position --
|
|
/// `resolved_region`, and through it every hit test -- must subtract
|
|
/// this from the chain sum. Without it a panned widget's own hit box
|
|
/// sits at twice the pan while its descendants' are correct, which is
|
|
/// how it went unnoticed: the composer's field became untappable
|
|
/// after a finger pan (2026-09-06). Reset to zero whenever the widget
|
|
/// is really redrawn, since `draw_inner` zeroes the slot then too.
|
|
pub move_applied: Vec2,
|
|
/// The offset the last `Painter::reposition` placed this widget's
|
|
/// content at *within* `region`, in window pixels. The move slot has
|
|
/// exactly one owner and one meaning:
|
|
/// `move_offsets[move_slot] == move_applied + repositioned`. `mov`
|
|
/// adds to the first, `reposition` overwrites the second (it
|
|
/// recomputes `from` afresh every call, so repeating it must land on
|
|
/// the same answer rather than drifting), and both then rewrite the
|
|
/// slot from the sum -- which is what lets a parent both move a child
|
|
/// with its own layout and place it inside that moved region in one
|
|
/// frame. `List::place`'s Bottom-known branch does exactly that once a
|
|
/// row's blocks wrap. Reset to zero on a real redraw, with
|
|
/// `move_applied` and the slot itself.
|
|
pub repositioned: Vec2,
|
|
}
|