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
+179
-106
No files matched your search
@@ -84,8 +84,7 @@ pub struct RegionAlign {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RegionAlign {
|
impl RegionAlign {
|
||||||
/// Both axes at the near edge. What a container passes as an override for
|
/// Both axes at the near edge: the start of a box in its own orientation.
|
||||||
/// a child it is going to position itself.
|
|
||||||
pub const NEAR: Self = Self {
|
pub const NEAR: Self = Self {
|
||||||
x: AxisAlign::NEG,
|
x: AxisAlign::NEG,
|
||||||
y: AxisAlign::NEG,
|
y: AxisAlign::NEG,
|
||||||
|
|||||||
@@ -125,6 +125,13 @@ impl Size {
|
|||||||
Axis::Y => self.y,
|
Axis::Y => self.y,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn axis_mut(&mut self, axis: Axis) -> &mut LayoutLen {
|
||||||
|
match axis {
|
||||||
|
Axis::X => &mut self.x,
|
||||||
|
Axis::Y => &mut self.y,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LayoutLen {
|
impl LayoutLen {
|
||||||
@@ -151,6 +158,18 @@ impl LayoutLen {
|
|||||||
Len::from_parts(self.rel.add(share), self.px)
|
Len::from_parts(self.rel.add(share), self.px)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This length, given as a part of a box `len` long, as a part of the
|
||||||
|
/// box `len` is itself a part of. The share is untouched: it is a claim
|
||||||
|
/// on whoever divides the room, not a fraction of anything.
|
||||||
|
pub const fn within_len(self, len: Len) -> Self {
|
||||||
|
let part = Len::from_parts(self.rel, self.px).within_len(len);
|
||||||
|
Self {
|
||||||
|
px: part.px,
|
||||||
|
rel: part.rel,
|
||||||
|
leftover: self.leftover,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn px(px: impl UiNum) -> Self {
|
pub fn px(px: impl UiNum) -> Self {
|
||||||
Self {
|
Self {
|
||||||
px: Px::from_num(px),
|
px: Px::from_num(px),
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ impl Len {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn within_len(&self, len: Len) -> Self {
|
pub const fn within_len(&self, len: Len) -> Self {
|
||||||
self.within(&UiSpan {
|
self.within(&UiSpan {
|
||||||
start: Len::ZERO,
|
start: Len::ZERO,
|
||||||
end: len,
|
end: len,
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ struct MoveOffset {
|
|||||||
// belongs to the pixel above it. Flooring the product instead drops a pixel
|
// belongs to the pixel above it. Flooring the product instead drops a pixel
|
||||||
// wherever a fraction divides a window exactly: a fifth of 1920 comes out of
|
// wherever a fraction divides a window exactly: a fifth of 1920 comes out of
|
||||||
// `REL_STEP` as 383.99998, and five tabs each lose their last column.
|
// `REL_STEP` as 383.99998, and five tabs each lose their last column.
|
||||||
|
//
|
||||||
|
// Taken over the whole coordinate, fraction and pixels summed, since a floor
|
||||||
|
// does not distribute over a sum: floored apart, a half of one and a half of
|
||||||
|
// the other lose the pixel the two together make.
|
||||||
fn snap_floor(v: vec2<f32>) -> vec2<f32> {
|
fn snap_floor(v: vec2<f32>) -> vec2<f32> {
|
||||||
return floor(v + PX_STEP * 0.5);
|
return floor(v + PX_STEP * 0.5);
|
||||||
}
|
}
|
||||||
@@ -147,8 +151,8 @@ fn vs_main(
|
|||||||
let bot_right_rel = vec2(r.x.end.rel, r.y.end.rel);
|
let bot_right_rel = vec2(r.x.end.rel, r.y.end.rel);
|
||||||
let bot_right_px = vec2(r.x.end.px, r.y.end.px);
|
let bot_right_px = vec2(r.x.end.px, r.y.end.px);
|
||||||
|
|
||||||
let top_left = snap_floor(top_left_rel * window.dim) + snap_floor(top_left_px);
|
let top_left = snap_floor(top_left_rel * window.dim + top_left_px);
|
||||||
let bot_right = snap_floor(bot_right_rel * window.dim) + snap_floor(bot_right_px);
|
let bot_right = snap_floor(bot_right_rel * window.dim + bot_right_px);
|
||||||
let size = bot_right - top_left;
|
let size = bot_right - top_left;
|
||||||
|
|
||||||
let uv = vec2<f32>(
|
let uv = vec2<f32>(
|
||||||
@@ -179,8 +183,8 @@ fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> {
|
|||||||
let br = vec2(m.x.end.rel, m.y.end.rel);
|
let br = vec2(m.x.end.rel, m.y.end.rel);
|
||||||
let br_px = vec2(m.x.end.px, m.y.end.px);
|
let br_px = vec2(m.x.end.px, m.y.end.px);
|
||||||
|
|
||||||
let top_left = snap_floor(tl * window.dim) + snap_floor(tl_px);
|
let top_left = snap_floor(tl * window.dim + tl_px);
|
||||||
let bot_right = snap_floor(br * window.dim) + snap_floor(br_px);
|
let bot_right = snap_floor(br * window.dim + br_px);
|
||||||
let pos = in.clip_position.xy;
|
let pos = in.clip_position.xy;
|
||||||
if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y {
|
if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y {
|
||||||
return color * 0.0;
|
return color * 0.0;
|
||||||
|
|||||||
@@ -41,14 +41,11 @@ pub struct ActiveData {
|
|||||||
/// A change to one moves a box this widget cannot fix by drawing again,
|
/// A change to one moves a box this widget cannot fix by drawing again,
|
||||||
/// and comparing them is what says so.
|
/// and comparing them is what says so.
|
||||||
pub declared: [Option<LayoutLen>; 2],
|
pub declared: [Option<LayoutLen>; 2],
|
||||||
/// The alignment its parent asked it with. A local redraw repeats that
|
/// The axes along which its parent chose its box from its own answer,
|
||||||
/// question, including an override chosen by a container.
|
/// so a local redraw asks the question its parent asked.
|
||||||
pub align: RegionAlign,
|
pub decided: [bool; 2],
|
||||||
/// Whether that alignment was the parent's override rather than the
|
/// Its alignment when it was last drawn, which a change to the property
|
||||||
/// widget's own property.
|
/// is found against.
|
||||||
pub align_override: bool,
|
|
||||||
/// Its own alignment when it was last drawn. A change to the property is
|
|
||||||
/// found against this even when its parent overrode the alignment.
|
|
||||||
pub own_align: RegionAlign,
|
pub own_align: RegionAlign,
|
||||||
/// The movable region whose coordinates `region` uses.
|
/// The movable region whose coordinates `region` uses.
|
||||||
pub parent_move: MoveIdx,
|
pub parent_move: MoveIdx,
|
||||||
|
|||||||
+56
-20
@@ -135,30 +135,34 @@ impl<'a> Painter<'a> {
|
|||||||
id: &'s StrongWidget<W>,
|
id: &'s StrongWidget<W>,
|
||||||
region: UiRegion,
|
region: UiRegion,
|
||||||
) -> DrawResult<'s, 'a, W> {
|
) -> DrawResult<'s, 'a, W> {
|
||||||
self.widget_at(id, region, None)
|
self.widget_at(id, region, [false; 2])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draws a widget with an alignment chosen by its container rather than
|
/// Draws a widget in a box this widget chose from the widget's own
|
||||||
/// the widget's property. Containers use this when the box they hand down
|
/// answer along the `decided` axes. On those the answer is not placed
|
||||||
/// already expresses the size they report around the child.
|
/// inside the box again: it already is the box, and a fraction the
|
||||||
pub fn widget_aligned<'s, W: ?Sized>(
|
/// widget reported of its offer, taken of this box a second time, would
|
||||||
|
/// shrink it twice. A container uses this where it hands back exactly
|
||||||
|
/// what a child asked for -- a span placing a child at the length it
|
||||||
|
/// reported, a scroll giving its content the content's own length.
|
||||||
|
pub fn widget_decided<'s, W: ?Sized>(
|
||||||
&'s mut self,
|
&'s mut self,
|
||||||
id: &'s StrongWidget<W>,
|
id: &'s StrongWidget<W>,
|
||||||
region: UiRegion,
|
region: UiRegion,
|
||||||
align: RegionAlign,
|
decided: [bool; 2],
|
||||||
) -> DrawResult<'s, 'a, W> {
|
) -> DrawResult<'s, 'a, W> {
|
||||||
self.widget_at(id, region, Some(align))
|
self.widget_at(id, region, decided)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn widget_at<'s, W: ?Sized>(
|
fn widget_at<'s, W: ?Sized>(
|
||||||
&'s mut self,
|
&'s mut self,
|
||||||
id: &'s StrongWidget<W>,
|
id: &'s StrongWidget<W>,
|
||||||
region: UiRegion,
|
region: UiRegion,
|
||||||
align_override: Option<RegionAlign>,
|
decided: [bool; 2],
|
||||||
) -> DrawResult<'s, 'a, W> {
|
) -> DrawResult<'s, 'a, W> {
|
||||||
let region_node = self.rsc.widgets().is_region_node(id.id());
|
let region_node = self.rsc.widgets().is_region_node(id.id());
|
||||||
let declared = self.declared_lens(id);
|
let declared = self.declared_lens(id);
|
||||||
let align = align_override.unwrap_or_else(|| self.rsc.widgets().alignment(id.id()));
|
let align = self.rsc.widgets().alignment(id.id());
|
||||||
// Composing `FULL` through a box is not quite the identity in f32,
|
// Composing `FULL` through a box is not quite the identity in f32,
|
||||||
// so a child with nothing declared keeps the box it would have had.
|
// so a child with nothing declared keeps the box it would have had.
|
||||||
let local = match declared.iter().any(Option::is_some) {
|
let local = match declared.iter().any(Option::is_some) {
|
||||||
@@ -200,7 +204,7 @@ impl<'a> Painter<'a> {
|
|||||||
offer,
|
offer,
|
||||||
offered_px: self.px_within_offer(offer),
|
offered_px: self.px_within_offer(offer),
|
||||||
slot_wide: self.slot_wide,
|
slot_wide: self.slot_wide,
|
||||||
align: align_override,
|
decided,
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
self.rsc,
|
self.rsc,
|
||||||
@@ -216,7 +220,7 @@ impl<'a> Painter<'a> {
|
|||||||
DrawResult {
|
DrawResult {
|
||||||
child: id,
|
child: id,
|
||||||
painter: self,
|
painter: self,
|
||||||
size,
|
size: in_parent_frame(size, local, declared),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,7 +286,7 @@ impl<'a> Painter<'a> {
|
|||||||
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) {
|
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) {
|
||||||
*under = under.and(holds[axis as usize].through(local.axis(axis).len()));
|
*under = under.and(holds[axis as usize].through(local.axis(axis).len()));
|
||||||
}
|
}
|
||||||
Some(size.axis(axis))
|
Some(in_parent_frame(size, local, declared).axis(axis))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether this is the first box a child is asked about in during a draw
|
/// Whether this is the first box a child is asked about in during a draw
|
||||||
@@ -393,7 +397,13 @@ impl<'a> Painter<'a> {
|
|||||||
/// near edge. A container that reports one child's size gives every child
|
/// near edge. A container that reports one child's size gives every child
|
||||||
/// this, so what it draws is inside what it says it occupies.
|
/// this, so what it draws is inside what it says it occupies.
|
||||||
pub fn box_of(&self, size: Size) -> UiRegion {
|
pub fn box_of(&self, size: Size) -> UiRegion {
|
||||||
placed_box(UiRegion::FULL, size, RegionAlign::NEAR, [None; 2])
|
placed_box(
|
||||||
|
UiRegion::FULL,
|
||||||
|
size,
|
||||||
|
RegionAlign::NEAR,
|
||||||
|
[None; 2],
|
||||||
|
[false; 2],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This widget's box in pixels. Reading it makes the drawing one that
|
/// This widget's box in pixels. Reading it makes the drawing one that
|
||||||
@@ -517,6 +527,23 @@ impl PrimitiveLike for &TextureHandle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A child's answer as lengths of the box it was asked from. A widget reports
|
||||||
|
/// a fraction of the box it was given, and the widget that gave it wants the
|
||||||
|
/// same length as a fraction of its own: one composition apart wherever the
|
||||||
|
/// offer was not the whole of the parent's extent, as a span's is after a
|
||||||
|
/// relative child. A declared axis is already the parent's: it resolved the
|
||||||
|
/// rule in its own box, and the rule is what the report says.
|
||||||
|
fn in_parent_frame(size: Size, local: UiRegion, declared: [Option<LayoutLen>; 2]) -> Size {
|
||||||
|
let mut size = size;
|
||||||
|
for (axis, declared) in AXES.into_iter().zip(declared) {
|
||||||
|
if declared.is_none() {
|
||||||
|
let len = local.axis(axis).len();
|
||||||
|
*size.axis_mut(axis) = size.axis(axis).within_len(len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
size
|
||||||
|
}
|
||||||
|
|
||||||
/// What a widget declares a length of its box to be. `leftover` is not one: a
|
/// What a widget declares a length of its box to be. `leftover` is not one: a
|
||||||
/// share of what is left over is only a length to the widget dividing one,
|
/// share of what is left over is only a length to the widget dividing one,
|
||||||
/// so it passes up in the size instead.
|
/// so it passes up in the size instead.
|
||||||
@@ -537,12 +564,20 @@ pub(crate) fn declared_lens(widgets: &Widgets, id: WidgetId) -> [Option<LayoutLe
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether what a widget reported along an axis is the whole of the box it
|
||||||
|
/// is in rather than a part to be placed inside it. A share fills, because a
|
||||||
|
/// share is a length only to whoever divides one, and whoever did is the one
|
||||||
|
/// that handed down this box. A declared axis does too: `declared_box`
|
||||||
|
/// already placed it, in the parent's box, and the rule's length is what the
|
||||||
|
/// widget reports there. And an axis the parent decided from the answer is
|
||||||
|
/// the answer already.
|
||||||
|
pub(crate) fn fills(reported: LayoutLen, declared: Option<LayoutLen>, decided: bool) -> bool {
|
||||||
|
reported.leftover != Weight::ZERO || declared.is_some() || decided
|
||||||
|
}
|
||||||
|
|
||||||
/// The box a drawing occupies: the size the widget reported, on the side of
|
/// The box a drawing occupies: the size the widget reported, on the side of
|
||||||
/// the box it was asked in that its alignment says. An axis reported as a
|
/// the box it was asked in that its alignment says, on every axis that is
|
||||||
/// share fills, because a share is a length only to whoever divides one, and
|
/// not simply filled.
|
||||||
/// whoever did is the one that handed down this box. A declared axis is
|
|
||||||
/// left alone too: `declared_box` already placed it, in the parent's box,
|
|
||||||
/// and the rule's length is what the widget reports there.
|
|
||||||
///
|
///
|
||||||
/// A reported fraction is a fraction of the box the widget drew in, where a
|
/// A reported fraction is a fraction of the box the widget drew in, where a
|
||||||
/// declared one is a fraction of the box its parent handed down -- a span
|
/// declared one is a fraction of the box its parent handed down -- a span
|
||||||
@@ -553,11 +588,12 @@ pub(crate) fn placed_box(
|
|||||||
size: Size,
|
size: Size,
|
||||||
align: RegionAlign,
|
align: RegionAlign,
|
||||||
declared: [Option<LayoutLen>; 2],
|
declared: [Option<LayoutLen>; 2],
|
||||||
|
decided: [bool; 2],
|
||||||
) -> UiRegion {
|
) -> UiRegion {
|
||||||
let mut placed = region;
|
let mut placed = region;
|
||||||
for (axis, declared) in AXES.into_iter().zip(declared) {
|
for (axis, (declared, decided)) in AXES.into_iter().zip(declared.into_iter().zip(decided)) {
|
||||||
let reported = size.axis(axis);
|
let reported = size.axis(axis);
|
||||||
if reported.leftover != Weight::ZERO || declared.is_some() {
|
if fills(reported, declared, decided) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let span = placed.axis_mut(axis);
|
let span = placed.axis_mut(axis);
|
||||||
|
|||||||
+36
-59
@@ -1,10 +1,10 @@
|
|||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind};
|
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::{
|
use crate::{
|
||||||
ActiveData, Axis, DrawLayers, Holds, IdLike, LayoutLen, Len, MaskIdx, MoveIdx, Moves, Painter,
|
ActiveData, Axis, DrawLayers, Holds, IdLike, LayoutLen, Len, MaskIdx, MoveIdx, Moves, Painter,
|
||||||
PixelRegion, Px, PxVec2, RegionAlign, Rel, Size, StrongWidget, UiRegion, UiRsc, UiSpan, Weight,
|
PixelRegion, Px, PxVec2, Rel, Size, StrongWidget, UiRegion, UiRsc, UiSpan, Weight, WideRegion,
|
||||||
WideRegion, WidgetId, Widgets,
|
WidgetId, Widgets,
|
||||||
util::{HashMap, Vec2},
|
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
|
/// 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.
|
/// box is one step further and not a walk back up the chain.
|
||||||
pub slot_wide: WideRegion,
|
pub slot_wide: WideRegion,
|
||||||
/// A container's answer for where the widget sits. `None` uses the
|
/// The axes along which the parent chose this box from the widget's own
|
||||||
/// widget's own property.
|
/// answer, so the answer is not placed inside it again. See
|
||||||
pub align: Option<RegionAlign>,
|
/// [`Painter::widget_decided`].
|
||||||
|
pub decided: [bool; 2],
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct UiRenderState {
|
pub struct UiRenderState {
|
||||||
@@ -38,8 +39,6 @@ pub struct UiRenderState {
|
|||||||
pub(super) output_size: PxVec2,
|
pub(super) output_size: PxVec2,
|
||||||
|
|
||||||
old_root: Option<WidgetId>,
|
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
|
/// Whether the output has changed since the last update. A frame is
|
||||||
/// owed for that whether or not anything has to be drawn again.
|
/// owed for that whether or not anything has to be drawn again.
|
||||||
resized: bool,
|
resized: bool,
|
||||||
@@ -67,35 +66,22 @@ impl UiRenderState {
|
|||||||
answer_invalid: Default::default(),
|
answer_invalid: Default::default(),
|
||||||
replace_answers: false,
|
replace_answers: false,
|
||||||
moves: Default::default(),
|
moves: Default::default(),
|
||||||
root_move: MoveIdx::NONE,
|
|
||||||
resized: false,
|
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
|
/// 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>) {
|
pub fn resize(&mut self, size: impl Into<Vec2>) {
|
||||||
let size = PxVec2::from_f32(size.into());
|
let size = PxVec2::from_f32(size.into());
|
||||||
if size == self.output_size {
|
if size == self.output_size {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.output_size = size;
|
self.output_size = size;
|
||||||
self.write_root();
|
|
||||||
self.resized = true;
|
self.resized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,13 +90,13 @@ impl UiRenderState {
|
|||||||
layer: 0,
|
layer: 0,
|
||||||
parent: None,
|
parent: None,
|
||||||
depth: 1,
|
depth: 1,
|
||||||
parent_move: self.root_move,
|
parent_move: MoveIdx::NONE,
|
||||||
region_node: false,
|
region_node: false,
|
||||||
mask: MaskIdx::NONE,
|
mask: MaskIdx::NONE,
|
||||||
offer: UiRegion::FULL,
|
offer: UiRegion::FULL,
|
||||||
offered_px: self.output_size,
|
offered_px: self.output_size,
|
||||||
slot_wide: self.moves.compose(self.root_move, UiRegion::FULL),
|
slot_wide: WideRegion::of(UiRegion::FULL),
|
||||||
align: None,
|
decided: [false; 2],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,8 +153,6 @@ impl UiRenderState {
|
|||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
let _layout = diag::timer(TimerKind::FullLayout);
|
let _layout = diag::timer(TimerKind::FullLayout);
|
||||||
self.clear(rsc);
|
self.clear(rsc);
|
||||||
// free all resources & cache
|
|
||||||
self.write_root();
|
|
||||||
if let Some(id) = root {
|
if let Some(id) = root {
|
||||||
let info = self.root_info();
|
let info = self.root_info();
|
||||||
let region = Self::root_region(id.id(), rsc.widgets());
|
let region = Self::root_region(id.id(), rsc.widgets());
|
||||||
@@ -203,8 +187,7 @@ impl UiRenderState {
|
|||||||
info.region_node,
|
info.region_node,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let own_align = rsc.widgets().alignment(id);
|
let align = rsc.widgets().alignment(id);
|
||||||
let align = info.align.unwrap_or(own_align);
|
|
||||||
let replace_answer = self.answer_invalid.remove(&id)
|
let replace_answer = self.answer_invalid.remove(&id)
|
||||||
|| (self.replace_answers
|
|| (self.replace_answers
|
||||||
&& (rsc.widgets().needs_redraw.contains(&id)
|
&& (rsc.widgets().needs_redraw.contains(&id)
|
||||||
@@ -219,19 +202,15 @@ impl UiRenderState {
|
|||||||
if old.is_none() {
|
if old.is_none() {
|
||||||
old = self.remove(id, false, rsc);
|
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);
|
let declared = declared_lens(rsc.widgets(), id);
|
||||||
// A near-edge override means the caller already chose this box from
|
// The second, final ask is in a box chosen from the answer on both
|
||||||
// the child's answer. Applying the answer again would compound the
|
// axes, which is also what makes it terminate.
|
||||||
// placement; it is also how the second, final ask terminates.
|
let placed = placed_box(region, answer.0, align, declared, info.decided);
|
||||||
let placed = match info.align == Some(RegionAlign::NEAR) {
|
|
||||||
true => region,
|
|
||||||
false => placed_box(region, answer.0, align, declared),
|
|
||||||
};
|
|
||||||
let placed_info = DrawInfo {
|
let placed_info = DrawInfo {
|
||||||
align: Some(RegionAlign::NEAR),
|
decided: [true; 2],
|
||||||
..info
|
..info
|
||||||
};
|
};
|
||||||
// The symbolic box can be unchanged while its parent slot changed
|
// The symbolic box can be unchanged while its parent slot changed
|
||||||
@@ -240,7 +219,7 @@ impl UiRenderState {
|
|||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
diag::bump(Counter::PlaceRedraws);
|
diag::bump(Counter::PlaceRedraws);
|
||||||
let old = self.remove(id, false, rsc);
|
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:
|
// The answer is only reusable while both parts of the operation are:
|
||||||
@@ -251,8 +230,11 @@ impl UiRenderState {
|
|||||||
let mut settled = answer;
|
let mut settled = answer;
|
||||||
for axis in AXES {
|
for axis in AXES {
|
||||||
let reported = answer.0.axis(axis);
|
let reported = answer.0.axis(axis);
|
||||||
let placed_len =
|
let placed_len = match fills(
|
||||||
match reported.leftover != Weight::ZERO || declared[axis as usize].is_some() {
|
reported,
|
||||||
|
declared[axis as usize],
|
||||||
|
info.decided[axis as usize],
|
||||||
|
) {
|
||||||
true => Len::FULL,
|
true => Len::FULL,
|
||||||
false => Len::from_parts(reported.rel, reported.px),
|
false => Len::from_parts(reported.rel, reported.px),
|
||||||
};
|
};
|
||||||
@@ -263,9 +245,8 @@ impl UiRenderState {
|
|||||||
let active = self.active.get_mut(&id).unwrap();
|
let active = self.active.get_mut(&id).unwrap();
|
||||||
active.offer = info.offer;
|
active.offer = info.offer;
|
||||||
active.answer = settled;
|
active.answer = settled;
|
||||||
active.align = align;
|
active.decided = info.decided;
|
||||||
active.align_override = info.align.is_some();
|
active.own_align = align;
|
||||||
active.own_align = own_align;
|
|
||||||
active.depth = info.depth;
|
active.depth = info.depth;
|
||||||
settled
|
settled
|
||||||
}
|
}
|
||||||
@@ -276,7 +257,6 @@ impl UiRenderState {
|
|||||||
id: WidgetId,
|
id: WidgetId,
|
||||||
region: UiRegion,
|
region: UiRegion,
|
||||||
info: DrawInfo,
|
info: DrawInfo,
|
||||||
align: RegionAlign,
|
|
||||||
old: Option<ActiveData>,
|
old: Option<ActiveData>,
|
||||||
rsc: &mut dyn UiRsc,
|
rsc: &mut dyn UiRsc,
|
||||||
) -> (Size, [Holds; 2]) {
|
) -> (Size, [Holds; 2]) {
|
||||||
@@ -417,7 +397,7 @@ impl UiRenderState {
|
|||||||
offer: UiRegion::FULL,
|
offer: UiRegion::FULL,
|
||||||
offered_px: px,
|
offered_px: px,
|
||||||
slot_wide,
|
slot_wide,
|
||||||
align: None,
|
decided: [false; 2],
|
||||||
},
|
},
|
||||||
rsc,
|
rsc,
|
||||||
);
|
);
|
||||||
@@ -441,8 +421,7 @@ impl UiRenderState {
|
|||||||
children,
|
children,
|
||||||
size_deps,
|
size_deps,
|
||||||
declared: declared_lens(rsc.widgets(), id),
|
declared: declared_lens(rsc.widgets(), id),
|
||||||
align,
|
decided: info.decided,
|
||||||
align_override: info.align.is_some(),
|
|
||||||
own_align: rsc.widgets().alignment(id),
|
own_align: rsc.widgets().alignment(id),
|
||||||
move_idx,
|
move_idx,
|
||||||
parent_move: info.parent_move,
|
parent_move: info.parent_move,
|
||||||
@@ -800,8 +779,7 @@ impl UiRenderState {
|
|||||||
size_deps: Vec::new(),
|
size_deps: Vec::new(),
|
||||||
move_idx: info.parent_move,
|
move_idx: info.parent_move,
|
||||||
declared: [None; 2],
|
declared: [None; 2],
|
||||||
align: RegionAlign::default(),
|
decided: [false; 2],
|
||||||
align_override: false,
|
|
||||||
own_align: rsc.widgets().alignment(id),
|
own_align: rsc.widgets().alignment(id),
|
||||||
parent_move: info.parent_move,
|
parent_move: info.parent_move,
|
||||||
mask: info.mask,
|
mask: info.mask,
|
||||||
@@ -820,7 +798,6 @@ impl UiRenderState {
|
|||||||
self.answer_invalid.clear();
|
self.answer_invalid.clear();
|
||||||
self.replace_answers = false;
|
self.replace_answers = false;
|
||||||
self.moves.clear();
|
self.moves.clear();
|
||||||
self.root_move = MoveIdx::NONE;
|
|
||||||
self.layers.clear();
|
self.layers.clear();
|
||||||
rsc.widgets_mut().needs_redraw.clear();
|
rsc.widgets_mut().needs_redraw.clear();
|
||||||
self.free(rsc);
|
self.free(rsc);
|
||||||
@@ -977,8 +954,8 @@ impl UiRenderState {
|
|||||||
self.px_region(active.parent_move, region),
|
self.px_region(active.parent_move, region),
|
||||||
self.px_region(active.parent_move, asked_in),
|
self.px_region(active.parent_move, asked_in),
|
||||||
);
|
);
|
||||||
let parent_must_place =
|
let decided = active.decided.contains(&true);
|
||||||
active.parent.is_some() && (!region_node || active.align_override) && !at_offer;
|
let parent_must_place = active.parent.is_some() && (!region_node || decided) && !at_offer;
|
||||||
// An independently positioned region node can redraw at its offer
|
// An independently positioned region node can redraw at its offer
|
||||||
// and move its slot to its own placement. Every other widget needs
|
// and move its slot to its own placement. Every other widget needs
|
||||||
// its parent to reproduce a different final position.
|
// its parent to reproduce a different final position.
|
||||||
@@ -1000,7 +977,7 @@ impl UiRenderState {
|
|||||||
offer: active.offer,
|
offer: active.offer,
|
||||||
offered_px,
|
offered_px,
|
||||||
slot_wide: self.moves.compose(active.parent_move, UiRegion::FULL),
|
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));
|
let (was_answer, was) = (active.answer, (active.size, active.holds));
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
@@ -1032,7 +1009,7 @@ impl UiRenderState {
|
|||||||
// the way it did for a region node under a `Stack` once the stack
|
// the way it did for a region node under a `Stack` once the stack
|
||||||
// stopped overriding every child's alignment.
|
// stopped overriding every child's alignment.
|
||||||
let placed_info = DrawInfo {
|
let placed_info = DrawInfo {
|
||||||
align: Some(RegionAlign::NEAR),
|
decided: [true; 2],
|
||||||
..info
|
..info
|
||||||
};
|
};
|
||||||
self.draw_inner(id, region, placed_info, None, rsc);
|
self.draw_inner(id, region, placed_info, None, rsc);
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ impl Widget for Scroll {
|
|||||||
region = region.offset(offset);
|
region = region.offset(offset);
|
||||||
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
|
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
|
||||||
}
|
}
|
||||||
painter.widget_aligned(&self.inner, region, RegionAlign::NEAR);
|
painter.widget_decided(&self.inner, region, [true; 2]);
|
||||||
// What it occupies is its box, on both axes: it clips its content to
|
// What it occupies is its box, on both axes: it clips its content to
|
||||||
// that box, so it can neither take less of one nor honestly ask for
|
// that box, so it can neither take less of one nor honestly ask for
|
||||||
// more. The content's length is what it scrolls through, not what it
|
// more. The content's length is what it scrolls through, not what it
|
||||||
|
|||||||
@@ -119,7 +119,10 @@ impl Widget for Span {
|
|||||||
if self.dir.sign == Sign::Neg {
|
if self.dir.sign == Sign::Neg {
|
||||||
region.flip(axis);
|
region.flip(axis);
|
||||||
}
|
}
|
||||||
let placed = painter.widget_within(child, region);
|
// Along the row this box is the child's own answer, so the answer
|
||||||
|
// is not placed in it again; across it the child sits where its
|
||||||
|
// alignment says.
|
||||||
|
let placed = painter.widget_decided(child, region, [axis == Axis::X, axis == Axis::Y]);
|
||||||
if shrinks {
|
if shrinks {
|
||||||
let used = placed.len(!axis);
|
let used = placed.len(!axis);
|
||||||
// Choosing between a fixed and a relative length from the
|
// Choosing between a fixed and a relative length from the
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ impl Widget for Stack {
|
|||||||
// child is handed a box that owes nothing to its own answer, and
|
// child is handed a box that owes nothing to its own answer, and
|
||||||
// where it sits in one bigger than itself is its own business.
|
// where it sits in one bigger than itself is its own business.
|
||||||
match sizing == Some(i) {
|
match sizing == Some(i) {
|
||||||
true => painter.widget_aligned(child, region, RegionAlign::NEAR),
|
true => painter.widget_decided(child, region, [true; 2]),
|
||||||
false => painter.widget_within(child, region),
|
false => painter.widget_within(child, region),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-9
@@ -20,6 +20,45 @@ fn a_span_gives_each_child_the_width_it_asked_for() {
|
|||||||
assert_corners!(h, right, (100, 0), (400, 200));
|
assert_corners!(h, right, (100, 0), (400, 200));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A drawn child reports a fraction of the box it was given, and a span
|
||||||
|
/// offers each child what is left after the one before. So a nested span
|
||||||
|
/// that takes half of the half it was offered has taken a quarter of the row,
|
||||||
|
/// and what follows starts three quarters along -- not at the end, which is
|
||||||
|
/// where adding its report straight into the cursor put it.
|
||||||
|
#[test]
|
||||||
|
fn a_span_reads_a_child_report_as_a_fraction_of_what_it_offered() {
|
||||||
|
let mut h = Harness::new((400, 100));
|
||||||
|
let half = rect(Color::RED).width(rel(0.5)).add(&mut h.rsc);
|
||||||
|
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc);
|
||||||
|
let nested = (inner,).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
let tail = rect(Color::BLUE).width(100).add(&mut h.rsc);
|
||||||
|
h.set_root((half, nested, tail).span(Dir::RIGHT).width(rel(1.0)));
|
||||||
|
|
||||||
|
// The nested span is placed at the length it reported and drawn there
|
||||||
|
// once more; half of that final box is what its child takes, packed at
|
||||||
|
// the nested span's own start.
|
||||||
|
assert_corners!(h, nested, (200, 0), (300, 100));
|
||||||
|
assert_corners!(h, inner, (200, 0), (250, 100));
|
||||||
|
assert_corners!(h, tail, (300, 0), (400, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The same reading through a pad: its inset is the whole box less the
|
||||||
|
/// padding, so half of the inset plus the padding is half the box plus one
|
||||||
|
/// padding, not two.
|
||||||
|
#[test]
|
||||||
|
fn a_pad_reports_a_fraction_of_its_inset_as_a_fraction_of_its_box() {
|
||||||
|
let mut h = Harness::new((400, 100));
|
||||||
|
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc);
|
||||||
|
let padded = (inner,).span(Dir::RIGHT).pad(10).add(&mut h.rsc);
|
||||||
|
let tail = rect(Color::BLUE).width(100).add(&mut h.rsc);
|
||||||
|
// Ruled to the window: a root reporting a fraction of it is otherwise
|
||||||
|
// placed inside it by its own alignment, which is not what is under test.
|
||||||
|
h.set_root((padded, tail).span(Dir::RIGHT).width(rel(1.0)));
|
||||||
|
|
||||||
|
assert_corners!(h, padded, (0, 0), (210, 100));
|
||||||
|
assert_corners!(h, tail, (210, 0), (310, 100));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_span_ruled_across_itself_does_not_measure_its_children_there() {
|
fn a_span_ruled_across_itself_does_not_measure_its_children_there() {
|
||||||
let mut h = Harness::new((400, 200));
|
let mut h = Harness::new((400, 200));
|
||||||
@@ -225,21 +264,21 @@ fn only_a_region_node_lengthens_the_chain_and_it_can_be_removed() {
|
|||||||
h.set_root((bar, buried).span(Dir::RIGHT));
|
h.set_root((bar, buried).span(Dir::RIGHT));
|
||||||
|
|
||||||
let move_idx = h.render.active[&leaf.id()].parent_move;
|
let move_idx = h.render.active[&leaf.id()].parent_move;
|
||||||
assert_eq!(h.render.moves.depth(move_idx), 1, "only the root region");
|
assert_eq!(h.render.moves.depth(move_idx), 0, "the window is no entry");
|
||||||
|
|
||||||
h.rsc.widgets_mut().set_region_node(buried, true);
|
h.rsc.widgets_mut().set_region_node(buried, true);
|
||||||
h.frame();
|
h.frame();
|
||||||
let move_idx = h.render.active[&leaf.id()].parent_move;
|
let move_idx = h.render.active[&leaf.id()].parent_move;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
h.render.moves.depth(move_idx),
|
h.render.moves.depth(move_idx),
|
||||||
2,
|
1,
|
||||||
"the opted-in widget's region and the root region"
|
"the opted-in widget's region alone"
|
||||||
);
|
);
|
||||||
|
|
||||||
h.rsc.widgets_mut().set_region_node(buried, false);
|
h.rsc.widgets_mut().set_region_node(buried, false);
|
||||||
h.frame();
|
h.frame();
|
||||||
let move_idx = h.render.active[&leaf.id()].parent_move;
|
let move_idx = h.render.active[&leaf.id()].parent_move;
|
||||||
assert_eq!(h.render.moves.depth(move_idx), 1);
|
assert_eq!(h.render.moves.depth(move_idx), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A span that sizes from its children passes their `leftover` weight up
|
/// A span that sizes from its children passes their `leftover` weight up
|
||||||
@@ -341,16 +380,15 @@ fn a_row_of_equal_shares_fills_it_exactly() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Where the shader puts an edge: the two parts of a scalar are floored
|
/// Where the shader puts an edge: the fraction resolved against the window
|
||||||
/// apart, so a fraction and a pixel offset snap independently, and each is
|
/// plus the pixel offset, taken to the boundary it composes to within half
|
||||||
/// taken to the boundary it composes to within half a step of. Kept in step
|
/// a step of. Kept in step with `snap_floor` in `prelude.wgsl`.
|
||||||
/// with `snap_floor` in `prelude.wgsl`.
|
|
||||||
fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) {
|
fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) {
|
||||||
let active = &h.render.active[&id];
|
let active = &h.render.active[&id];
|
||||||
let region = h.render.moves.resolve(active.parent_move, active.region);
|
let region = h.render.moves.resolve(active.parent_move, active.region);
|
||||||
let dim = h.size().axis(axis);
|
let dim = h.size().axis(axis);
|
||||||
let snap = |v: f32| (v + Px::STEP.to_f32() * 0.5).floor();
|
let snap = |v: f32| (v + Px::STEP.to_f32() * 0.5).floor();
|
||||||
let edge = |s: Len| snap(s.rel.to_f32() * dim) + snap(s.px.to_f32());
|
let edge = |s: Len| snap(s.rel.to_f32() * dim + s.px.to_f32());
|
||||||
let span = region.axis(axis);
|
let span = region.axis(axis);
|
||||||
(edge(span.start), edge(span.end))
|
(edge(span.start), edge(span.end))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user