Compare commits

...
2 Commits
Author SHA1 Message Date
iris-aiandClaude Opus 5 2807a925af Make a declared length one that cannot carry a share
`declared_lens` filtered `leftover` out of both its sources and every
consumer then re-dropped it, so the rule lived in two filters and a comment.
A declaration is a `Len`: `LayoutLen::declared` states the rule once and both
sources go through it, and `Declared` replaces the bare two-element array on
`ActiveData` and in four signatures.

The two sources stay one value deliberately. A rule decides the child's box;
a hint only promises what it will report -- but `size_hint` is by contract an
exact answer with no painter context, and `hints_agree` fails a widget that
draws something else, so narrowing the box to a hint cannot change what is
drawn. Every consumer asks about the length, never which said it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 21:06:31 -04:00
iris-aiandClaude Opus 5 55df32a33c Say layout's operations by name, and index a pair by its axis
Four rounds over the same idea: an expression that needed a comment to say
what it computed wanted to be a named operation.

The placement description is built by chaining off the value that says it.
`UiSpan::within_desc`/`shifted_desc` and `Len::as_desc` replace the
`PlaceDescAxis::` constructors, `PlaceDescAxis::axis` lifts one axis into a
pair with the whole box across it, and `PlaceDesc::per_axis` covers the case
where the two axes differ. `beside` is dropped: `from_axis` already said it.

Seven module-level functions become methods on the value each took first --
`Widgets::declared_lens`, `LayoutLen::fills`, `PlaceDesc::placement` and
`::rel_base_and_region`, `Size::within_box`, `UiRegion::at_origin` and
`::as_translation`.

`UiSpan::place` is the aligned-placement rule, which was written out three
times; `LayoutLen::without_leftover` is the sibling `apply_leftover` never
had, at six sites; `is_px` and `is_only_leftover` name field comparisons the
surrounding comments had to translate; `Holds::covers` was interval
containment spelled out by hand. A span's `shared` loses the two arguments
that did not vary across its loop.

`LayoutHolds` was four two-element arrays where every other pair here is a
struct of two per-axis values, so nothing it did could be written once.
It becomes `AxisHolds` on `x` and `y`, and `and`, `covers` and `contains`
lose their loops.

Every pair gets `Index<Axis>`/`IndexMut<Axis>` through one macro, and the
eighteen `axis`/`axis_mut` methods go. `const_index` keeps the accessors
usable in const context.

Cold layout is unchanged: `layout_dump` over 400 depth-5 trees is identical
to 58ce74d byte for byte, across all 34,492 boxes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 20:56:59 -04:00
22 changed files with 475 additions and 477 deletions

No files matched your search

+1
View File
@@ -9,6 +9,7 @@
#![feature(unsize)]
#![feature(coerce_unsized)]
#![feature(option_into_flat_iter)]
#![feature(const_index)]
#[cfg(feature = "layout-diagnostics")]
pub mod layout_diagnostics;
+3 -14
View File
@@ -1,3 +1,4 @@
use crate::util::impl_axis_index;
use crate::{Px, Rel};
use super::*;
@@ -89,20 +90,6 @@ impl RegionAlign {
x: AxisAlign::NEG,
y: AxisAlign::NEG,
};
pub fn axis(&self, axis: Axis) -> AxisAlign {
match axis {
Axis::X => self.x,
Axis::Y => self.y,
}
}
pub fn axis_mut(&mut self, axis: Axis) -> &mut AxisAlign {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
}
impl RegionAlign {
@@ -231,3 +218,5 @@ impl RegionAlign {
UiVec2::from(self)
}
}
impl_axis_index!(RegionAlign => AxisAlign);
+4 -28
View File
@@ -1,4 +1,5 @@
use super::*;
use crate::util::impl_axis_index;
use crate::{Fixed, FixedVec2};
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
@@ -53,20 +54,6 @@ pub enum Sign {
}
impl<const SHIFT: u32> FixedVec2<SHIFT> {
pub const fn axis(&self, axis: Axis) -> Fixed<SHIFT> {
match axis {
Axis::X => self.x,
Axis::Y => self.y,
}
}
pub const fn axis_mut(&mut self, axis: Axis) -> &mut Fixed<SHIFT> {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
pub const fn from_axis(axis: Axis, aligned: Fixed<SHIFT>, ortho: Fixed<SHIFT>) -> Self {
match axis {
Axis::X => Self::new(aligned, ortho),
@@ -76,20 +63,6 @@ impl<const SHIFT: u32> FixedVec2<SHIFT> {
}
impl Vec2 {
pub fn axis(&self, axis: Axis) -> f32 {
match axis {
Axis::X => self.x,
Axis::Y => self.y,
}
}
pub fn axis_mut(&mut self, axis: Axis) -> &mut f32 {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
pub const fn from_axis(axis: Axis, aligned: f32, ortho: f32) -> Self {
Self {
x: match axis {
@@ -148,3 +121,6 @@ impl<T> BothAxis<T> {
}
}
}
impl_axis_index!({const SHIFT: u32} FixedVec2<SHIFT> => Fixed<SHIFT>);
impl_axis_index!(Vec2 => f32);
+32 -15
View File
@@ -1,4 +1,5 @@
use super::*;
use crate::util::impl_axis_index;
use crate::{Px, PxVec2, Rel, UiNum, Weight, util::impl_op};
#[derive(Debug, Default, Clone, Copy, PartialEq)]
@@ -118,20 +119,6 @@ impl Size {
},
}
}
pub fn axis(&self, axis: Axis) -> LayoutLen {
match axis {
Axis::X => self.x,
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 {
@@ -158,11 +145,39 @@ impl LayoutLen {
Len::from_parts(self.rel.add(share), self.px)
}
/// Only pixels: the same number of them whatever box it lands in, and
/// whatever anyone else in the row asks for. A length that is any part
/// of a box or of what is left over is not one.
pub fn is_px(self) -> bool {
self.rel == Rel::ZERO && self.leftover == Weight::ZERO
}
/// Nothing but a claim on what is left over, so there is no length here
/// at all where nothing is.
pub fn is_only_leftover(self) -> bool {
self.leftover > Weight::ZERO && self.without_leftover() == Len::ZERO
}
/// This as a length of a box, where it is one. `leftover` is not: a
/// share of what is left over is a length only to whoever divides one,
/// so it passes up in the reported size instead and is resolved there.
pub fn declared(self) -> Option<Len> {
(self.leftover == Weight::ZERO).then(|| self.without_leftover())
}
/// What this takes whatever is left over: the reading of a length for
/// anyone not dividing a box between siblings, where a share is a claim
/// on someone else's room rather than a length of its own.
/// [`Self::apply_leftover`] is the opposite reading of the same value.
pub const fn without_leftover(self) -> Len {
Len::from_parts(self.rel, 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);
let part = self.without_leftover().within_len(len);
Self {
px: part.px,
rel: part.rel,
@@ -236,3 +251,5 @@ impl std::fmt::Display for LayoutLen {
Ok(())
}
}
impl_axis_index!(Size => LayoutLen);
+13 -28
View File
@@ -1,3 +1,4 @@
use crate::util::impl_axis_index;
use std::{fmt::Display, marker::Destruct};
use super::*;
@@ -61,20 +62,6 @@ impl UiVec2 {
}
}
pub fn axis_mut(&mut self, axis: Axis) -> &mut Len {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
pub fn axis(&self, axis: Axis) -> Len {
match axis {
Axis::X => self.x,
Axis::Y => self.y,
}
}
/// Resolved against a box of `size`, which is where a fraction stops
/// being one and becomes a place.
pub fn to_px(&self, size: PxVec2) -> PxVec2 {
@@ -294,6 +281,15 @@ impl UiSpan {
}
}
/// A box `len` long inside this one, on the side `align` says. Both must
/// be lengths of the same rel base: it subtracts one from the other
/// rather than composing it in, which is what keeps a fraction the same
/// fraction however long this box turns out to be.
pub const fn place(self, len: Len, align: AxisAlign) -> Self {
let start = self.start + (self.len() - len).scale(align.rel());
Self::new(start, start + len)
}
pub const fn len(&self) -> Len {
self.end - self.start
}
@@ -348,20 +344,6 @@ impl UiRegion {
y: self.y.within(&parent.y),
}
}
pub const fn axis(&self, axis: Axis) -> &UiSpan {
match axis {
Axis::X => &self.x,
Axis::Y => &self.y,
}
}
pub const fn axis_mut(&mut self, axis: Axis) -> &mut UiSpan {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
pub const fn flip(&mut self, axis: Axis) {
match axis {
Axis::X => self.x.flip(),
@@ -462,3 +444,6 @@ impl Display for PixelRegion {
write!(f, "{} -> {}", self.top_left, self.bot_right)
}
}
impl_axis_index!(UiVec2 => Len);
impl_axis_index!(UiRegion => UiSpan);
+2 -2
View File
@@ -1,5 +1,5 @@
use crate::{
LayerId, LayoutHolds, LayoutLen, MaskIdx, MoveIdx, PlaceDesc, RegionAlign, RetainedPrimitive,
Declared, LayerId, LayoutHolds, MaskIdx, MoveIdx, PlaceDesc, RegionAlign, RetainedPrimitive,
Size, TextureHandle, UiRegion, UiVec2, WidgetId,
};
@@ -59,7 +59,7 @@ pub struct ActiveData {
/// The declared lengths whoever drew this widget resolved into its rel base.
/// A change to one moves a box this widget cannot fix by drawing again,
/// and comparing them is what says so.
pub declared: [Option<LayoutLen>; 2],
pub declared: Declared,
/// Its alignment when it was last drawn, which a change to the property
/// is found against.
pub own_align: RegionAlign,
+6
View File
@@ -33,6 +33,12 @@ impl Holds {
len.raw() >= self.lo.raw() && len.raw() <= self.hi.raw()
}
/// Every length `other` holds for is one this holds for, so a drawing
/// made under this range is still good wherever `other` is.
pub const fn covers(self, other: Self) -> bool {
self.lo.raw() <= other.lo.raw() && self.hi.raw() >= other.hi.raw()
}
pub const fn and(self, other: Self) -> Self {
Self {
lo: self.lo.max(other.lo),
+75 -45
View File
@@ -1,10 +1,12 @@
use crate::{Axis, Holds, Len, PxVec2, UiRegion, UiVec2};
use crate::util::impl_axis_index;
use crate::{Axis, Holds, Len, Px, PxVec2, UiRegion, UiVec2};
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
/// What one evaluation of a widget depends on: the window lengths its reads
/// hold for, the pixel lengths of its own box, and the symbolic lengths of
/// that box and of its rel base where either one is what it was expressed in.
/// What one evaluation of a widget depends on along one axis: the window
/// lengths its reads hold for, the pixel lengths of its own box, and the
/// symbolic lengths of that box and of its rel base where either one is what
/// it was expressed in.
///
/// The symbolic lengths are pins rather than ranges: a container places its
/// children as lengths of its rel base measured from where its own box starts,
@@ -19,61 +21,89 @@ const AXES: [Axis; 2] = [Axis::X, Axis::Y];
/// of the rel base that is only pixels is not one: it is that many pixels
/// whatever the rel base turns out to be.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AxisHolds {
pub window: Holds,
pub rel_base: Option<Len>,
pub region: Holds,
pub region_len: Option<Len>,
}
impl AxisHolds {
pub const ANY: Self = Self {
window: Holds::ANY,
rel_base: None,
region: Holds::ANY,
region_len: None,
};
pub fn and(self, other: Self) -> Self {
// Two pins of the same length disagreeing would mean one drawing was
// a fraction of two different lengths at once.
debug_assert!(
self.region_len.is_none()
|| other.region_len.is_none()
|| self.region_len == other.region_len
);
debug_assert!(
self.rel_base.is_none() || other.rel_base.is_none() || self.rel_base == other.rel_base
);
Self {
window: self.window.and(other.window),
rel_base: self.rel_base.or(other.rel_base),
region: self.region.and(other.region),
region_len: self.region_len.or(other.region_len),
}
}
pub fn covers(self, other: Self) -> bool {
self.window.covers(other.window)
&& self.region.covers(other.region)
&& self
.region_len
.is_none_or(|len| other.region_len == Some(len))
&& self.rel_base.is_none_or(|len| other.rel_base == Some(len))
}
/// Whether a widget in a box `len` long, with that rel base, in that
/// window, is one this drawing holds for.
pub fn contains(self, window: Px, rel_base: Len, len: Len) -> bool {
self.window.contains(window)
&& self.rel_base.is_none_or(|pinned| pinned == rel_base)
&& self.region.contains(len.to_px(window))
&& self.region_len.is_none_or(|pinned| pinned == len)
}
}
/// [`AxisHolds`] on both axes. Every question asked of it is asked of one
/// axis at a time, since a widget that read one length holds for any length
/// of the other.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LayoutHolds {
pub window: [Holds; 2],
pub rel_base: [Option<Len>; 2],
pub region: [Holds; 2],
pub region_len: [Option<Len>; 2],
pub x: AxisHolds,
pub y: AxisHolds,
}
impl LayoutHolds {
pub const ANY: Self = Self {
window: [Holds::ANY; 2],
rel_base: [None; 2],
region: [Holds::ANY; 2],
region_len: [None; 2],
x: AxisHolds::ANY,
y: AxisHolds::ANY,
};
pub fn and(self, other: Self) -> Self {
let mut result = Self::ANY;
for n in 0..2 {
result.window[n] = self.window[n].and(other.window[n]);
result.region[n] = self.region[n].and(other.region[n]);
debug_assert!(
self.region_len[n].is_none()
|| other.region_len[n].is_none()
|| self.region_len[n] == other.region_len[n]
);
debug_assert!(
self.rel_base[n].is_none()
|| other.rel_base[n].is_none()
|| self.rel_base[n] == other.rel_base[n]
);
result.region_len[n] = self.region_len[n].or(other.region_len[n]);
result.rel_base[n] = self.rel_base[n].or(other.rel_base[n]);
Self {
x: self.x.and(other.x),
y: self.y.and(other.y),
}
result
}
pub fn covers(self, other: Self) -> bool {
(0..2).all(|n| {
self.window[n].lo <= other.window[n].lo
&& self.window[n].hi >= other.window[n].hi
&& self.region[n].lo <= other.region[n].lo
&& self.region[n].hi >= other.region[n].hi
&& self.region_len[n].is_none_or(|len| other.region_len[n] == Some(len))
&& self.rel_base[n].is_none_or(|len| other.rel_base[n] == Some(len))
})
self.x.covers(other.x) && self.y.covers(other.y)
}
pub fn contains(self, window: PxVec2, rel_base: UiVec2, region: UiRegion) -> bool {
AXES.into_iter().all(|axis| {
let n = axis as usize;
let len = region.axis(axis).len();
self.window[n].contains(window.axis(axis))
&& self.rel_base[n].is_none_or(|pinned| pinned == rel_base.axis(axis))
&& self.region[n].contains(len.to_px(window.axis(axis)))
&& self.region_len[n].is_none_or(|pinned| pinned == len)
})
AXES.into_iter()
.all(|axis| self[axis].contains(window[axis], rel_base[axis], region[axis].len()))
}
}
impl_axis_index!(LayoutHolds => AxisHolds);
+98 -103
View File
@@ -1,9 +1,9 @@
#[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter};
use crate::{
Axis, Holds, LayoutHolds, LayoutLen, Len, PlaceDesc, Px, PxVec2, RegionAlign, Rel,
Axis, Declared, Holds, LayoutHolds, LayoutLen, Len, PlaceDesc, Px, PxVec2, RegionAlign, Rel,
RenderedText, RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextData,
TextureHandle, UiRegion, UiRenderState, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets,
TextureHandle, UiRegion, UiRenderState, UiRsc, UiVec2, Weight, WidgetId, Widgets,
render::{
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind,
TexturePrimitive,
@@ -157,10 +157,10 @@ impl<'a> Painter<'a> {
/// only moves its child does not pin its drawing to a rel base.
fn state_rel_base(&mut self, mut place: PlaceDesc) -> PlaceDesc {
for axis in AXES {
if let Some(span) = place.axis(axis).narrows_rel_base() {
if let Some(span) = place[axis].narrows_rel_base() {
let len = span.len();
let stated = (len != Len::FULL).then(|| len.within_len(self.rel_base(axis)));
*place.axis_mut(axis) = place.axis(axis).with_rel_base(stated);
place[axis] = place[axis].with_rel_base(stated);
}
}
place
@@ -186,7 +186,7 @@ impl<'a> Painter<'a> {
let declared = self.declared_lens(id);
let align = self.rsc.widgets().alignment(id.id());
let (rel_base, region) =
rel_base_and_region(self.region, self.rel_base, place, declared, align);
place.rel_base_and_region(self.region, self.rel_base, declared, align);
#[cfg(feature = "layout-diagnostics")]
if region_node {
diag::bump(Counter::RegionNodeDraws);
@@ -258,7 +258,7 @@ impl<'a> Painter<'a> {
let place = self.state_rel_base(place.into());
let states_rel_base = AXES
.iter()
.any(|&axis| place.axis(axis).stated_rel_base().is_some());
.any(|&axis| place[axis].stated_rel_base().is_some());
if states_rel_base || !self.children.contains(&id.id()) {
return self.widget_at(id, place);
}
@@ -292,8 +292,8 @@ impl<'a> Painter<'a> {
/// it resolves into its rel base. Reading them depends on nothing -- the box
/// that comes of them is kept on the child, and `redraw` compares it
/// there.
fn declared_lens<W: ?Sized>(&self, id: &StrongWidget<W>) -> [Option<LayoutLen>; 2] {
declared_lens(self.rsc.widgets(), id.id())
fn declared_lens<W: ?Sized>(&self, id: &StrongWidget<W>) -> Declared {
self.rsc.widgets().declared_lens(id.id())
}
/// What a child says its length is without being drawn, if it can say,
@@ -304,12 +304,12 @@ impl<'a> Painter<'a> {
let widgets = self.rsc.widgets();
// A rule is the answer where there is one: it wins over whatever the
// widget would draw, so it has to win over what the widget says too.
let hint = widgets.size_rules(id.id()).axis(axis).exact().or_else(|| {
let hint = widgets.size_rules(id.id())[axis].exact().or_else(|| {
widgets
.get_dyn(id.id())
.and_then(|widget| widget.size_hint(axis))
});
let rel_base = self.rel_base.axis(axis);
let rel_base = self.rel_base[axis];
let resolved = hint.map(|hint| hint.within_len(rel_base));
#[cfg(feature = "layout-diagnostics")]
{
@@ -326,7 +326,7 @@ impl<'a> Painter<'a> {
// the child's own: resolved against a rel base of pixels, none is
// left to see it by.
if hint.rel != Rel::ZERO {
self.own.rel_base[axis as usize] = Some(rel_base);
self.own[axis].rel_base = Some(rel_base);
}
}
resolved
@@ -392,8 +392,8 @@ impl<'a> Painter<'a> {
/// again. One axis at a time, because a container that divides one axis
/// holds for any length of the other.
pub fn region_len(&mut self, axis: Axis) -> Len {
let len = self.region.axis(axis).len();
self.own.region_len[axis as usize] = Some(len);
let len = self.region[axis].len();
self.own[axis].region_len = Some(len);
len
}
@@ -403,8 +403,8 @@ impl<'a> Painter<'a> {
/// Reading it pins the drawing to that rel base, the way
/// [`Self::region_len`] pins it to the box.
pub fn rel_base(&mut self, axis: Axis) -> Len {
let len = self.rel_base.axis(axis);
self.own.rel_base[axis as usize] = Some(len);
let len = self.rel_base[axis];
self.own[axis].rel_base = Some(len);
len
}
@@ -425,10 +425,7 @@ impl<'a> Painter<'a> {
/// worth anything, since reading one is also what makes its own size
/// depend on it.
pub fn has_exact_size(&self, axis: Axis) -> bool {
self.rsc
.widgets()
.size_rules(self.id)
.axis(axis)
self.rsc.widgets().size_rules(self.id)[axis]
.exact()
.is_some()
}
@@ -442,9 +439,9 @@ impl<'a> Painter<'a> {
/// One axis of this widget's own box in pixels. Prefer this to
/// [`Self::px_size`] when the other axis cannot affect the drawing.
pub fn px_len(&mut self, axis: Axis) -> Px {
let len = self.region.axis(axis).len();
let px = len.to_px(self.window.axis(axis));
let own = &mut self.own.region[axis as usize];
let len = self.region[axis].len();
let px = len.to_px(self.window[axis]);
let own = &mut self.own[axis].region;
if *own == Holds::ANY {
*own = Holds::at(px);
}
@@ -456,15 +453,15 @@ impl<'a> Painter<'a> {
/// of the box, and the same reported size. A widget that read its length
/// in pixels holds for that one alone until it says otherwise.
pub fn holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
let len = self.region.axis(axis).len();
let len = self.region[axis].len();
let holds = holds.into();
debug_assert!(
holds.contains(len.to_px(self.window.axis(axis))),
holds.contains(len.to_px(self.window[axis])),
"'{}' ({:?}) says its drawing holds for lengths that leave out its own box",
self.label(),
self.id
);
self.own.region[axis as usize] = holds;
self.own[axis].region = holds;
}
/// A window length in pixels, which is what every length in layout is
@@ -472,9 +469,9 @@ impl<'a> Painter<'a> {
/// length is a fraction of it; one that is only pixels is that many
/// pixels in any window and pins nothing.
pub fn to_px(&mut self, len: Len, axis: Axis) -> Px {
let window = self.window.axis(axis);
let window = self.window[axis];
if len.rel != Rel::ZERO {
let own = &mut self.own.window[axis as usize];
let own = &mut self.own[axis].window;
if *own == Holds::ANY {
*own = Holds::at(window);
}
@@ -489,12 +486,12 @@ impl<'a> Painter<'a> {
pub fn window_holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
let holds = holds.into();
debug_assert!(
holds.contains(self.window.axis(axis)),
holds.contains(self.window[axis]),
"'{}' ({:?}) says its drawing holds for windows that leave out this one",
self.label(),
self.id
);
self.own.window[axis as usize] = holds;
self.own[axis].window = holds;
}
pub fn text_data(&mut self) -> &mut TextData {
@@ -553,7 +550,7 @@ impl<W: ?Sized> DrawResult<'_, '_, W> {
}
pub fn len(self, axis: Axis) -> LayoutLen {
self.size().axis(axis)
self.size()[axis]
}
}
@@ -602,20 +599,22 @@ impl Painter<'_> {
holds: LayoutHolds,
region: UiRegion,
place: PlaceDesc,
declared: [Option<LayoutLen>; 2],
declared: Declared,
) -> LayoutHolds {
let mut result = LayoutHolds::ANY;
for axis in AXES {
let n = axis as usize;
let declared = declared[axis];
let holds = holds[axis];
let result = &mut result[axis];
// Every read became pixels against the window, so a range on
// it is already in this widget's terms.
result.window[n] = holds.window[n];
let at = *place.axis(axis);
result.window = holds.window;
let at = place[axis];
let reaches = at.stated_rel_base().is_none()
&& !at.is_sized()
&& declared[n].is_none_or(|len| len.rel != Rel::ZERO);
result.rel_base[n] = holds.rel_base[n].and(reaches.then(|| self.rel_base.axis(axis)));
match (at.within_span(), declared[n].is_some()) {
&& declared.is_none_or(|len| len.rel != Rel::ZERO);
result.rel_base = holds.rel_base.and(reaches.then(|| self.rel_base[axis]));
match (at.within_span(), declared.is_some()) {
// Its box is a part of this widget's own box, in that box's
// own lengths, so what it holds for maps back through that
// part into a range on this widget's box. A length it pinned
@@ -625,10 +624,10 @@ impl Painter<'_> {
// widget's own length.
(Some(span), false) => {
let part_len = span.len();
result.region[n] = holds.region[n].through(part_len);
result.region_len[n] = holds.region_len[n].map(|pinned| match part_len.rel {
result.region = holds.region.through(part_len);
result.region_len = holds.region_len.map(|pinned| match part_len.rel {
Rel::ONE => pinned - Len::from_parts(Rel::ZERO, part_len.px),
_ => self.region.axis(axis).len(),
_ => self.region[axis].len(),
});
}
// Its box is a length this widget decided, from its own
@@ -636,8 +635,7 @@ impl Painter<'_> {
// widget's box reaches it, so what it holds for is a range
// on the window and none of it on that box.
_ => {
result.window[n] =
result.window[n].and(holds.region[n].through(region.axis(axis).len()));
result.window = result.window.and(holds.region.through(region[axis].len()));
}
}
}
@@ -645,35 +643,38 @@ impl Painter<'_> {
}
}
/// 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,
/// so it passes up in the size instead.
pub(crate) fn declared_lens(widgets: &Widgets, id: WidgetId) -> [Option<LayoutLen>; 2] {
let rules = widgets.size_rules(id);
let widget = widgets.get_dyn(id);
AXES.map(|axis| {
rules.axis(axis).declared().or_else(|| {
// A hint still narrows the box where no rule does, which is how a
// widget with a natural pixel size -- an image, a gap -- gets that
// size rather than the whole offer. That is the offer's business
// rather than a declaration's, and this falls away once a widget
// occupies its reported size inside the box it was offered.
impl Widgets {
/// What a widget's box is where a rule or its own hint says so outright.
pub(crate) fn declared_lens(&self, id: WidgetId) -> Declared {
let rules = self.size_rules(id);
let widget = self.get_dyn(id);
Declared::per_axis(|axis| {
rules[axis].declared().or_else(|| {
// A hint still narrows the box where no rule does, which is
// how a widget with a natural pixel size -- an image, a gap
// -- gets that size rather than the whole offer. That is the
// offer's business rather than a declaration's, and this
// falls away once a widget occupies its reported size inside
// the box it was offered.
widget
.and_then(|widget| widget.size_hint(axis))
.filter(|len| len.leftover == Weight::ZERO)
.and_then(LayoutLen::declared)
})
})
}
}
/// 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: the rule already gave
/// the region its length, 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
impl LayoutLen {
/// 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:
/// the rule already gave the region its length, 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(self, declared: Option<Len>, decided: bool) -> bool {
self.leftover != Weight::ZERO || declared.is_some() || decided
}
}
/// Where a widget's drawing goes inside the part its parent gave it: what
@@ -685,63 +686,57 @@ pub(crate) fn fills(reported: LayoutLen, declared: Option<LayoutLen>, decided: b
/// That is what makes a fraction the same fraction wherever the part it is
/// placed in sits and however long it is -- the fraction is resolved once,
/// here, against the rel base it was reported of.
pub(crate) fn placement(
impl PlaceDesc {
pub(crate) fn placement(
self,
region: UiRegion,
size: Size,
declared: [Option<LayoutLen>; 2],
place: PlaceDesc,
declared: Declared,
align: RegionAlign,
) -> UiRegion {
) -> UiRegion {
let mut placed = region;
for axis in AXES {
let n = axis as usize;
let reported = size.axis(axis);
if fills(reported, declared[n], place.axis(axis).does_fill()) {
let reported = size[axis];
if reported.fills(declared[axis], self[axis].does_fill()) {
continue;
}
let len = Len::from_parts(reported.rel, reported.px);
let span = placed.axis_mut(axis);
span.start += (span.len() - len).scale(align.axis(axis).rel());
span.end = span.start + len;
placed[axis] = placed[axis].place(reported.without_leftover(), align[axis]);
}
placed
}
}
/// The rel base length and the box a child is asked in, in the coordinates the
/// widget asking draws in.
///
/// `own` is that widget's own box, and `place` what of it the child is
/// given, including any rel base it states -- a row's slot, or padding's rel
/// base less its pixels. That is a window length, like every other length
/// here, since a slot of a row is not a fraction of anything the row can
/// name. The child's declaration is a fraction of whichever reached it, and
/// is the only one that also places the box: a box the caller decided is
/// what `place` names.
pub(crate) fn rel_base_and_region(
/// The rel base length and the box a child is asked in, in the coordinates the
/// widget asking draws in.
///
/// `own` is that widget's own box, and `place` what of it the child is
/// given, including any rel base it states -- a row's slot, or padding's rel
/// base less its pixels. That is a window length, like every other length
/// here, since a slot of a row is not a fraction of anything the row can
/// name. The child's declaration is a fraction of whichever reached it, and
/// is the only one that also places the box: a box the caller decided is
/// what `place` names.
pub(crate) fn rel_base_and_region(
self,
own: UiRegion,
parent_rel_base: UiVec2,
place: PlaceDesc,
declared: [Option<LayoutLen>; 2],
declared: Declared,
align: RegionAlign,
) -> (UiVec2, UiRegion) {
let given = place.of(own, align);
) -> (UiVec2, UiRegion) {
let given = self.of(own, align);
let mut rel_base = parent_rel_base;
let mut region = given;
for axis in AXES {
let n = axis as usize;
let base = place
.axis(axis)
let base = self[axis]
.stated_rel_base()
.unwrap_or_else(|| parent_rel_base.axis(axis));
let len = declared[n]
.map(|len| Len::from_parts(len.rel, len.px).within_len(base))
.unwrap_or_else(|| parent_rel_base[axis]);
let len = declared[axis]
.map(|len| len.within_len(base))
.unwrap_or(base);
*rel_base.axis_mut(axis) = len;
if declared[n].is_some() {
let slot = given.axis(axis);
let start = slot.start + (slot.len() - len).scale(align.axis(axis).rel());
*region.axis_mut(axis) = UiSpan::new(start, start + len);
rel_base[axis] = len;
if declared[axis].is_some() {
region[axis] = given[axis].place(len, align[axis]);
}
}
(rel_base, region)
}
}
+74 -71
View File
@@ -1,3 +1,4 @@
use crate::util::impl_axis_index;
use crate::{Axis, AxisAlign, Len, PrimitiveHandle, RegionAlign, UiRegion, UiSpan};
/// How a child's region along one axis comes from the region of the widget
@@ -35,53 +36,7 @@ enum RelBase {
impl PlaceDescAxis {
/// The whole of the caller's box.
pub const WHOLE: Self = Self::within(UiSpan::FULL);
/// `span` composed into the caller's own box, so it moves and scales
/// with it: [`UiSpan::within`], which is what a container that insets
/// one speaks. Taking eleven pixels off the end needs no length, where
/// saying the same thing in window lengths would make the container read
/// its own box -- and a box chosen from its own answer then feeds back
/// into the answer.
///
/// The child's rel base is narrowed the same way, so padding takes its
/// pixels off both and `rel(1)` under it fills the caller rather than
/// overflowing it.
pub const fn within(span: UiSpan) -> Self {
Self {
span: PlaceSpan::Within(span),
fills: false,
rel_base: RelBase::WithRegion,
}
}
/// `span` shifted to where the caller's own box starts: window lengths
/// along a cursor, which is what a container dividing room among its
/// children speaks. A child's report is a window length, so the cursor
/// that sums those reports is one too, and a moved box re-places every
/// child by re-adding its start, exactly.
///
/// The child's rel base passes through: how far along the cursor a child
/// sits says nothing about what a fraction under it is of.
pub const fn shifted(span: UiSpan) -> Self {
Self {
span: PlaceSpan::Shifted(span),
fills: false,
rel_base: RelBase::Inherit,
}
}
/// A box this long, placed in the caller's own by the child's alignment:
/// the rule that places an answer, with the length given from above
/// rather than reported. What a stack's sizing child decides for the
/// rest. It is the child's rel base too.
pub const fn sized(len: Len) -> Self {
Self {
span: PlaceSpan::Sized(len),
fills: false,
rel_base: RelBase::Len(len),
}
}
pub const WHOLE: Self = UiSpan::FULL.within_desc();
/// This region is the child's placement: its answer is not placed inside
/// it again. A container uses it where it hands back exactly what the
@@ -91,6 +46,14 @@ impl PlaceDescAxis {
self
}
/// This along `axis`, and the whole of the caller's box across it: what
/// a container dividing one axis says, since nothing divides the other.
/// [`PlaceDesc::from_axis`] says the across one where it is not the
/// whole.
pub const fn axis(self, axis: Axis) -> PlaceDesc {
PlaceDesc::from_axis(axis, self, Self::WHOLE)
}
/// What the child's fractions are of, as a length of the window: a
/// resolved share, or a box a sibling's answer decided.
pub const fn rel_base(mut self, len: Len) -> Self {
@@ -112,10 +75,7 @@ impl PlaceDescAxis {
span.shift(own.start);
span
}
PlaceSpan::Sized(len) => {
let start = own.start + (own.len() - len).scale(align.rel());
UiSpan::new(start, start + len)
}
PlaceSpan::Sized(len) => own.place(len, align),
}
}
@@ -185,29 +145,21 @@ impl PlaceDesc {
Self { x: place, y: place }
}
/// A description per axis, where the two differ and neither is the
/// axis a container divides.
pub fn per_axis(f: impl Fn(Axis) -> PlaceDescAxis) -> Self {
Self::new(f(Axis::X), f(Axis::Y))
}
/// `aligned` on `axis` and `ortho` on the other, which is how a
/// container that divides one axis says what it is doing.
pub fn from_axis(axis: Axis, aligned: PlaceDescAxis, ortho: PlaceDescAxis) -> Self {
pub const fn from_axis(axis: Axis, aligned: PlaceDescAxis, ortho: PlaceDescAxis) -> Self {
match axis {
Axis::X => Self::new(aligned, ortho),
Axis::Y => Self::new(ortho, aligned),
}
}
pub const fn axis(&self, axis: Axis) -> &PlaceDescAxis {
match axis {
Axis::X => &self.x,
Axis::Y => &self.y,
}
}
pub const fn axis_mut(&mut self, axis: Axis) -> &mut PlaceDescAxis {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
/// Both regions are the child's placement. See [`PlaceDescAxis::fills`].
pub const fn fills(self) -> Self {
Self::new(self.x.fills(), self.y.fills())
@@ -215,7 +167,7 @@ impl PlaceDesc {
/// The child's rel base on one axis. See [`PlaceDescAxis::rel_base`].
pub const fn rel_base(mut self, axis: Axis, len: Len) -> Self {
*self.axis_mut(axis) = self.axis(axis).rel_base(len);
self[axis] = self[axis].rel_base(len);
self
}
@@ -225,12 +177,61 @@ impl PlaceDesc {
}
}
impl UiSpan {
/// This span composed into the caller's own box, so it moves and scales
/// with it: [`UiSpan::within`], which is what a container that insets
/// one speaks. Taking eleven pixels off the end needs no length, where
/// saying the same thing in window lengths would make the container read
/// its own box -- and a box chosen from its own answer then feeds back
/// into the answer.
///
/// The child's rel base is narrowed the same way, so padding takes its
/// pixels off both and `rel(1)` under it fills the caller rather than
/// overflowing it.
pub const fn within_desc(self) -> PlaceDescAxis {
PlaceDescAxis {
span: PlaceSpan::Within(self),
fills: false,
rel_base: RelBase::WithRegion,
}
}
/// This span shifted to where the caller's own box starts: window
/// lengths along a cursor, which is what a container dividing room among
/// its children speaks. A child's report is a window length, so the
/// cursor that sums those reports is one too, and a moved box re-places
/// every child by re-adding its start, exactly.
///
/// The child's rel base passes through: how far along the cursor a child
/// sits says nothing about what a fraction under it is of. The same span
/// says [`Self::within_desc`] as a part of that box instead, and which is
/// meant cannot be read off the numbers.
pub const fn shifted_desc(self) -> PlaceDescAxis {
PlaceDescAxis {
span: PlaceSpan::Shifted(self),
fills: false,
rel_base: RelBase::Inherit,
}
}
}
impl Len {
/// A box this long, placed in the caller's own by the child's alignment:
/// the rule that places an answer, with the length given from above
/// rather than reported. What a stack's sizing child decides for the
/// rest. It is the child's rel base too.
pub const fn as_desc(self) -> PlaceDescAxis {
PlaceDescAxis {
span: PlaceSpan::Sized(self),
fills: false,
rel_base: RelBase::Len(self),
}
}
}
impl From<UiRegion> for PlaceDesc {
fn from(region: UiRegion) -> Self {
Self::new(
PlaceDescAxis::within(region.x),
PlaceDescAxis::within(region.y),
)
Self::new(region.x.within_desc(), region.y.within_desc())
}
}
@@ -247,3 +248,5 @@ pub struct RetainedPrimitive {
pub handle: PrimitiveHandle,
pub region: UiRegion,
}
impl_axis_index!(PlaceDesc => PlaceDescAxis);
+57 -69
View File
@@ -1,10 +1,9 @@
#[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind};
use crate::ui::painter::{declared_lens, placement, rel_base_and_region};
use crate::{
ActiveData, Axis, DrawLayers, IdLike, LayoutHolds, LayoutLen, Len, MaskIdx, MoveIdx, Moves,
Painter, PixelRegion, PlaceDesc, PxVec2, Rel, Size, StrongWidget, UiRegion, UiRsc, UiSpan,
UiVec2, Weight, WidgetId, Widgets,
ActiveData, Axis, Declared, DrawLayers, IdLike, LayoutHolds, LayoutLen, Len, MaskIdx, MoveIdx,
Moves, Painter, PixelRegion, PlaceDesc, PxVec2, Rel, Size, StrongWidget, UiRegion, UiRsc,
UiSpan, UiVec2, Weight, WidgetId, Widgets,
util::{HashMap, Vec2},
};
@@ -194,11 +193,10 @@ impl UiRenderState {
/// rules. Nothing above it narrowed anything or chose where it goes, so
/// its declaration is the whole of what decides either.
fn root_layout(id: WidgetId, widgets: &Widgets) -> (UiVec2, UiRegion) {
rel_base_and_region(
PlaceDesc::WHOLE.rel_base_and_region(
UiRegion::FULL,
UiVec2::FULL_SIZE,
PlaceDesc::WHOLE,
declared_lens(widgets, id),
widgets.declared_lens(id),
widgets.alignment(id),
)
}
@@ -221,7 +219,7 @@ impl UiRenderState {
diag::draw_request(id, info.parent, region, info.px, info.region_node);
}
let align = rsc.widgets().alignment(id);
let declared = declared_lens(rsc.widgets(), id);
let declared = rsc.widgets().declared_lens(id);
// Nothing this widget measured can be dirty while it draws: layout is
// one bottom-up walk, so anything deeper has settled or deferred to
// its own parent, and a deferred one leaves that parent marked.
@@ -235,7 +233,7 @@ impl UiRenderState {
.then(|| self.retained_answer(id, region, info))
.flatten()
.and_then(|answer| {
let placed = placement(region, answer.0, declared, info.placed, align);
let placed = info.placed.placement(region, answer.0, declared, align);
self.try_reuse(id, region, placed, info, rsc)
.map(|()| answer)
});
@@ -247,7 +245,7 @@ impl UiRenderState {
// Where the drawing goes: the part its parent gave it, with the
// answer placed inside that part on any axis the parent left
// open.
let placed = placement(region, answer.0, declared, info.placed, align);
let placed = info.placed.placement(region, answer.0, declared, align);
if placed != region {
self.relocate(id, placed, info, rsc);
}
@@ -292,8 +290,8 @@ impl UiRenderState {
// A node entry is only a translation. Its local box keeps the
// same window-unit length as the box in its parent's node.
true => (
self.move_slot(id, info.parent_move, translation(region)),
local_region(region),
self.move_slot(id, info.parent_move, region.as_translation()),
region.at_origin(),
None,
),
// Keep the old entry alive until every descendant has migrated.
@@ -377,14 +375,14 @@ impl UiRenderState {
// resolved into the rel base when the child was asked, and resolving it
// again here would take the fraction of a fraction.
let rules = rsc.widgets().size_rules(id);
let ruled = |axis: Axis, reported: LayoutLen| match rules.axis(axis).exact() {
let ruled = |axis: Axis, reported: LayoutLen| match rules[axis].exact() {
None => reported,
Some(len) if len.leftover == Weight::ZERO => LayoutLen {
rel: info.rel_base.axis(axis).rel,
px: info.rel_base.axis(axis).px,
rel: info.rel_base[axis].rel,
px: info.rel_base[axis].px,
leftover: Weight::ZERO,
},
Some(len) => len.within_len(info.rel_base.axis(axis)),
Some(len) => len.within_len(info.rel_base[axis]),
};
let size = Size {
x: ruled(Axis::X, size.x),
@@ -399,7 +397,7 @@ impl UiRenderState {
mask == info.mask
|| AXES
.into_iter()
.all(|axis| within_box(size, region, self.output_size, axis)),
.all(|axis| size.within_box(region, self.output_size, axis)),
"'{}' ({id:?}) clips to {px:?} and reports {size}",
rsc.widgets().label(id),
);
@@ -418,17 +416,13 @@ impl UiRenderState {
// rel base's own length, so the answer is that rel base's and not just
// that many pixels of this window -- the same pin a widget that read
// its rel base took for its drawing.
let rel_base = AXES.map(|axis| {
let fraction = rules
.axis(axis)
.exact()
.is_some_and(|len| len.rel != Rel::ZERO);
match fraction {
true => Some(info.rel_base.axis(axis)),
false => own.rel_base[axis as usize],
let mut own_holds = own;
for axis in AXES {
let fraction = rules[axis].exact().is_some_and(|len| len.rel != Rel::ZERO);
if fraction {
own_holds[axis].rel_base = Some(info.rel_base[axis]);
}
}
});
let own_holds = LayoutHolds { rel_base, ..own };
let answer_holds = own_holds.and(answer_under);
let holds = under
.into_iter()
@@ -485,7 +479,7 @@ impl UiRenderState {
mask_region,
children,
size_deps,
declared: declared_lens(rsc.widgets(), id),
declared: rsc.widgets().declared_lens(id),
own_align: rsc.widgets().alignment(id),
move_idx,
parent_move: info.parent_move,
@@ -613,21 +607,21 @@ impl UiRenderState {
// Which of the three said no, so a rel base that redraws more
// than it should says where to look. They overlap: a drawing
// can be outside two of them at once.
let holds = active.holds;
for axis in AXES {
let n = axis as usize;
if holds.region_len[n].is_some_and(|pinned| pinned != region.axis(axis).len()) {
let holds = active.holds[axis];
let len = region[axis].len();
let window = self.output_size[axis];
if holds.region_len.is_some_and(|pinned| pinned != len) {
diag::bump(Counter::OutsidePinnedLen);
}
if !holds.window[n].contains(self.output_size.axis(axis))
|| holds.rel_base[n]
.is_some_and(|pinned| pinned != info.rel_base.axis(axis))
if !holds.window.contains(window)
|| holds
.rel_base
.is_some_and(|pinned| pinned != info.rel_base[axis])
{
diag::bump(Counter::OutsideRelBase);
}
if !holds.region[n]
.contains(region.axis(axis).len().to_px(self.output_size.axis(axis)))
{
if !holds.region.contains(len.to_px(window)) {
diag::bump(Counter::OutsideRegion);
}
}
@@ -652,13 +646,13 @@ impl UiRenderState {
);
let has_region_node = active.move_idx != active.parent_move;
let local = match has_region_node {
true => local_region(placed),
true => placed.at_origin(),
false => placed,
};
let moved = active.placement != local;
let slot = active.move_idx;
if has_region_node {
self.moves.set(slot, translation(placed));
self.moves.set(slot, placed.as_translation());
}
if moved {
self.reposition(id, local, info, rsc);
@@ -707,11 +701,10 @@ impl UiRenderState {
) {
let active = &self.active[&child];
let (rel_base, region) = Self::ask_again(active, at, place);
let placed = placement(
let placed = place.placement(
region,
active.measured().unwrap_or(active.size),
active.declared,
place,
active.own_align,
);
let info = DrawInfo {
@@ -736,13 +729,7 @@ impl UiRenderState {
/// it declared are its own record's, so both are resolved against that
/// parent's rel base again exactly as the first ask resolved them.
fn ask_again(active: &ActiveData, at: &Placing, place: PlaceDesc) -> (UiVec2, UiRegion) {
rel_base_and_region(
at.region,
at.rel_base,
place,
active.declared,
active.own_align,
)
place.rel_base_and_region(at.region, at.rel_base, active.declared, active.own_align)
}
/// Re-places everything inside a widget whose own box moved. Every child
@@ -797,11 +784,8 @@ impl UiRenderState {
let Some(widget) = rsc.widgets().get_dyn(id) else {
return true;
};
AXES.into_iter().all(|axis| {
widget
.size_hint(axis)
.is_none_or(|hint| hint == size.axis(axis))
})
AXES.into_iter()
.all(|axis| widget.size_hint(axis).is_none_or(|hint| hint == size[axis]))
}
/// Takes a widget's record out and frees what it drew.
@@ -884,7 +868,7 @@ impl UiRenderState {
children: Vec::new(),
size_deps: Vec::new(),
move_idx: info.parent_move,
declared: [None; 2],
declared: Declared::NONE,
own_align: rsc.widgets().alignment(id),
parent_move: info.parent_move,
mask: info.mask,
@@ -1074,7 +1058,7 @@ impl UiRenderState {
// to draw -- with the mark left on, so the parent draws it rather
// than keeping it. So is a widget the parent asked twice: its
// layout rests on an answer this widget cannot give again alone.
let declared_changed = declared_lens(rsc.widgets(), id) != active.declared;
let declared_changed = rsc.widgets().declared_lens(id) != active.declared;
let alignment_changed = rsc.widgets().alignment(id) != active.own_align;
if let Some(parent) = active.parent
&& (declared_changed
@@ -1189,29 +1173,33 @@ impl UiRenderState {
/// Both are lengths of the window, so the comparison is in its pixels. A
/// share is a length only to whoever divides one, so it is not a claim about
/// this box and cannot exceed it.
fn within_box(size: Size, region: UiRegion, window: PxVec2, axis: Axis) -> bool {
let len = size.axis(axis);
let window = window.axis(axis);
impl Size {
fn within_box(self, region: UiRegion, window: PxVec2, axis: Axis) -> bool {
let len = self[axis];
let window = window[axis];
len.leftover != Weight::ZERO
|| Len::from_parts(len.rel, len.px).to_px(window) <= region.axis(axis).len().to_px(window)
|| len.without_leftover().to_px(window) <= region[axis].len().to_px(window)
}
}
/// A box in a fresh region node keeps its window-unit length and starts at
/// that node's origin.
fn local_region(region: UiRegion) -> UiRegion {
let size = region.size();
impl UiRegion {
/// A box in a fresh region node keeps its window-unit length and starts
/// at that node's origin.
fn at_origin(self) -> UiRegion {
let size = self.size();
UiRegion::new(
UiSpan::new(Len::ZERO, size.x),
UiSpan::new(Len::ZERO, size.y),
)
}
}
/// A region node changes only the origin. A full relative span anchored at
/// the box start composes as that translation in both the CPU and shader.
fn translation(region: UiRegion) -> UiRegion {
/// A region node changes only the origin. A full relative span anchored at
/// the box start composes as that translation in both the CPU and shader.
fn as_translation(self) -> UiRegion {
UiRegion {
x: UiSpan::new(region.x.start, region.x.start + Len::FULL),
y: UiSpan::new(region.y.start, region.y.start + Len::FULL),
x: UiSpan::new(self.x.start, self.x.start + Len::FULL),
y: UiSpan::new(self.y.start, self.y.start + Len::FULL),
}
}
}
+28
View File
@@ -93,3 +93,31 @@ macro_rules! impl_op {
}
pub(crate) use impl_op;
/// `Index<Axis>` for a pair, which is how every pair here is read by axis.
/// The generics clause is given in braces where the type has one.
macro_rules! impl_axis_index {
($({$($gen:tt)*})? $T:ty => $Out:ty) => {
const impl $(<$($gen)*>)? std::ops::Index<crate::Axis> for $T {
type Output = $Out;
fn index(&self, axis: crate::Axis) -> &$Out {
match axis {
crate::Axis::X => &self.x,
crate::Axis::Y => &self.y,
}
}
}
const impl $(<$($gen)*>)? std::ops::IndexMut<crate::Axis> for $T {
fn index_mut(&mut self, axis: crate::Axis) -> &mut $Out {
match axis {
crate::Axis::X => &mut self.x,
crate::Axis::Y => &mut self.y,
}
}
}
};
}
pub(crate) use impl_axis_index;
+27 -20
View File
@@ -1,4 +1,5 @@
use crate::{Axis, LayoutLen, Weight};
use crate::util::impl_axis_index;
use crate::{Axis, LayoutLen, Len};
/// What a widget's length on one axis is, as a rule its parent applies where
/// it draws it rather than an answer the widget gives about itself.
@@ -19,14 +20,9 @@ pub enum SizeRule {
impl SizeRule {
/// The length this rule gives without the widget being drawn, if it can
/// give one. `leftover` is never among them: a share is a length only to
/// whoever divides one, so it passes up in the reported size instead and
/// is resolved there.
pub fn declared(&self) -> Option<LayoutLen> {
match self {
Self::Exact(len) if len.leftover == Weight::ZERO => Some(*len),
_ => None,
}
/// give one.
pub fn declared(&self) -> Option<Len> {
self.exact().and_then(LayoutLen::declared)
}
/// The length this rule gives outright, whatever the widget reports --
@@ -70,18 +66,29 @@ pub struct SizeRules {
pub y: SizeRule,
}
impl SizeRules {
pub fn axis(&self, axis: Axis) -> SizeRule {
match axis {
Axis::X => self.x,
Axis::Y => self.y,
}
}
impl_axis_index!(SizeRules => SizeRule);
pub fn axis_mut(&mut self, axis: Axis) -> &mut SizeRule {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
/// What a widget's box is on each axis where something says so outright,
/// before it is drawn: a rule beside it, or a hint it gives about itself.
/// Whoever draws the widget resolves these against its rel base.
///
/// A [`Len`] rather than a [`LayoutLen`], because a share can never be one
/// -- see [`LayoutLen::declared`].
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Declared {
pub x: Option<Len>,
pub y: Option<Len>,
}
impl Declared {
pub const NONE: Self = Self { x: None, y: None };
pub fn per_axis(f: impl Fn(Axis) -> Option<Len>) -> Self {
Self {
x: f(Axis::X),
y: f(Axis::Y),
}
}
}
impl_axis_index!(Declared => Option<Len>);
+4 -4
View File
@@ -130,10 +130,10 @@ impl Widgets {
pub fn set_size_rule(&mut self, id: impl IdLike, axis: Axis, rule: SizeRule) {
let id = id.id();
let data = self.data_mut(id).unwrap();
if *data.size.axis_mut(axis) == rule {
if data.size[axis] == rule {
return;
}
*data.size.axis_mut(axis) = rule;
data.size[axis] = rule;
self.needs_redraw.insert(id);
}
@@ -147,10 +147,10 @@ impl Widgets {
pub fn set_alignment(&mut self, id: impl IdLike, axis: Axis, align: AxisAlign) {
let id = id.id();
let data = self.data_mut(id).unwrap();
if *data.align.axis_mut(axis) == align {
if data.align[axis] == align {
return;
}
*data.align.axis_mut(axis) = align;
data.align[axis] = align;
self.needs_redraw.insert(id);
}
+4 -4
View File
@@ -118,9 +118,9 @@ pub struct Branch {
impl Widget for Branch {
fn draw(&mut self, painter: &mut Painter) -> Size {
let cut = Len::from_parts(Rel::ZERO, Px::from_int(40));
let top = PlaceDescAxis::shifted(UiSpan::new(Len::ZERO, cut));
let top = UiSpan::new(Len::ZERO, cut).shifted_desc();
let measured = painter
.widget_at(&self.probe, PlaceDesc::new(PlaceDescAxis::WHOLE, top))
.widget_at(&self.probe, top.axis(Axis::Y))
.len(Axis::X);
let len = measured.apply_leftover();
let px = painter.to_px(len, Axis::X);
@@ -134,8 +134,8 @@ impl Widget for Branch {
};
painter.window_holds(Axis::X, holds.through(len));
let below = PlaceDescAxis::shifted(UiSpan::new(cut, painter.region_len(Axis::Y)));
let place = PlaceDesc::new(PlaceDescAxis::WHOLE, below);
let below = UiSpan::new(cut, painter.region_len(Axis::Y)).shifted_desc();
let place = below.axis(Axis::Y);
match px > threshold {
true => painter.widget_at(&self.wide, place),
false => painter.widget_at(&self.narrow, place),
+1 -1
View File
@@ -12,7 +12,7 @@ impl Widget for Image {
}
fn size_hint(&self, axis: Axis) -> Option<LayoutLen> {
Some(LayoutLen::px(self.handle.size().axis(axis)))
Some(LayoutLen::px(self.handle.size()[axis]))
}
}
+5 -8
View File
@@ -16,7 +16,7 @@ impl Widget for Scroll {
let answer_len = painter
.widget_at(&self.inner, PlaceDesc::WHOLE.fills())
.len(self.axis);
let fixed = painter.to_px(Len::from_parts(answer_len.rel, answer_len.px), self.axis);
let fixed = painter.to_px(answer_len.without_leftover(), self.axis);
self.container_len = container_len;
self.content_len = fixed.max(container_len);
@@ -24,14 +24,14 @@ impl Widget for Scroll {
self.amt = self.content_len - self.container_len;
}
self.update_amt();
let align = painter.alignment().axis(self.axis);
let align = painter.alignment()[self.axis];
// Content of a fixed length that fits sits at the start of any box it
// fits in -- but only anchored there. Anywhere else it is a part of
// the room left over, so it moves with every length the box takes and
// the drawing holds for that length alone. One scrolled part way sits
// where it is until the box shrinks past what is left of it. Kept to
// the end, it moves with every length.
let fixed_len = answer_len.rel == Rel::ZERO && answer_len.leftover == Weight::ZERO;
let fixed_len = answer_len.is_px();
if fixed_len && self.content_len <= self.container_len && align == AxisAlign::NEG {
painter.holds(self.axis, fixed..=Px::MAX);
} else if fixed_len && !self.snap_end {
@@ -54,7 +54,7 @@ impl Widget for Scroll {
let content = match moved || self.content_len != self.container_len {
true => {
let start = Len::from_parts(Rel::ZERO, anchor - self.amt);
PlaceDescAxis::shifted(UiSpan::new(start, start.offset(self.content_len)))
UiSpan::new(start, start.offset(self.content_len)).shifted_desc()
}
false => PlaceDescAxis::WHOLE,
};
@@ -62,10 +62,7 @@ impl Widget for Scroll {
// reports is a fraction of what is on screen rather than of the
// content box its own answer decided. Where it goes is the content
// box, scrolled: its drawing moved there, not made again there.
painter.place_at(
&self.inner,
PlaceDesc::from_axis(self.axis, content.fills(), PlaceDescAxis::WHOLE.fills()),
);
painter.place_at(&self.inner, content.axis(self.axis).fills());
// 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
// more. The content's length is what it scrolls through, not what it
+17 -29
View File
@@ -18,10 +18,6 @@ impl Widget for Span {
Sign::Pos => UiSpan::new(from, to),
Sign::Neg => UiSpan::new(far - to, far - from),
};
// Across itself the child sits where its own alignment says, in the
// whole of the row: a span is what contains its children there, and
// nothing divides that axis.
let across = PlaceDescAxis::WHOLE;
// A length for every child before their final slots are chosen: from
// a hint where one says, and from drawing otherwise. The rel base passes
// through unchanged, so `rel(0.5)` is half the area this span was
@@ -34,10 +30,11 @@ impl Widget for Span {
let len = match painter.size_hint(child, axis) {
Some(len) => len,
None => {
let room = PlaceDescAxis::shifted(along(cursor, far));
painter
.widget_at(child, PlaceDesc::from_axis(axis, room, across))
.len(axis)
// Across itself the child sits where its own alignment
// says, in the whole of the row: a span is what contains
// its children there, and nothing divides that axis.
let room = along(cursor, far).shifted_desc().axis(axis);
painter.widget_at(child, room).len(axis)
}
};
cursor.px += len.px + self.gap;
@@ -58,7 +55,7 @@ impl Widget for Span {
// What is left for the shares to divide: the row less everything
// fixed, as a length of the rel base rather than a number of pixels.
let room = far - Len::from_parts(total.rel, total.px);
let room = far - total.without_leftover();
// Whether anything is left over is a question in pixels: `rel(0.5)`
// beside 300 px is full at 600 and overfull at 400. Asked of `room`
// itself, and answered back through the same expression, so the
@@ -94,24 +91,26 @@ impl Widget for Span {
let mut taken = Weight::ZERO;
let mut start = Len::rel_min();
let mut ortho = LayoutLen::ZERO;
let shared = |fixed: Len, taken: Weight| match taken == Weight::ZERO {
true => fixed,
false => fixed + room.scale(Rel::ratio(taken, total.leftover)),
};
for (child, &len) in self.children.iter().zip(&lens) {
// A child asking for nothing but a part of what is left over,
// when nothing is, is not drawn at all. One that also asked for
// pixels or a fraction keeps those and overflows.
if len.leftover > Weight::ZERO && len.px == Px::ZERO && len.rel == Rel::ZERO && !shares
{
if len.is_only_leftover() && !shares {
painter.undraw(child);
fixed.px += self.gap;
start = shared(fixed, taken, total.leftover, room);
start = shared(fixed, taken);
continue;
}
let from = start;
if len.leftover > Weight::ZERO && shares {
taken += len.leftover;
}
fixed.px += len.px;
fixed.rel += len.rel;
start = shared(fixed, taken, total.leftover, room);
fixed += len.without_leftover();
start = shared(fixed, taken);
// Along the row the span says where the child goes, and that slot
// is the child's box outright rather than something to place an
// answer inside again. A share is decided here and nowhere
@@ -120,8 +119,7 @@ impl Widget for Span {
// fixed child's slot is its own answer, so a drawing made in the
// room is put there as it is, and one not made yet is made here.
let slot = along(from, start);
let slot_place = PlaceDescAxis::shifted(slot).fills();
let mut place = PlaceDesc::from_axis(axis, slot_place, across);
let mut place = slot.shifted_desc().fills().axis(axis);
if len.leftover > Weight::ZERO && shares {
place = place.rel_base(axis, slot.len());
}
@@ -131,14 +129,14 @@ impl Widget for Span {
// span's own eventual width admits multiple fixed points.
// A scalable child therefore makes Children scalable too;
// only fixed children are compared with one another.
if used.rel != Rel::ZERO || used.leftover != Weight::ZERO {
if !used.is_px() {
ortho = LayoutLen::LEFTOVER;
} else if ortho.leftover == Weight::ZERO {
ortho.px = ortho.px.max(used.px);
}
}
fixed.px += self.gap;
start = shared(fixed, taken, total.leftover, room);
start = shared(fixed, taken);
}
// Carried whole rather than collapsed to one share: a span that sizes
@@ -157,16 +155,6 @@ impl Widget for Span {
}
}
/// Where a row has reached: everything fixed before this point, which is a
/// sum and exact, plus the share of the room the weights so far are worth,
/// which is one rounding wherever it is asked for.
fn shared(fixed: Len, taken: Weight, weight: Weight, room: Len) -> Len {
if taken == Weight::ZERO {
return fixed;
}
fixed + room.scale(Rel::ratio(taken, weight))
}
impl Span {
pub fn empty(dir: Dir) -> Self {
Self {
+4 -5
View File
@@ -31,14 +31,13 @@ impl Widget for Stack {
// fraction under them is a fraction of it. A share leaves the axis
// to whoever gave the stack its box. Where a child sits in a box
// bigger than itself is its own business.
let on = |axis| {
let len = size.axis(axis);
let place = PlaceDesc::per_axis(|axis| {
let len = size[axis];
match len.leftover == Weight::ZERO {
true => PlaceDescAxis::sized(Len::from_parts(len.rel, len.px)).fills(),
true => len.without_leftover().as_desc().fills(),
false => PlaceDescAxis::WHOLE,
}
};
let place = PlaceDesc::new(on(Axis::X), on(Axis::Y));
});
for (i, child) in self.children.iter().enumerate() {
if sizing == Some(i) {
continue;
+4 -4
View File
@@ -22,14 +22,14 @@ struct BranchesOnMeasurement {
impl Widget for BranchesOnMeasurement {
fn draw(&mut self, painter: &mut Painter) -> Size {
let cut = Len::from_parts(Rel::ZERO, Px::from_int(40));
let top = PlaceDescAxis::shifted(UiSpan::new(Len::ZERO, cut));
let top = UiSpan::new(Len::ZERO, cut).shifted_desc();
let measured = painter
.widget_at(&self.probe, PlaceDesc::new(PlaceDescAxis::WHOLE, top))
.widget_at(&self.probe, top.axis(Axis::Y))
.len(Axis::X);
let px = painter.to_px(measured.apply_leftover(), Axis::X);
let below = PlaceDescAxis::shifted(UiSpan::new(cut, painter.region_len(Axis::Y)));
let place = PlaceDesc::new(PlaceDescAxis::WHOLE, below);
let below = UiSpan::new(cut, painter.region_len(Axis::Y)).shifted_desc();
let place = below.axis(Axis::Y);
match px > Px::from_f32(self.threshold) {
true => painter.widget_at(&self.wide, place),
false => painter.widget_at(&self.narrow, place),
+4 -4
View File
@@ -501,10 +501,10 @@ fn a_row_of_equal_shares_fills_it_exactly() {
fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) {
let active = &h.render.active[&id];
let region = h.render.moves.resolve(active.move_idx, active.placement);
let dim = h.size().axis(axis);
let dim = h.size()[axis];
let snap = |v: f32| (v + Px::STEP.to_f32() * 0.5).floor();
let edge = |s: Len| snap(s.rel.to_f32() * dim + s.px.to_f32());
let span = region.axis(axis);
let span = region[axis];
(edge(span.start), edge(span.end))
}
@@ -853,8 +853,8 @@ fn a_collapsed_share_keeps_the_gaps_before_the_next_slot() {
Sign::Pos => (400 - tail_len, 400),
Sign::Neg => (0, tail_len),
};
assert_eq!(region.top_left.axis(dir.axis), Px::from_int(from));
assert_eq!(region.bot_right.axis(dir.axis), Px::from_int(to));
assert_eq!(region.top_left[dir.axis], Px::from_int(from));
assert_eq!(region.bot_right[dir.axis], Px::from_int(to));
}
}
}
+12 -23
View File
@@ -214,10 +214,7 @@ impl Widget for FromHint {
fn draw(&mut self, painter: &mut Painter) -> Size {
let len = painter.size_hint(&self.inner, Axis::Y).unwrap();
let top = UiSpan::new(Len::ZERO, Len::from_parts(Rel::ZERO, len.px));
painter.widget_at(
&self.inner,
PlaceDesc::new(PlaceDescAxis::WHOLE, PlaceDescAxis::shifted(top)),
);
painter.widget_at(&self.inner, top.shifted_desc().axis(Axis::Y));
Size::LEFTOVER
}
}
@@ -876,10 +873,7 @@ fn resizing_a_fixed_frame_recomposes_its_contents_without_drawing_them() {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget_at(
&self.child,
PlaceDesc::new(
PlaceDescAxis::shifted(self.region.x),
PlaceDescAxis::shifted(self.region.y),
),
PlaceDesc::new(self.region.x.shifted_desc(), self.region.y.shifted_desc()),
);
Size::LEFTOVER
}
@@ -965,8 +959,8 @@ fn glyph_origins_compose_identically_when_drawn_and_when_retained() {
painter.widget_at(
&self.child,
PlaceDesc::new(
PlaceDescAxis::shifted(self.region.x).fills(),
PlaceDescAxis::shifted(self.region.y).fills(),
self.region.x.shifted_desc().fills(),
self.region.y.shifted_desc().fills(),
)
.rel_base(Axis::X, self.frame.x.len()),
);
@@ -1135,8 +1129,8 @@ fn padding_and_stack_boxes_follow_the_region_without_drawing_again() {
painter.widget_at(
&self.child,
PlaceDesc::new(
PlaceDescAxis::shifted(self.region.x).fills(),
PlaceDescAxis::shifted(self.region.y).fills(),
self.region.x.shifted_desc().fills(),
self.region.y.shifted_desc().fills(),
),
);
Size::LEFTOVER
@@ -1222,12 +1216,10 @@ fn moving_a_childs_region_preserves_the_slot_chosen_from_its_measurement() {
painter.widget_at(
&self.child,
PlaceDesc::new(
PlaceDescAxis::shifted(UiSpan::new(
Len::px(self.start),
Len::px(self.start + 200.0),
))
UiSpan::new(Len::px(self.start), Len::px(self.start + 200.0))
.shifted_desc()
.fills(),
PlaceDescAxis::shifted(UiSpan::FULL).fills(),
UiSpan::FULL.shifted_desc().fills(),
),
);
Size::LEFTOVER
@@ -1263,10 +1255,7 @@ fn changing_regions_keep_fractional_reports_and_numeric_dependencies_valid() {
painter
.widget_at(
&self.child,
PlaceDesc::new(
PlaceDescAxis::shifted(self.region.x),
PlaceDescAxis::shifted(self.region.y),
),
PlaceDesc::new(self.region.x.shifted_desc(), self.region.y.shifted_desc()),
)
.size()
}
@@ -1283,8 +1272,8 @@ fn changing_regions_keep_fractional_reports_and_numeric_dependencies_valid() {
.widget_at(
&self.child,
PlaceDesc::new(
PlaceDescAxis::shifted(self.region.x).fills(),
PlaceDescAxis::shifted(self.region.y).fills(),
self.region.x.shifted_desc().fills(),
self.region.y.shifted_desc().fills(),
),
)
.size(),