Files
iris/core/src/orientation/pos.rs
T
iris-aiandClaude Fable 5.1 5b7800264d 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>
2026-09-16 23:00:15 -04:00

465 lines
12 KiB
Rust

use std::{fmt::Display, marker::Destruct};
use super::*;
use crate::{Px, PxVec2, Rel, UiNum, util::impl_op};
#[repr(C)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, bytemuck::Zeroable, Default)]
pub struct UiVec2 {
pub x: Len,
pub y: Len,
}
impl UiVec2 {
pub const ZERO: Self = Self {
x: Len::ZERO,
y: Len::ZERO,
};
pub const fn new(x: Len, y: Len) -> Self {
Self { x, y }
}
pub const fn px(px: impl const Into<Vec2>) -> Self {
let px = px.into();
Self {
x: Len::px(px.x),
y: Len::px(px.y),
}
}
/// From lengths already on the grid, with no fraction of a box.
pub const fn from_px(px: PxVec2) -> Self {
Self {
x: Len::from_parts(Rel::ZERO, px.x),
y: Len::from_parts(Rel::ZERO, px.y),
}
}
pub const fn rel(rel: impl const Into<Vec2>) -> Self {
let rel = rel.into();
Self {
x: Len::rel(rel.x),
y: Len::rel(rel.y),
}
}
pub const fn shift(&mut self, offset: impl const Into<UiVec2>) {
let offset = offset.into();
*self += offset;
}
pub const fn offset(mut self, offset: impl const Into<UiVec2>) -> Self {
self.shift(offset);
self
}
pub const fn within(&self, region: &UiRegion) -> UiVec2 {
UiVec2 {
x: self.x.within(&region.x),
y: self.y.within(&region.y),
}
}
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 {
PxVec2::new(self.x.to_px(size.x), self.y.to_px(size.y))
}
pub const FULL_SIZE: Self = Self::rel(Vec2::ONE);
pub const fn from_axis(axis: Axis, aligned: Len, ortho: Len) -> Self {
match axis {
Axis::X => Self {
x: aligned,
y: ortho,
},
Axis::Y => Self {
x: ortho,
y: aligned,
},
}
}
pub fn get_px(&self) -> Vec2 {
(self.x.px.to_f32(), self.y.px.to_f32()).into()
}
pub fn get_rel(&self) -> Vec2 {
(self.x.rel.to_f32(), self.y.rel.to_f32()).into()
}
}
impl Display for UiVec2 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "rel{};px{}", self.get_rel(), self.get_px())
}
}
impl_op!(same UiVec2 Add add; x y);
impl_op!(same UiVec2 Sub sub; x y);
const impl From<Vec2> for UiVec2 {
fn from(px: Vec2) -> Self {
Self::px(px)
}
}
const impl<T: const UiNum, U: const UiNum> From<(T, U)> for UiVec2
where
(T, U): const Destruct,
{
fn from(px: (T, U)) -> Self {
Self::px(px)
}
}
/// A length along one axis: a fraction of the box it is measured in plus an
/// offset, `rel * box + px`. A position is the same number -- the length from
/// the start of the box to the point -- which is why a [`UiSpan`] is two of
/// these. Both parts are fixed point, so composing one through a chain of
/// boxes rounds only where it multiplies, and lands on the same number as any
/// other route to the same place.
///
/// It carries no claim on what a container has left over. That is
/// [`crate::LayoutLen`], which is this plus a weight, and which means nothing
/// to anyone but whoever divides the room.
#[repr(C)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, Default, bytemuck::Zeroable)]
pub struct Len {
pub rel: Rel,
pub px: Px,
}
impl_op!(same Len Add add; rel px);
impl_op!(same Len Sub sub; rel px);
impl Len {
pub const ZERO: Self = Self {
rel: Rel::ZERO,
px: Px::ZERO,
};
pub const FULL: Self = Self {
rel: Rel::ONE,
px: Px::ZERO,
};
pub const fn new(rel: f32, px: f32) -> Self {
Self::from_parts(Rel::from_f32(rel), Px::from_f32(px))
}
/// From parts already on the grid, rather than numbers to be put on it.
pub const fn from_parts(rel: Rel, px: Px) -> Self {
Self { rel, px }
}
pub const fn rel(rel: f32) -> Self {
Self::from_parts(Rel::from_f32(rel), Px::ZERO)
}
pub const fn px(px: f32) -> Self {
Self::from_parts(Rel::ZERO, Px::from_f32(px))
}
pub const fn rel_min() -> Self {
Self::ZERO
}
pub const fn rel_max() -> Self {
Self::FULL
}
pub const fn max(&self, other: Self) -> Self {
Self {
rel: self.rel.max(other.rel),
px: self.px.max(other.px),
}
}
pub const fn min(&self, other: Self) -> Self {
Self {
rel: self.rel.min(other.rel),
px: self.px.min(other.px),
}
}
/// Both parts by the same fraction, which is what a part of a length
/// means when the length is part pixels and part a fraction of a box.
pub const fn scale(&self, by: Rel) -> Self {
Self {
rel: self.rel.mul(by),
px: self.px.mul(by),
}
}
pub const fn offset(mut self, amt: Px) -> Self {
self.px = self.px.add(amt);
self
}
pub const fn within(&self, span: &UiSpan) -> Self {
Self {
rel: self.rel.lerp(span.start.rel, span.end.rel),
px: self.px.add(self.rel.lerp(span.start.px, span.end.px)),
}
}
pub const fn within_len(&self, len: Len) -> Self {
self.within(&UiSpan {
start: Len::ZERO,
end: len,
})
}
pub fn select_len(&self, len: Len) -> Self {
len.within_len(*self)
}
pub const fn flip(&mut self) {
self.rel = Rel::ONE.sub(self.rel);
self.px = self.px.neg();
}
pub const fn to(&self, end: Self) -> UiSpan {
UiSpan { start: *self, end }
}
/// Resolved against a box of `len`, which is the only place a fraction
/// becomes a number of pixels.
pub const fn to_px(&self, len: Px) -> Px {
self.px.add(len.mul(self.rel))
}
}
#[repr(C)]
#[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
pub struct UiSpan {
pub start: Len,
pub end: Len,
}
impl UiSpan {
pub const FULL: Self = Self {
start: Len::ZERO,
end: Len::FULL,
};
pub const fn rel(rel: f32) -> Self {
Self {
start: Len::rel(rel),
end: Len::rel(rel),
}
}
pub const fn new(start: Len, end: Len) -> Self {
Self { start, end }
}
pub const fn flip(&mut self) {
self.start.flip();
self.end.flip();
std::mem::swap(&mut self.start.rel, &mut self.end.rel);
std::mem::swap(&mut self.start.px, &mut self.end.px);
}
pub const fn shift(&mut self, offset: Len) {
self.start += offset;
self.end += offset;
}
/// Composing a box through the one it sits in, and the hottest line in
/// layout. It used to skip the multiplies where a span was the whole of
/// its parent or the parent the whole of its own; both come out of the
/// multiply unchanged anyway, and the body those comparisons cost was
/// what kept the inliner from taking this at all.
pub const fn within(&self, parent: &Self) -> Self {
Self {
start: self.start.within(parent),
end: self.end.within(parent),
}
}
pub const fn len(&self) -> Len {
self.end - self.start
}
/// Both ends by the same amount, which is what moving a box without
/// changing its length does to every part of it.
pub const fn translated(self, by: Len) -> Self {
Self {
start: self.start + by,
end: self.end + by,
}
}
}
#[repr(C)]
#[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
pub struct UiRegion {
pub x: UiSpan,
pub y: UiSpan,
}
impl UiRegion {
/// Every part of the box by the same amount on each axis. Done to the
/// whole region rather than an end at a time, because that is what it is
/// -- and because four adds in a row are four adds, where four asked for
/// separately are four sequences.
pub const fn translated(self, x: Len, y: Len) -> Self {
Self {
x: self.x.translated(x),
y: self.y.translated(y),
}
}
pub const FULL: Self = Self {
x: UiSpan::FULL,
y: UiSpan::FULL,
};
pub const fn new(x: UiSpan, y: UiSpan) -> Self {
Self { x, y }
}
pub const fn rel(rel: Vec2) -> Self {
Self {
x: UiSpan::rel(rel.x),
y: UiSpan::rel(rel.y),
}
}
pub const fn within(&self, parent: &Self) -> Self {
Self {
x: self.x.within(&parent.x),
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(),
Axis::Y => self.y.flip(),
}
}
pub fn shift(&mut self, offset: impl Into<UiVec2>) {
let offset = offset.into();
self.x.shift(offset.x);
self.y.shift(offset.y);
}
pub fn offset(mut self, offset: impl Into<UiVec2>) -> Self {
self.shift(offset);
self
}
pub fn to_px(&self, size: PxVec2) -> PixelRegion {
PixelRegion {
top_left: self.top_left().to_px(size),
bot_right: self.bot_right().to_px(size),
}
}
pub const fn center(&self) -> UiVec2 {
Align::CENTER.pos().within(self)
}
pub const fn size(&self) -> UiVec2 {
UiVec2 {
x: self.x.len(),
y: self.y.len(),
}
}
pub const fn top_left(&self) -> UiVec2 {
UiVec2 {
x: self.x.start,
y: self.y.start,
}
}
pub const fn bot_right(&self) -> UiVec2 {
UiVec2 {
x: self.x.end,
y: self.y.end,
}
}
pub const fn from_axis(axis: Axis, aligned: UiSpan, ortho: UiSpan) -> Self {
Self {
x: match axis {
Axis::X => aligned,
Axis::Y => ortho,
},
y: match axis {
Axis::X => ortho,
Axis::Y => aligned,
},
}
}
}
impl Display for UiRegion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} -> {} (size: {})",
self.top_left(),
self.bot_right(),
self.size()
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PixelRegion {
pub top_left: PxVec2,
pub bot_right: PxVec2,
}
impl PixelRegion {
pub fn contains(&self, pos: PxVec2) -> bool {
pos.x >= self.top_left.x
&& pos.x <= self.bot_right.x
&& pos.y >= self.top_left.y
&& pos.y <= self.bot_right.y
}
pub fn size(&self) -> PxVec2 {
self.bot_right - self.top_left
}
}
impl Display for PixelRegion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} -> {}", self.top_left, self.bot_right)
}
}