`Px` and `PxVec2` reach the last places a pixel was a float: the window, the box a widget reads, the box it is compared against, and `PixelRegion`. A pointer, a wheel notch and a shaped glyph advance still arrive as floats, and each is put on the grid where it arrives. `Holds` is an interval of `Px`. `HOLDS_EPSILON_PX` is gone with the `exact`/tolerant split it existed for: `at` is the length a widget read, an open end is the next step along, and `same_px` is equality. `Span`'s margin from `5ed9e87` goes too -- the box a parent hands back and the sum of what its children asked for are counts of the same step, so the boundary decides the same way from either side. Three things had to be true for that, and were not: `Holds::through` inverts `px + rel * box`, which rounds -- so a part of a given length came from a range of boxes, and inverting the length alone gave a point that need not contain the box the part was drawn in. It now maps the half step either side, and one more for a length composed down the chain against the same length measured against the window. `RegionRemap` translates when a box only moved, rather than dividing to find each part's fraction and multiplying to place it again. Two roundings landed a step from where growing the tree that way does; a move is exact on a grid, which is the whole reason `tests/drift.rs` was written. A pixel is `1/1024` rather than `1/64`. At `1/64` the residue of a length reached two ways was one step, and one step was 0.016 px -- enough to move a box. `PX_SHIFT` and `REL_SHIFT` are the only statement of the grid now, and the shader's copy is prepended from them rather than written twice. Checked: fmt, clippy, 102 tests, 100 generated seeds in 75 s, all five shrinker cases at 300 seeds, and `tabs`, `view`, `minimal`, `text` and `random` byte-identical at 1920x1200. What the fuzzers ask for is now a step, not a twentieth of a pixel: the shrinker's five cases agree within one (`resize` exactly), and the oracle's two-operation cases within two. The residue is a single rounding either way -- it scales with the grid rather than accumulating, which is why it is a thousandth of a pixel now. Closing it means one way of asking how long a box is, rather than a chain composed down and a length measured against the window; that is a bigger change than this one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
513 lines
16 KiB
Rust
513 lines
16 KiB
Rust
use crate::{UiNum, util::Vec2};
|
|
use std::{
|
|
fmt::{Debug, Display, Formatter},
|
|
ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign},
|
|
};
|
|
|
|
/// A number held as a whole count of `1 / 2^SHIFT`.
|
|
///
|
|
/// Layout reaches one place by more than one route -- a box composed down the
|
|
/// chain, and the same box summed from what its children asked for -- and has
|
|
/// to decide whether the two are the same place. In floats they land a few
|
|
/// bits apart, which is a defect wherever the answer changes what is drawn
|
|
/// rather than where. Here adding and subtracting are exact and only a
|
|
/// multiply or a conversion rounds, back onto the same steps, so two routes
|
|
/// that come within half a step land on one number and everything downstream
|
|
/// compares for equality instead of for nearness.
|
|
///
|
|
/// `SHIFT` is the number of fractional bits, which is what makes the steps
|
|
/// divide a whole number: a power of two also converts to `f32` without
|
|
/// rounding while the value fits in its mantissa.
|
|
#[repr(transparent)]
|
|
#[derive(
|
|
Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, bytemuck::Pod, bytemuck::Zeroable,
|
|
)]
|
|
pub struct Fixed<const SHIFT: u32>(i32);
|
|
|
|
/// A length or a coordinate in pixels, to a sixty-fourth. Finer than anything
|
|
/// a display can show, and exact in `f32` up to 262,144 px, which is what lets
|
|
/// the same number reach the GPU.
|
|
pub type Px = Fixed<PX_SHIFT>;
|
|
|
|
/// How many bits of a pixel a [`Px`] keeps. One place, because [`PxVec2`]
|
|
/// and the shader's own decoding are the same grid or nothing lines up.
|
|
pub const PX_SHIFT: u32 = 10;
|
|
|
|
/// A share of what a box has left over, which is a weight beside its
|
|
/// siblings rather than a fraction of anything: a list divides its room by
|
|
/// the total of these, so the range has to hold a whole list's worth and the
|
|
/// precision only has to tell two weights apart.
|
|
pub type Weight = Fixed<16>;
|
|
|
|
/// A fraction of a box. Twenty-four bits of it, which matches `f32` around a
|
|
/// half and beats it above one -- where anchors actually sit -- and leaves
|
|
/// +/-128 of range, enough to sum a hundred children each asking for a whole
|
|
/// box. A `leftover` weight is not one of these: it is a share of what is
|
|
/// left rather than a fraction of anything, and it sums over a whole list.
|
|
pub type Rel = Fixed<REL_SHIFT>;
|
|
|
|
/// How many bits of a box a [`Rel`] keeps, beside [`PX_SHIFT`] and for the
|
|
/// same reason.
|
|
pub const REL_SHIFT: u32 = 24;
|
|
|
|
impl<const SHIFT: u32> Fixed<SHIFT> {
|
|
pub const ZERO: Self = Self(0);
|
|
pub const ONE: Self = Self::one();
|
|
/// The gap between neighbouring values, which is also how far apart two
|
|
/// numbers can be and still mean the same place.
|
|
pub const STEP: Self = Self(1);
|
|
/// Also what stands in for an unbounded end, since arithmetic saturates
|
|
/// here rather than wrapping past it.
|
|
pub const MIN: Self = Self(i32::MIN);
|
|
pub const MAX: Self = Self(i32::MAX);
|
|
|
|
const fn one() -> Self {
|
|
assert!(SHIFT < 31, "a Fixed needs a bit for the whole part");
|
|
Self(1 << SHIFT)
|
|
}
|
|
|
|
pub const fn from_raw(raw: i32) -> Self {
|
|
Self(raw)
|
|
}
|
|
|
|
/// The count of steps, for a caller that needs the representation rather
|
|
/// than the number.
|
|
pub const fn raw(self) -> i32 {
|
|
self.0
|
|
}
|
|
|
|
pub const fn from_int(v: i32) -> Self {
|
|
Self(v.saturating_mul(Self::one().0))
|
|
}
|
|
|
|
/// Rounds to the nearest step, and saturates rather than wrapping. A NaN
|
|
/// has no nearest step and becomes zero, which is a caller's mistake
|
|
/// rather than a value worth carrying.
|
|
///
|
|
/// Half-away is written out rather than called through `f32::round`,
|
|
/// which is not `const`: a layout constant has to stay a constant.
|
|
pub const fn from_f32(v: f32) -> Self {
|
|
debug_assert!(!v.is_nan(), "a NaN has no place on the grid");
|
|
let scaled = v * Self::one().0 as f32;
|
|
// Above 2^23 an `f32` has no fractional part left to round, and
|
|
// adding a half there rounds the number itself up instead. The cast
|
|
// saturates at both ends and sends NaN to zero, which is the
|
|
// behaviour wanted at both.
|
|
const WHOLE: f32 = (1 << 23) as f32;
|
|
Self(match (scaled >= WHOLE, scaled <= -WHOLE, scaled < 0.0) {
|
|
(true, _, _) | (_, true, _) => scaled as i32,
|
|
(_, _, true) => (scaled - 0.5) as i32,
|
|
_ => (scaled + 0.5) as i32,
|
|
})
|
|
}
|
|
|
|
/// From a number as it is written in source -- `16`, `1.5` -- which is
|
|
/// the other place a value enters the grid.
|
|
pub fn from_num(v: impl UiNum) -> Self {
|
|
Self::from_f32(v.to_f32())
|
|
}
|
|
|
|
pub const fn to_f32(self) -> f32 {
|
|
self.0 as f32 / Self::one().0 as f32
|
|
}
|
|
|
|
/// The same value on another grid, rounded where the new one is coarser.
|
|
pub const fn to_scale<const TO: u32>(self) -> Fixed<TO> {
|
|
Fixed(match TO >= SHIFT {
|
|
true => narrow((self.0 as i64) << (TO - SHIFT)),
|
|
false => narrow(shift_round(self.0 as i64, SHIFT - TO)),
|
|
})
|
|
}
|
|
|
|
pub const fn add(self, rhs: Self) -> Self {
|
|
Self(self.0.saturating_add(rhs.0))
|
|
}
|
|
|
|
pub const fn sub(self, rhs: Self) -> Self {
|
|
Self(self.0.saturating_sub(rhs.0))
|
|
}
|
|
|
|
pub const fn neg(self) -> Self {
|
|
Self(self.0.saturating_neg())
|
|
}
|
|
|
|
/// Scaled by a number on any grid, which is how a length takes a fraction
|
|
/// of itself and keeps being a length: the product is measured in the
|
|
/// receiver's steps.
|
|
pub const fn mul<const BY: u32>(self, by: Fixed<BY>) -> Self {
|
|
Self(narrow(shift_round(self.0 as i64 * by.0 as i64, BY)))
|
|
}
|
|
|
|
/// Repeated a whole number of times, which no grid rounds.
|
|
pub const fn mul_int(self, by: i32) -> Self {
|
|
Self(narrow(self.0 as i64 * by as i64))
|
|
}
|
|
|
|
/// Divided into a whole number of parts, rounded to the nearest step.
|
|
pub const fn div_int(self, by: i32) -> Self {
|
|
debug_assert!(by != 0, "no part of nothing");
|
|
if by == 0 {
|
|
return Self::ZERO;
|
|
}
|
|
Self(narrow(div_round(self.0 as i64, by as i64)))
|
|
}
|
|
|
|
/// Divided by a number on any grid. A zero divisor is a caller bug -- a
|
|
/// box of no length has no fraction of itself -- and saturates so that a
|
|
/// release build lays out something absurd rather than dying.
|
|
pub const fn div<const BY: u32>(self, by: Fixed<BY>) -> Self {
|
|
debug_assert!(by.0 != 0, "dividing by a length of zero");
|
|
if by.0 == 0 {
|
|
return match self.0 < 0 {
|
|
true => Self::MIN,
|
|
false => Self::MAX,
|
|
};
|
|
}
|
|
Self(narrow(div_round((self.0 as i64) << BY, by.0 as i64)))
|
|
}
|
|
|
|
/// `num / den` on *this* grid rather than on theirs, for weights coarser
|
|
/// than the share they divide.
|
|
pub const fn ratio<const OF: u32>(num: Fixed<OF>, den: Fixed<OF>) -> Self {
|
|
debug_assert!(den.0 != 0, "no part of a whole of nothing");
|
|
if den.0 == 0 {
|
|
return Self::ZERO;
|
|
}
|
|
Self(narrow(div_round((num.0 as i64) << SHIFT, den.0 as i64)))
|
|
}
|
|
|
|
/// `from` and `to` a fraction of the way apart, the fraction being the
|
|
/// receiver -- the argument order [`crate::util::LerpUtil`] already uses.
|
|
pub const fn lerp<const OF: u32>(self, from: Fixed<OF>, to: Fixed<OF>) -> Fixed<OF> {
|
|
from.add(to.sub(from).mul(self))
|
|
}
|
|
|
|
pub const fn min(self, other: Self) -> Self {
|
|
match self.0 < other.0 {
|
|
true => self,
|
|
false => other,
|
|
}
|
|
}
|
|
|
|
pub const fn max(self, other: Self) -> Self {
|
|
match self.0 > other.0 {
|
|
true => self,
|
|
false => other,
|
|
}
|
|
}
|
|
|
|
pub const fn abs(self) -> Self {
|
|
Self(self.0.saturating_abs())
|
|
}
|
|
|
|
pub const fn clamp(self, lo: Self, hi: Self) -> Self {
|
|
debug_assert!(lo.0 <= hi.0, "an empty clamp has no answer");
|
|
self.max(lo).min(hi)
|
|
}
|
|
|
|
/// The next value along, for an interval that must not admit its own
|
|
/// boundary. The step is the whole gap, so there is nothing to exclude
|
|
/// between this and the boundary itself.
|
|
pub const fn next_up(self) -> Self {
|
|
Self(self.0.saturating_add(1))
|
|
}
|
|
|
|
pub const fn next_down(self) -> Self {
|
|
Self(self.0.saturating_sub(1))
|
|
}
|
|
}
|
|
|
|
/// Back to a single step, rounding halves away from zero so that a value and
|
|
/// its negation round to the same distance.
|
|
const fn shift_round(v: i64, bits: u32) -> i64 {
|
|
let half = (1i64 << bits) >> 1;
|
|
match v < 0 {
|
|
true => -((-v + half) >> bits),
|
|
false => (v + half) >> bits,
|
|
}
|
|
}
|
|
|
|
const fn div_round(num: i64, den: i64) -> i64 {
|
|
let (q, rem) = (num / den, num % den);
|
|
match rem.unsigned_abs() * 2 >= den.unsigned_abs() {
|
|
true => match (num < 0) == (den < 0) {
|
|
true => q + 1,
|
|
false => q - 1,
|
|
},
|
|
false => q,
|
|
}
|
|
}
|
|
|
|
/// Toward positive infinity when `up`, toward negative infinity otherwise.
|
|
pub(crate) const fn div_toward(num: i64, den: i64, up: bool) -> i64 {
|
|
let (q, rem) = (num / den, num % den);
|
|
if rem == 0 {
|
|
return q;
|
|
}
|
|
match (rem < 0) == (den < 0) {
|
|
true => q + up as i64,
|
|
false => q - !up as i64,
|
|
}
|
|
}
|
|
|
|
pub(crate) const fn narrow(v: i64) -> i32 {
|
|
if v > i32::MAX as i64 {
|
|
return i32::MAX;
|
|
}
|
|
if v < i32::MIN as i64 {
|
|
return i32::MIN;
|
|
}
|
|
v as i32
|
|
}
|
|
|
|
const impl<const SHIFT: u32> Add for Fixed<SHIFT> {
|
|
type Output = Self;
|
|
|
|
fn add(self, rhs: Self) -> Self {
|
|
Fixed::add(self, rhs)
|
|
}
|
|
}
|
|
|
|
const impl<const SHIFT: u32> Sub for Fixed<SHIFT> {
|
|
type Output = Self;
|
|
|
|
fn sub(self, rhs: Self) -> Self {
|
|
Fixed::sub(self, rhs)
|
|
}
|
|
}
|
|
|
|
const impl<const SHIFT: u32> Neg for Fixed<SHIFT> {
|
|
type Output = Self;
|
|
|
|
fn neg(self) -> Self {
|
|
Fixed::neg(self)
|
|
}
|
|
}
|
|
|
|
const impl<const SHIFT: u32> AddAssign for Fixed<SHIFT> {
|
|
fn add_assign(&mut self, rhs: Self) {
|
|
*self = Fixed::add(*self, rhs);
|
|
}
|
|
}
|
|
|
|
const impl<const SHIFT: u32> SubAssign for Fixed<SHIFT> {
|
|
fn sub_assign(&mut self, rhs: Self) {
|
|
*self = Fixed::sub(*self, rhs);
|
|
}
|
|
}
|
|
|
|
const impl<const SHIFT: u32, const BY: u32> Mul<Fixed<BY>> for Fixed<SHIFT> {
|
|
type Output = Self;
|
|
|
|
fn mul(self, rhs: Fixed<BY>) -> Self {
|
|
Fixed::mul(self, rhs)
|
|
}
|
|
}
|
|
|
|
const impl<const SHIFT: u32, const BY: u32> Div<Fixed<BY>> for Fixed<SHIFT> {
|
|
type Output = Self;
|
|
|
|
fn div(self, rhs: Fixed<BY>) -> Self {
|
|
Fixed::div(self, rhs)
|
|
}
|
|
}
|
|
|
|
impl<const SHIFT: u32> Display for Fixed<SHIFT> {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
Display::fmt(&self.to_f32(), f)
|
|
}
|
|
}
|
|
|
|
/// Prints the number rather than the count of steps: a failing layout test
|
|
/// reports boxes, and `1126` is not a height anybody can read.
|
|
impl<const SHIFT: u32> Debug for Fixed<SHIFT> {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
Display::fmt(&self.to_f32(), f)
|
|
}
|
|
}
|
|
|
|
/// Two of them, for the places a size or a position needs both axes: a
|
|
/// window, a box in pixels, a pointer. Held apart from [`crate::util::Vec2`]
|
|
/// because that one is what the GPU and the platform speak.
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
|
|
pub struct FixedVec2<const SHIFT: u32> {
|
|
pub x: Fixed<SHIFT>,
|
|
pub y: Fixed<SHIFT>,
|
|
}
|
|
|
|
pub type PxVec2 = FixedVec2<PX_SHIFT>;
|
|
|
|
impl<const SHIFT: u32> FixedVec2<SHIFT> {
|
|
pub const ZERO: Self = Self::splat(Fixed::ZERO);
|
|
|
|
pub const fn new(x: Fixed<SHIFT>, y: Fixed<SHIFT>) -> Self {
|
|
Self { x, y }
|
|
}
|
|
|
|
pub const fn splat(v: Fixed<SHIFT>) -> Self {
|
|
Self { x: v, y: v }
|
|
}
|
|
|
|
pub fn from_f32(v: Vec2) -> Self {
|
|
Self::new(Fixed::from_f32(v.x), Fixed::from_f32(v.y))
|
|
}
|
|
|
|
pub fn to_f32(self) -> Vec2 {
|
|
Vec2::new(self.x.to_f32(), self.y.to_f32())
|
|
}
|
|
|
|
pub const fn div_int(self, by: i32) -> Self {
|
|
Self::new(self.x.div_int(by), self.y.div_int(by))
|
|
}
|
|
|
|
pub const fn min(self, other: Self) -> Self {
|
|
Self::new(self.x.min(other.x), self.y.min(other.y))
|
|
}
|
|
|
|
pub const fn max(self, other: Self) -> Self {
|
|
Self::new(self.x.max(other.x), self.y.max(other.y))
|
|
}
|
|
}
|
|
|
|
// `impl_op!` names one concrete type, and this one is generic.
|
|
const impl<const SHIFT: u32> Add for FixedVec2<SHIFT> {
|
|
type Output = Self;
|
|
|
|
fn add(self, rhs: Self) -> Self {
|
|
Self::new(self.x.add(rhs.x), self.y.add(rhs.y))
|
|
}
|
|
}
|
|
|
|
const impl<const SHIFT: u32> Sub for FixedVec2<SHIFT> {
|
|
type Output = Self;
|
|
|
|
fn sub(self, rhs: Self) -> Self {
|
|
Self::new(self.x.sub(rhs.x), self.y.sub(rhs.y))
|
|
}
|
|
}
|
|
|
|
const impl<const SHIFT: u32> AddAssign for FixedVec2<SHIFT> {
|
|
fn add_assign(&mut self, rhs: Self) {
|
|
*self = Add::add(*self, rhs);
|
|
}
|
|
}
|
|
|
|
const impl<const SHIFT: u32> SubAssign for FixedVec2<SHIFT> {
|
|
fn sub_assign(&mut self, rhs: Self) {
|
|
*self = Sub::sub(*self, rhs);
|
|
}
|
|
}
|
|
|
|
impl<const SHIFT: u32> Debug for FixedVec2<SHIFT> {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "({}, {})", self.x, self.y)
|
|
}
|
|
}
|
|
|
|
impl<const SHIFT: u32> Display for FixedVec2<SHIFT> {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "({}, {})", self.x, self.y)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn a_sum_of_steps_does_not_drift() {
|
|
let mut at = Px::ZERO;
|
|
for _ in 0..20_000 {
|
|
at += Px::from_raw(3);
|
|
}
|
|
assert_eq!(at, Px::from_raw(60_000));
|
|
for _ in 0..20_000 {
|
|
at -= Px::from_raw(3);
|
|
}
|
|
assert_eq!(at, Px::ZERO);
|
|
}
|
|
|
|
#[test]
|
|
fn a_pixel_survives_the_trip_through_f32() {
|
|
for raw in [0, 1, -1, 64, -1000, 16_777_215, -16_777_215] {
|
|
let px = Px::from_raw(raw);
|
|
assert_eq!(Px::from_f32(px.to_f32()), px);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_fraction_of_a_length_is_a_length() {
|
|
let half = Px::from_int(100) * Rel::from_f32(0.5);
|
|
assert_eq!(half, Px::from_int(50));
|
|
assert_eq!(Px::from_int(100) * Rel::ONE, Px::from_int(100));
|
|
assert_eq!(Px::from_int(100) * Rel::ZERO, Px::ZERO);
|
|
}
|
|
|
|
#[test]
|
|
fn halves_round_away_from_zero_either_side() {
|
|
// A step and a half of one, which has no step of its own.
|
|
let step_and_a_half = Rel::from_f32(1.5).div_int(Px::ONE.raw());
|
|
assert_eq!(Px::ONE * step_and_a_half, Px::from_raw(2));
|
|
assert_eq!(Px::ONE.neg() * step_and_a_half, Px::from_raw(-2));
|
|
}
|
|
|
|
#[test]
|
|
fn dividing_by_a_fraction_undoes_multiplying_by_it() {
|
|
let third = Rel::ONE / Rel::from_int(3);
|
|
let len = Px::from_int(300);
|
|
assert_eq!(len * third / third, len);
|
|
assert_eq!(Px::from_int(100) / Rel::from_f32(0.5), Px::from_int(200));
|
|
}
|
|
|
|
#[test]
|
|
fn arithmetic_saturates_rather_than_wrapping() {
|
|
assert_eq!(Px::MAX + Px::ONE, Px::MAX);
|
|
assert_eq!(Px::MIN - Px::ONE, Px::MIN);
|
|
assert_eq!(Px::from_f32(1e12), Px::MAX);
|
|
assert_eq!(Px::from_f32(-1e12), Px::MIN);
|
|
assert_eq!(Px::from_int(i32::MAX), Px::MAX);
|
|
}
|
|
|
|
#[test]
|
|
fn a_coarser_grid_rounds_and_a_finer_one_does_not() {
|
|
// A third, which neither grid holds exactly.
|
|
let third = Rel::ONE / Rel::from_int(3);
|
|
assert_eq!(third.to_scale::<6>(), Fixed::<6>::from_raw(21));
|
|
let coarse = Fixed::<6>::from_raw(21);
|
|
assert_eq!(coarse.to_scale::<24>().to_scale::<6>(), coarse);
|
|
}
|
|
|
|
#[test]
|
|
fn lerp_takes_the_fraction_as_the_receiver() {
|
|
let (from, to) = (Px::from_int(10), Px::from_int(20));
|
|
assert_eq!(Rel::ZERO.lerp(from, to), from);
|
|
assert_eq!(Rel::ONE.lerp(from, to), to);
|
|
assert_eq!(Rel::from_f32(0.5).lerp(from, to), Px::from_int(15));
|
|
assert_eq!(Rel::from_f32(0.5).lerp(to, from), Px::from_int(15));
|
|
}
|
|
|
|
#[test]
|
|
fn a_ratio_is_finer_than_the_weights_it_divides() {
|
|
let (one, three) = (Weight::ONE, Weight::from_int(3));
|
|
// A third, which the weights' own grid could only hold to 1/65536.
|
|
assert_eq!(Rel::ratio(one, three), Rel::from_raw(5592405));
|
|
assert_eq!(Rel::ratio(three, three), Rel::ONE);
|
|
assert_eq!(Rel::ratio(Weight::ZERO, three), Rel::ZERO);
|
|
}
|
|
|
|
#[test]
|
|
fn nothing_sits_between_a_value_and_the_next_one() {
|
|
let at = Px::from_int(3);
|
|
assert_eq!(at.next_up().next_down(), at);
|
|
assert_eq!(at.next_up().raw() - at.raw(), 1);
|
|
assert!(at.next_down() < at && at < at.next_up());
|
|
}
|
|
|
|
#[test]
|
|
fn it_prints_the_number_rather_than_the_steps() {
|
|
assert_eq!(format!("{:?}", Px::from_f32(17.59375)), "17.59375");
|
|
assert_eq!(format!("{}", Px::from_int(-2)), "-2");
|
|
}
|
|
}
|