From 9f4311774b12aeee1d2f133852f44e61f8060c55 Mon Sep 17 00:00:00 2001 From: iris-ai <4+iris-ai@noreply.localhost> Date: Mon, 14 Sep 2026 22:32:53 -0400 Subject: [PATCH] Hold the output as the box every chain bottoms out in A position was composed up the slot chain to a normalized region and then multiplied by the output's size, so the window was the one box in the system that was not a box. Seeding the chain with a root slot holding it in pixels makes composing through it leave everything below in pixels, which is what the multiplication was doing. `within` already does the arithmetic: a child at `rel` 1 inside a span of `px` 0 to `px` 1920 composes to `px` 1920 and `rel` 0, so the trailing `to_px` becomes the identity rather than a step. The shader walks the same chain and needs no change for the same reason. This is the shape the resize machinery wants before it can be deleted: a resize becomes one slot written, which `try_reuse` and `redraws_under` already carry. Nothing is removed yet. Co-Authored-By: Claude Opus 5 --- core/src/ui/render_state.rs | 25 +++++++++++++++++++++++-- tests/layout.rs | 6 +++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index 3ec5ff5..8667dcf 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -2,7 +2,7 @@ use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind}; use crate::{ ActiveData, Axis, DrawLayers, IdLike, MaskIdx, MoveIdx, Moves, OnResize, Painter, PixelRegion, - Size, StrongWidget, UiRegion, UiRsc, WidgetId, Widgets, + Size, StrongWidget, UiRegion, UiRsc, UiScalar, UiSpan, WidgetId, Widgets, util::{HashMap, HashSet, Vec2}, }; @@ -19,6 +19,8 @@ pub struct UiRenderState { pub(super) output_size: Vec2, old_root: Option, + /// The slot every chain bottoms out in, holding the output as a box. + root_move: MoveIdx, resized: [bool; 2], /// Content/state dirtiness whose retained size cannot answer a layout /// question until that widget has drawn again. @@ -50,6 +52,22 @@ impl UiRenderState { draw_started: Default::default(), slots: Default::default(), moves: Default::default(), + root_move: MoveIdx::NONE, + } + } + + /// The window as a box, so a chain bottoms out in one rather than in a + /// multiplication applied after it. Composing through a box held in + /// pixels leaves everything below it in pixels, which is why nothing + /// downstream has to know the output's size to resolve a position. + fn write_root(&mut self) { + let region = UiRegion::new( + UiSpan::new(UiScalar::ZERO, UiScalar::px(self.output_size.x)), + UiSpan::new(UiScalar::ZERO, UiScalar::px(self.output_size.y)), + ); + match self.root_move == MoveIdx::NONE { + true => self.root_move = self.moves.push(MoveIdx::NONE, region), + false => self.moves.set(self.root_move, region), } } @@ -59,6 +77,7 @@ impl UiRenderState { *resized |= size.axis(axis) != self.output_size.axis(axis); } self.output_size = size; + self.write_root(); } pub fn output_size(&self) -> Vec2 { @@ -149,6 +168,7 @@ impl UiRenderState { let _layout = diag::timer(TimerKind::FullLayout); self.clear(rsc); // free all resources & cache + self.write_root(); if let Some(id) = root { self.draw_inner( 0, @@ -156,7 +176,7 @@ impl UiRenderState { UiRegion::FULL, None, 1, - MoveIdx::NONE, + self.root_move, false, MaskIdx::NONE, None, @@ -589,6 +609,7 @@ impl UiRenderState { } self.slots.clear(); self.moves.clear(); + self.root_move = MoveIdx::NONE; self.layers.clear(); self.invalid_sizes.clear(); self.resize_marks.clear(); diff --git a/tests/layout.rs b/tests/layout.rs index 569e9d3..6e88906 100644 --- a/tests/layout.rs +++ b/tests/layout.rs @@ -180,5 +180,9 @@ fn only_a_container_that_places_its_children_lengthens_the_chain() { h.set_root((bar, buried).span(Dir::RIGHT)); let slot = h.render.active[&leaf.id()].parent_move; - assert_eq!(h.render.moves.depth(slot), 1, "one span above the leaf"); + assert_eq!( + h.render.moves.depth(slot), + 2, + "the span above the leaf, and the root the window is held in" + ); }