Read a child's answer in the asker's frame, and drop the root move entry
A widget reports a fraction of the box it was given. Span added that
fraction straight into a cursor that counts fractions of the row, and Pad
summed its padding onto it, both right only while the offer had the
parent's whole extent -- which a span's does not after a relative child.
DrawResult::size and known_len now compose the answer through the offer's
length, so a container reads lengths of its own box.
That exposed placed_box scaling a fractional answer against a box the
parent had already chosen from it, halving a nested span twice. The
near-edge alignment override becomes per-axis `decided` flags: a box the
parent chose from the answer is the answer, and is not placed again.
Span decides the row axis; Scroll and Stack's sizing child decide both.
Alignment is always the widget's own property now.
The window is no longer a move entry. Chains bottom out in MoveIdx::NONE
and the window is applied where a fraction becomes pixels, in to_px on the
CPU and by the uniform in the shader, which now snaps the summed coordinate
since a floor does not distribute over a sum. A resize rewrites no entry.
Verified: view, minimal, random, tabs and text render byte-identical at
1920x1200 against 5f16617, a live resize to 1280x800 is identical to a
cold render, and the 100-seed oracle, all fifteen shrinker cases at 400
seeds of depth 5, and 1000 seeds of depth 6 pass.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
5f16617511
commit
5b7800264d
11 files changed
+182
-109
No files matched your search
+39
-62
@@ -1,10 +1,10 @@
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind};
|
||||
use crate::ui::painter::{declared_box, declared_lens, placed_box};
|
||||
use crate::ui::painter::{declared_box, declared_lens, fills, placed_box};
|
||||
use crate::{
|
||||
ActiveData, Axis, DrawLayers, Holds, IdLike, LayoutLen, Len, MaskIdx, MoveIdx, Moves, Painter,
|
||||
PixelRegion, Px, PxVec2, RegionAlign, Rel, Size, StrongWidget, UiRegion, UiRsc, UiSpan, Weight,
|
||||
WideRegion, WidgetId, Widgets,
|
||||
PixelRegion, Px, PxVec2, Rel, Size, StrongWidget, UiRegion, UiRsc, UiSpan, Weight, WideRegion,
|
||||
WidgetId, Widgets,
|
||||
util::{HashMap, Vec2},
|
||||
};
|
||||
|
||||
@@ -27,9 +27,10 @@ pub(super) struct DrawInfo {
|
||||
/// The box `parent_move` composes to, on the fine grid, so a widget's own
|
||||
/// box is one step further and not a walk back up the chain.
|
||||
pub slot_wide: WideRegion,
|
||||
/// A container's answer for where the widget sits. `None` uses the
|
||||
/// widget's own property.
|
||||
pub align: Option<RegionAlign>,
|
||||
/// The axes along which the parent chose this box from the widget's own
|
||||
/// answer, so the answer is not placed inside it again. See
|
||||
/// [`Painter::widget_decided`].
|
||||
pub decided: [bool; 2],
|
||||
}
|
||||
|
||||
pub struct UiRenderState {
|
||||
@@ -38,8 +39,6 @@ pub struct UiRenderState {
|
||||
pub(super) output_size: PxVec2,
|
||||
|
||||
old_root: Option<WidgetId>,
|
||||
/// The slot every chain bottoms out in, holding the output as a box.
|
||||
root_move: MoveIdx,
|
||||
/// Whether the output has changed since the last update. A frame is
|
||||
/// owed for that whether or not anything has to be drawn again.
|
||||
resized: bool,
|
||||
@@ -67,35 +66,22 @@ impl UiRenderState {
|
||||
answer_invalid: Default::default(),
|
||||
replace_answers: false,
|
||||
moves: Default::default(),
|
||||
root_move: MoveIdx::NONE,
|
||||
resized: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(Len::ZERO, Len::from_parts(Rel::ZERO, self.output_size.x)),
|
||||
UiSpan::new(Len::ZERO, Len::from_parts(Rel::ZERO, 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),
|
||||
}
|
||||
}
|
||||
|
||||
/// The window, in whatever the platform measures it in, onto the grid
|
||||
/// everything below it is decided on.
|
||||
/// everything below it is decided on. No move entry holds it: a chain
|
||||
/// bottoms out in `MoveIdx::NONE`, which is the window, and the window's
|
||||
/// size is applied where a fraction becomes pixels -- here in `to_px`,
|
||||
/// and in the shader by its uniform. A resize therefore rewrites no
|
||||
/// retained entry at all.
|
||||
pub fn resize(&mut self, size: impl Into<Vec2>) {
|
||||
let size = PxVec2::from_f32(size.into());
|
||||
if size == self.output_size {
|
||||
return;
|
||||
}
|
||||
self.output_size = size;
|
||||
self.write_root();
|
||||
self.resized = true;
|
||||
}
|
||||
|
||||
@@ -104,13 +90,13 @@ impl UiRenderState {
|
||||
layer: 0,
|
||||
parent: None,
|
||||
depth: 1,
|
||||
parent_move: self.root_move,
|
||||
parent_move: MoveIdx::NONE,
|
||||
region_node: false,
|
||||
mask: MaskIdx::NONE,
|
||||
offer: UiRegion::FULL,
|
||||
offered_px: self.output_size,
|
||||
slot_wide: self.moves.compose(self.root_move, UiRegion::FULL),
|
||||
align: None,
|
||||
slot_wide: WideRegion::of(UiRegion::FULL),
|
||||
decided: [false; 2],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,8 +153,6 @@ impl UiRenderState {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
let _layout = diag::timer(TimerKind::FullLayout);
|
||||
self.clear(rsc);
|
||||
// free all resources & cache
|
||||
self.write_root();
|
||||
if let Some(id) = root {
|
||||
let info = self.root_info();
|
||||
let region = Self::root_region(id.id(), rsc.widgets());
|
||||
@@ -203,8 +187,7 @@ impl UiRenderState {
|
||||
info.region_node,
|
||||
);
|
||||
}
|
||||
let own_align = rsc.widgets().alignment(id);
|
||||
let align = info.align.unwrap_or(own_align);
|
||||
let align = rsc.widgets().alignment(id);
|
||||
let replace_answer = self.answer_invalid.remove(&id)
|
||||
|| (self.replace_answers
|
||||
&& (rsc.widgets().needs_redraw.contains(&id)
|
||||
@@ -219,19 +202,15 @@ impl UiRenderState {
|
||||
if old.is_none() {
|
||||
old = self.remove(id, false, rsc);
|
||||
}
|
||||
self.draw_at(id, region, info, align, old.take(), rsc)
|
||||
self.draw_at(id, region, info, old.take(), rsc)
|
||||
});
|
||||
|
||||
let declared = declared_lens(rsc.widgets(), id);
|
||||
// A near-edge override means the caller already chose this box from
|
||||
// the child's answer. Applying the answer again would compound the
|
||||
// placement; it is also how the second, final ask terminates.
|
||||
let placed = match info.align == Some(RegionAlign::NEAR) {
|
||||
true => region,
|
||||
false => placed_box(region, answer.0, align, declared),
|
||||
};
|
||||
// The second, final ask is in a box chosen from the answer on both
|
||||
// axes, which is also what makes it terminate.
|
||||
let placed = placed_box(region, answer.0, align, declared, info.decided);
|
||||
let placed_info = DrawInfo {
|
||||
align: Some(RegionAlign::NEAR),
|
||||
decided: [true; 2],
|
||||
..info
|
||||
};
|
||||
// The symbolic box can be unchanged while its parent slot changed
|
||||
@@ -240,7 +219,7 @@ impl UiRenderState {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::PlaceRedraws);
|
||||
let old = self.remove(id, false, rsc);
|
||||
self.draw_at(id, placed, placed_info, RegionAlign::NEAR, old, rsc);
|
||||
self.draw_at(id, placed, placed_info, old, rsc);
|
||||
}
|
||||
|
||||
// The answer is only reusable while both parts of the operation are:
|
||||
@@ -251,11 +230,14 @@ impl UiRenderState {
|
||||
let mut settled = answer;
|
||||
for axis in AXES {
|
||||
let reported = answer.0.axis(axis);
|
||||
let placed_len =
|
||||
match reported.leftover != Weight::ZERO || declared[axis as usize].is_some() {
|
||||
true => Len::FULL,
|
||||
false => Len::from_parts(reported.rel, reported.px),
|
||||
};
|
||||
let placed_len = match fills(
|
||||
reported,
|
||||
declared[axis as usize],
|
||||
info.decided[axis as usize],
|
||||
) {
|
||||
true => Len::FULL,
|
||||
false => Len::from_parts(reported.rel, reported.px),
|
||||
};
|
||||
settled.1[axis as usize] =
|
||||
settled.1[axis as usize].and(drawing_holds[axis as usize].through(placed_len));
|
||||
}
|
||||
@@ -263,9 +245,8 @@ impl UiRenderState {
|
||||
let active = self.active.get_mut(&id).unwrap();
|
||||
active.offer = info.offer;
|
||||
active.answer = settled;
|
||||
active.align = align;
|
||||
active.align_override = info.align.is_some();
|
||||
active.own_align = own_align;
|
||||
active.decided = info.decided;
|
||||
active.own_align = align;
|
||||
active.depth = info.depth;
|
||||
settled
|
||||
}
|
||||
@@ -276,7 +257,6 @@ impl UiRenderState {
|
||||
id: WidgetId,
|
||||
region: UiRegion,
|
||||
info: DrawInfo,
|
||||
align: RegionAlign,
|
||||
old: Option<ActiveData>,
|
||||
rsc: &mut dyn UiRsc,
|
||||
) -> (Size, [Holds; 2]) {
|
||||
@@ -417,7 +397,7 @@ impl UiRenderState {
|
||||
offer: UiRegion::FULL,
|
||||
offered_px: px,
|
||||
slot_wide,
|
||||
align: None,
|
||||
decided: [false; 2],
|
||||
},
|
||||
rsc,
|
||||
);
|
||||
@@ -441,8 +421,7 @@ impl UiRenderState {
|
||||
children,
|
||||
size_deps,
|
||||
declared: declared_lens(rsc.widgets(), id),
|
||||
align,
|
||||
align_override: info.align.is_some(),
|
||||
decided: info.decided,
|
||||
own_align: rsc.widgets().alignment(id),
|
||||
move_idx,
|
||||
parent_move: info.parent_move,
|
||||
@@ -800,8 +779,7 @@ impl UiRenderState {
|
||||
size_deps: Vec::new(),
|
||||
move_idx: info.parent_move,
|
||||
declared: [None; 2],
|
||||
align: RegionAlign::default(),
|
||||
align_override: false,
|
||||
decided: [false; 2],
|
||||
own_align: rsc.widgets().alignment(id),
|
||||
parent_move: info.parent_move,
|
||||
mask: info.mask,
|
||||
@@ -820,7 +798,6 @@ impl UiRenderState {
|
||||
self.answer_invalid.clear();
|
||||
self.replace_answers = false;
|
||||
self.moves.clear();
|
||||
self.root_move = MoveIdx::NONE;
|
||||
self.layers.clear();
|
||||
rsc.widgets_mut().needs_redraw.clear();
|
||||
self.free(rsc);
|
||||
@@ -977,8 +954,8 @@ impl UiRenderState {
|
||||
self.px_region(active.parent_move, region),
|
||||
self.px_region(active.parent_move, asked_in),
|
||||
);
|
||||
let parent_must_place =
|
||||
active.parent.is_some() && (!region_node || active.align_override) && !at_offer;
|
||||
let decided = active.decided.contains(&true);
|
||||
let parent_must_place = active.parent.is_some() && (!region_node || decided) && !at_offer;
|
||||
// An independently positioned region node can redraw at its offer
|
||||
// and move its slot to its own placement. Every other widget needs
|
||||
// its parent to reproduce a different final position.
|
||||
@@ -1000,7 +977,7 @@ impl UiRenderState {
|
||||
offer: active.offer,
|
||||
offered_px,
|
||||
slot_wide: self.moves.compose(active.parent_move, UiRegion::FULL),
|
||||
align: active.align_override.then_some(active.align),
|
||||
decided: active.decided,
|
||||
};
|
||||
let (was_answer, was) = (active.answer, (active.size, active.holds));
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
@@ -1032,7 +1009,7 @@ impl UiRenderState {
|
||||
// the way it did for a region node under a `Stack` once the stack
|
||||
// stopped overriding every child's alignment.
|
||||
let placed_info = DrawInfo {
|
||||
align: Some(RegionAlign::NEAR),
|
||||
decided: [true; 2],
|
||||
..info
|
||||
};
|
||||
self.draw_inner(id, region, placed_info, None, rsc);
|
||||
|
||||
Reference in new issue
Block a user