Decide layout on the grid end to end, and delete the tolerance

`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>
This commit is contained in:
iris-aiandClaude Opus 5 committed 2026-09-16 02:56:48 -04:00
1 parent bd6de71a55
commit 39e4ca20e6
24 files changed
+420 -194

No files matched your search

+124 -12
View File
@@ -1,4 +1,4 @@
use crate::UiNum; use crate::{UiNum, util::Vec2};
use std::{ use std::{
fmt::{Debug, Display, Formatter}, fmt::{Debug, Display, Formatter},
ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign}, ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign},
@@ -27,7 +27,11 @@ pub struct Fixed<const SHIFT: u32>(i32);
/// A length or a coordinate in pixels, to a sixty-fourth. Finer than anything /// 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 /// a display can show, and exact in `f32` up to 262,144 px, which is what lets
/// the same number reach the GPU. /// the same number reach the GPU.
pub type Px = Fixed<6>; 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 /// 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 /// siblings rather than a fraction of anything: a list divides its room by
@@ -40,7 +44,11 @@ pub type Weight = Fixed<16>;
/// +/-128 of range, enough to sum a hundred children each asking for a whole /// +/-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 /// 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. /// left rather than a fraction of anything, and it sums over a whole list.
pub type Rel = Fixed<24>; 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> { impl<const SHIFT: u32> Fixed<SHIFT> {
pub const ZERO: Self = Self(0); pub const ZERO: Self = Self(0);
@@ -135,6 +143,15 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
Self(narrow(self.0 as i64 * by as i64)) 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 /// 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 /// box of no length has no fraction of itself -- and saturates so that a
/// release build lays out something absurd rather than dying. /// release build lays out something absurd rather than dying.
@@ -221,7 +238,19 @@ const fn div_round(num: i64, den: i64) -> i64 {
} }
} }
const fn narrow(v: i64) -> i32 { /// 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 { if v > i32::MAX as i64 {
return i32::MAX; return i32::MAX;
} }
@@ -297,6 +326,91 @@ impl<const SHIFT: u32> Debug for Fixed<SHIFT> {
} }
} }
/// 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -332,10 +446,10 @@ mod tests {
#[test] #[test]
fn halves_round_away_from_zero_either_side() { fn halves_round_away_from_zero_either_side() {
// A sixty-fourth and a half of one, which has no step of its own. // A step and a half of one, which has no step of its own.
let one_and_a_half = Rel::from_f32(1.5) / Rel::from_int(64); let step_and_a_half = Rel::from_f32(1.5).div_int(Px::ONE.raw());
assert_eq!(Px::ONE * one_and_a_half, Px::from_raw(2)); assert_eq!(Px::ONE * step_and_a_half, Px::from_raw(2));
assert_eq!(Px::ONE.neg() * one_and_a_half, Px::from_raw(-2)); assert_eq!(Px::ONE.neg() * step_and_a_half, Px::from_raw(-2));
} }
#[test] #[test]
@@ -360,10 +474,8 @@ mod tests {
// A third, which neither grid holds exactly. // A third, which neither grid holds exactly.
let third = Rel::ONE / Rel::from_int(3); let third = Rel::ONE / Rel::from_int(3);
assert_eq!(third.to_scale::<6>(), Fixed::<6>::from_raw(21)); assert_eq!(third.to_scale::<6>(), Fixed::<6>::from_raw(21));
assert_eq!( let coarse = Fixed::<6>::from_raw(21);
Px::from_raw(21).to_scale::<24>().to_scale::<6>(), assert_eq!(coarse.to_scale::<24>().to_scale::<6>(), coarse);
Px::from_raw(21)
);
} }
#[test] #[test]
+3 -3
View File
@@ -15,7 +15,7 @@
//! reuse, size, placement, and text events for one suspicious widget. The //! reuse, size, placement, and text events for one suspicious widget. The
//! selection is a set and survives [`take`] until cleared. //! selection is a set and survives [`take`] until cleared.
use crate::{Axis, Len, Size, UiRegion, WidgetId, util::Vec2}; use crate::{Axis, Len, PxVec2, Size, UiRegion, WidgetId};
use std::{ use std::{
cell::RefCell, cell::RefCell,
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
@@ -258,7 +258,7 @@ pub enum TraceEvent {
id: WidgetId, id: WidgetId,
parent: Option<WidgetId>, parent: Option<WidgetId>,
region: UiRegion, region: UiRegion,
pixel_size: Vec2, pixel_size: PxVec2,
region_node: bool, region_node: bool,
}, },
Reuse { Reuse {
@@ -354,7 +354,7 @@ pub(crate) fn draw_request(
id: WidgetId, id: WidgetId,
parent: Option<WidgetId>, parent: Option<WidgetId>,
region: UiRegion, region: UiRegion,
pixel_size: Vec2, pixel_size: PxVec2,
region_node: bool, region_node: bool,
) { ) {
trace( trace(
+24
View File
@@ -1,4 +1,5 @@
use super::*; use super::*;
use crate::{Fixed, FixedVec2};
#[derive(Copy, Clone, Debug, Eq, PartialEq)] #[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Axis { pub enum Axis {
@@ -40,6 +41,29 @@ pub enum Sign {
Pos, Pos,
} }
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),
Axis::Y => Self::new(ortho, aligned),
}
}
}
impl Vec2 { impl Vec2 {
pub fn axis(&self, axis: Axis) -> f32 { pub fn axis(&self, axis: Axis) -> f32 {
match axis { match axis {
+15 -3
View File
@@ -1,5 +1,5 @@
use super::*; use super::*;
use crate::{Px, Rel, UiNum, Weight, util::impl_op}; use crate::{Px, PxVec2, Rel, UiNum, Weight, util::impl_op};
#[derive(Debug, Default, Clone, Copy, PartialEq)] #[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct Size { pub struct Size {
@@ -49,10 +49,22 @@ impl Size {
y: Len::LEFTOVER, y: Len::LEFTOVER,
}; };
/// From something measured outside layout -- a texture, a shaped line --
/// which is where a size in floats comes from.
pub fn px(v: Vec2) -> Self { pub fn px(v: Vec2) -> Self {
Self::from_px(PxVec2::from_f32(v))
}
pub const fn from_px(v: PxVec2) -> Self {
Self { Self {
x: Len::px(v.x), x: Len {
y: Len::px(v.y), px: v.x,
..Len::ZERO
},
y: Len {
px: v.y,
..Len::ZERO
},
} }
} }
+13 -16
View File
@@ -1,7 +1,7 @@
use std::{fmt::Display, marker::Destruct}; use std::{fmt::Display, marker::Destruct};
use super::*; use super::*;
use crate::{Px, Rel, UiNum, util::impl_op}; use crate::{Px, PxVec2, Rel, UiNum, util::impl_op};
#[repr(C)] #[repr(C)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, bytemuck::Zeroable, Default)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, bytemuck::Zeroable, Default)]
@@ -67,13 +67,10 @@ impl UiVec2 {
} }
} }
/// Resolved against a box of `size`, in whole `f32` pixels for a caller /// Resolved against a box of `size`, which is where a fraction stops
/// outside layout -- a pointer position, or something being drawn. /// being one and becomes a place.
pub fn to_px(&self, size: Vec2) -> Vec2 { pub fn to_px(&self, size: PxVec2) -> PxVec2 {
Vec2 { PxVec2::new(self.x.to_px(size.x), self.y.to_px(size.y))
x: self.x.to_px(Px::from_f32(size.x)).to_f32(),
y: self.y.to_px(Px::from_f32(size.y)).to_f32(),
}
} }
pub const FULL_SIZE: Self = Self::rel(Vec2::ONE); pub const FULL_SIZE: Self = Self::rel(Vec2::ONE);
@@ -344,10 +341,10 @@ impl UiRegion {
self self
} }
pub fn to_px(&self, size: Vec2) -> PixelRegion { pub fn to_px(&self, size: PxVec2) -> PixelRegion {
PixelRegion { PixelRegion {
top_left: self.top_left().get_rel() * size + self.top_left().get_px(), top_left: self.top_left().to_px(size),
bot_right: self.bot_right().get_rel() * size + self.bot_right().get_px(), bot_right: self.bot_right().to_px(size),
} }
} }
@@ -402,21 +399,21 @@ impl Display for UiRegion {
} }
} }
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PixelRegion { pub struct PixelRegion {
pub top_left: Vec2, pub top_left: PxVec2,
pub bot_right: Vec2, pub bot_right: PxVec2,
} }
impl PixelRegion { impl PixelRegion {
pub fn contains(&self, pos: Vec2) -> bool { pub fn contains(&self, pos: PxVec2) -> bool {
pos.x >= self.top_left.x pos.x >= self.top_left.x
&& pos.x <= self.bot_right.x && pos.x <= self.bot_right.x
&& pos.y >= self.top_left.y && pos.y >= self.top_left.y
&& pos.y <= self.bot_right.y && pos.y <= self.bot_right.y
} }
pub fn size(&self) -> Vec2 { pub fn size(&self) -> PxVec2 {
self.bot_right - self.top_left self.bot_right - self.top_left
} }
} }
+8 -1
View File
@@ -23,7 +23,14 @@ pub use primitive::*;
const PRELUDE: &str = include_str!("./shader/prelude.wgsl"); const PRELUDE: &str = include_str!("./shader/prelude.wgsl");
fn module_source(wgsl: &str) -> String { fn module_source(wgsl: &str) -> String {
format!("{PRELUDE}\n{wgsl}") // The steps come from the same constants the CPU counts in, rather than
// a second copy of them written into the shader: a grid the two disagree
// about puts every coordinate somewhere else.
format!(
"const PX_STEP: f32 = 1.0 / {}.0;\nconst REL_STEP: f32 = 1.0 / {}.0;\n{PRELUDE}\n{wgsl}",
1u32 << crate::PX_SHIFT,
1u32 << crate::REL_SHIFT,
)
} }
pub struct UiRenderNode { pub struct UiRenderNode {
+3 -5
View File
@@ -26,11 +26,9 @@ struct MoveOffset {
parent: u32, parent: u32,
} }
// What `iris_core` stores: a whole count of a sixty-fourth of a pixel, and of // `PX_STEP` and `REL_STEP` are prepended from `iris_core`'s own constants:
// a `1 / 2^24` of a box. Both steps are powers of two, so decoding one is // what it stores is a whole count of each, both powers of two, so decoding
// exact and the number here is the number the CPU decided. // is exact and the number here is the number the CPU decided.
const PX_STEP: f32 = 1.0 / 64.0;
const REL_STEP: f32 = 1.0 / 16777216.0;
// Every coordinate the CPU decided is a whole count of `PX_STEP`, so one that // Every coordinate the CPU decided is a whole count of `PX_STEP`, so one that
// composes to within half a step of a pixel boundary is on that boundary and // composes to within half a step of a pixel boundary is on that boundary and
+1 -1
View File
@@ -58,7 +58,7 @@ pub struct ActiveData {
impl ActiveData { impl ActiveData {
/// Whether its drawing and size hold for a box of these pixel lengths. /// Whether its drawing and size hold for a box of these pixel lengths.
pub fn holds_at(&self, px: crate::util::Vec2) -> bool { pub fn holds_at(&self, px: crate::PxVec2) -> bool {
self.holds[0].contains(px.x) && self.holds[1].contains(px.y) self.holds[0].contains(px.x) && self.holds[1].contains(px.y)
} }
} }
+74 -48
View File
@@ -1,4 +1,4 @@
use crate::{Rel, UiScalar}; use crate::{Px, REL_SHIFT, UiScalar, fixed::div_toward, fixed::narrow};
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
/// The lengths of a box, in pixels, that one drawing of a widget holds for: /// The lengths of a box, in pixels, that one drawing of a widget holds for:
@@ -7,48 +7,33 @@ use std::ops::RangeInclusive;
/// for every length; one that does holds for the one it read unless it says /// for every length; one that does holds for the one it read unless it says
/// otherwise, and a parent holds for whatever keeps every child it asked /// otherwise, and a parent holds for whatever keeps every child it asked
/// about or drew inside its own range. /// about or drew inside its own range.
#[derive(Clone, Copy, Debug, PartialEq)] ///
/// The ends are lengths on the grid rather than floats with a tolerance
/// around them: a box offered back at the length a widget reported comes back
/// as the same number, so a range means what it says. What widening there is
/// belongs to [`Self::through`], which has a rounding to undo, and is derived
/// from that rounding rather than chosen.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Holds { pub struct Holds {
pub lo: f32, pub lo: Px,
pub hi: f32, pub hi: Px,
} }
/// How far outside a range a length may fall and still be inside it: a box
/// offered back to a widget at the length it reported comes back through the
/// chain a few bits off, and nothing a reader could see lives in that gap.
pub const HOLDS_EPSILON_PX: f32 = 0.05;
impl Holds { impl Holds {
pub const ANY: Self = Self { pub const ANY: Self = Self {
lo: f32::NEG_INFINITY, lo: Px::MIN,
hi: f32::INFINITY, hi: Px::MAX,
}; };
pub const fn at(len: f32) -> Self { pub const fn at(len: Px) -> Self {
Self::tolerant(len, len) Self { lo: len, hi: len }
} }
const fn tolerant(lo: f32, hi: f32) -> Self { pub const fn contains(&self, len: Px) -> bool {
Self { len.raw() >= self.lo.raw() && len.raw() <= self.hi.raw()
lo: lo - HOLDS_EPSILON_PX,
hi: hi + HOLDS_EPSILON_PX,
}
} }
/// A range whose endpoints are exact, for a widget decision with a hard pub const fn and(self, other: Self) -> Self {
/// boundary rather than an accumulated coordinate-rounding difference.
pub fn exact(range: RangeInclusive<f32>) -> Self {
Self {
lo: *range.start(),
hi: *range.end(),
}
}
pub fn contains(&self, len: f32) -> bool {
len >= self.lo && len <= self.hi
}
pub fn and(self, other: Self) -> Self {
Self { Self {
lo: self.lo.max(other.lo), lo: self.lo.max(other.lo),
hi: self.hi.min(other.hi), hi: self.hi.min(other.hi),
@@ -58,41 +43,82 @@ impl Holds {
/// What a box has to be for a part of it, `len` of the box long, to stay /// What a box has to be for a part of it, `len` of the box long, to stay
/// in this range. A part with no relative extent is a fixed length: it /// in this range. A part with no relative extent is a fixed length: it
/// was drawn at that length and any box keeps it there. /// was drawn at that length and any box keeps it there.
pub fn through(self, len: UiScalar) -> Self { ///
if len.rel == Rel::ZERO { /// The way in is `px + rel * box` taken to the nearest step, so a part
/// of exactly `lo` came from anything within half a step of it and the
/// answer is an interval even where this range is one length. Inverting
/// the length alone instead gives a point that need not even contain the
/// box the part was drawn in, which is a range excluding the drawing it
/// was made for.
pub const fn through(self, len: UiScalar) -> Self {
let rel = len.rel.raw() as i64;
if rel == 0 {
return Self::ANY; return Self::ANY;
} }
let (rel, px) = (len.rel.to_f32(), len.px.to_f32()); // Three half steps either side -- one for the rounding on the way
let a = (self.lo - px) / rel; // in, two for the difference between a length composed down the
let b = (self.hi - px) / rel; // chain and the same length measured against the window -- and half
// of what a `Rel` counts in, to divide by the fraction. Exact until
// the division takes it back to the grid.
let px = len.px.raw() as i64;
let half_rel = REL_SHIFT - 1;
let lo = ((self.lo.raw() as i64 - px) * 2 - 3) << half_rel;
let hi = ((self.hi.raw() as i64 - px) * 2 + 3) << half_rel;
let (a, b) = (div_toward(lo, rel, true), div_toward(hi, rel, false));
let (c, d) = (div_toward(lo, rel, false), div_toward(hi, rel, true));
match rel > 0 {
true => Self::raws(a, b),
false => Self::raws(d, c),
}
}
const fn raws(lo: i64, hi: i64) -> Self {
Self { Self {
lo: a.min(b), lo: Px::from_raw(narrow(lo)),
hi: a.max(b), hi: Px::from_raw(narrow(hi)),
} }
} }
} }
impl From<RangeInclusive<f32>> for Holds { impl From<RangeInclusive<Px>> for Holds {
fn from(range: RangeInclusive<f32>) -> Self { fn from(range: RangeInclusive<Px>) -> Self {
Self::tolerant(*range.start(), *range.end()) Self {
lo: *range.start(),
hi: *range.end(),
}
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::Rel;
#[test] #[test]
fn through_reverses_a_range_for_a_negative_fraction() { fn through_reverses_a_range_for_a_negative_fraction() {
let holds = Holds::from(20.0..=40.0).through(UiScalar::new(-0.5, 10.0)); // `10 - box / 2` is between 20 and 40 for boxes from -60 to -20.
assert!((holds.lo - -60.1).abs() < 0.001); let part = UiScalar::from_parts(Rel::from_f32(-0.5), Px::from_int(10));
assert!((holds.hi - -19.9).abs() < 0.001); let holds = Holds::from(Px::from_int(20)..=Px::from_int(40)).through(part);
assert!(holds.contains(Px::from_int(-60)) && holds.contains(Px::from_int(-20)));
assert!(!holds.contains(Px::from_int(-61)) && !holds.contains(Px::from_int(-19)));
}
/// The case the widening is for: a part that holds only for the length it
/// was drawn at has to hold for the box it was drawn in, and a third of a
/// box is not a whole number of steps.
#[test]
fn a_part_maps_back_onto_the_box_it_was_measured_in() {
let part = UiScalar::from_parts(Rel::from_f32(1.0 / 3.0), Px::from_int(-146));
for box_len in (440..460).map(Px::from_int) {
let holds = Holds::at(part.to_px(box_len)).through(part);
assert!(holds.contains(box_len), "{box_len:?} left out by {holds:?}");
}
} }
#[test] #[test]
fn an_exact_open_boundary_does_not_admit_the_boundary() { fn a_boundary_the_next_step_along_does_not_admit_it() {
let boundary = 10.0_f32; let boundary = Px::from_int(10);
let above = Holds::exact(boundary.next_up()..=f32::INFINITY); let above = Holds::from(boundary.next_up()..=Px::MAX);
assert!(!above.contains(boundary)); assert!(!above.contains(boundary));
assert!(above.contains(boundary.next_up())); assert!(above.contains(boundary.next_up()));
} }
+8 -9
View File
@@ -1,7 +1,7 @@
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter}; use crate::layout_diagnostics::{self as diag, Counter};
use crate::{ use crate::{
Axis, Holds, Len, Px, RegionAlign, Rel, RenderedText, Size, StrongWidget, TextAttrs, Axis, Holds, Len, Px, PxVec2, RegionAlign, Rel, RenderedText, Size, StrongWidget, TextAttrs,
TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Weight, TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Weight,
WidgetId, Widgets, WidgetId, Widgets,
render::{ render::{
@@ -9,7 +9,6 @@ use crate::{
PrimitiveKind, TexturePrimitive, PrimitiveKind, TexturePrimitive,
}, },
ui::render_state::DrawInfo, ui::render_state::DrawInfo,
util::Vec2,
}; };
const AXES: [Axis; 2] = [Axis::X, Axis::Y]; const AXES: [Axis; 2] = [Axis::X, Axis::Y];
@@ -28,7 +27,7 @@ pub struct Painter<'a> {
/// is the one recorded as its offer. /// is the one recorded as its offer.
pub(super) offered: Vec<WidgetId>, pub(super) offered: Vec<WidgetId>,
/// The box this widget was first asked about in, in pixels. /// The box this widget was first asked about in, in pixels.
pub(super) offered_px: Vec2, pub(super) offered_px: PxVec2,
/// Whether this draw is in that box, which makes the questions it asks /// Whether this draw is in that box, which makes the questions it asks
/// the ones a cold layout asks and their answers the ones to keep. /// the ones a cold layout asks and their answers the ones to keep.
pub(super) at_offer: bool, pub(super) at_offer: bool,
@@ -291,11 +290,11 @@ impl<'a> Painter<'a> {
} }
/// The pixel size of a part of the box this widget was asked in. /// The pixel size of a part of the box this widget was asked in.
fn px_within_offer(&self, local: UiRegion) -> Vec2 { fn px_within_offer(&self, local: UiRegion) -> PxVec2 {
let size = local.size(); let size = local.size();
Vec2::new( PxVec2::new(
size.x.to_px(Px::from_f32(self.offered_px.x)).to_f32(), size.x.to_px(self.offered_px.x),
size.y.to_px(Px::from_f32(self.offered_px.y)).to_f32(), size.y.to_px(self.offered_px.y),
) )
} }
@@ -363,7 +362,7 @@ impl<'a> Painter<'a> {
/// This widget's box in pixels. Reading it makes the drawing one that /// This widget's box in pixels. Reading it makes the drawing one that
/// holds for this box only, until `holds` says how far it goes. /// holds for this box only, until `holds` says how far it goes.
pub fn px_size(&mut self) -> Vec2 { pub fn px_size(&mut self) -> PxVec2 {
let px = self.state.px_of(self.move_idx, self.region); let px = self.state.px_of(self.move_idx, self.region);
for (own, len) in self.own.iter_mut().zip([px.x, px.y]) { for (own, len) in self.own.iter_mut().zip([px.x, px.y]) {
if *own == Holds::ANY { if *own == Holds::ANY {
@@ -375,7 +374,7 @@ impl<'a> Painter<'a> {
/// One axis of this widget's box in pixels. Prefer this to /// One axis of this widget's box in pixels. Prefer this to
/// [`Self::px_size`] when the other axis cannot affect the drawing. /// [`Self::px_size`] when the other axis cannot affect the drawing.
pub fn px_len(&mut self, axis: Axis) -> f32 { pub fn px_len(&mut self, axis: Axis) -> Px {
let len = self.state.px_of(self.move_idx, self.region).axis(axis); let len = self.state.px_of(self.move_idx, self.region).axis(axis);
let own = &mut self.own[axis as usize]; let own = &mut self.own[axis as usize];
if *own == Holds::ANY { if *own == Holds::ANY {
+30 -14
View File
@@ -3,8 +3,8 @@ use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind};
use crate::ui::painter::{declared_box, declared_lens, placed_box}; use crate::ui::painter::{declared_box, declared_lens, placed_box};
use crate::{ use crate::{
ActiveData, Axis, DrawLayers, Holds, IdLike, Len, MaskIdx, MoveIdx, Moves, Painter, ActiveData, Axis, DrawLayers, Holds, IdLike, Len, MaskIdx, MoveIdx, Moves, Painter,
PixelRegion, RegionAlign, Rel, Size, StrongWidget, UiRegion, UiRsc, UiScalar, UiSpan, Weight, PixelRegion, PxVec2, RegionAlign, Rel, Size, StrongWidget, UiRegion, UiRsc, UiScalar, UiSpan,
WidgetId, Widgets, Weight, WidgetId, Widgets,
util::{HashMap, Vec2}, util::{HashMap, Vec2},
}; };
@@ -23,7 +23,7 @@ pub(super) struct DrawInfo {
/// The box it was first asked about in, as a part of its parent's, and /// The box it was first asked about in, as a part of its parent's, and
/// that box in pixels. /// that box in pixels.
pub offer: UiRegion, pub offer: UiRegion,
pub offered_px: Vec2, pub offered_px: PxVec2,
/// A container's answer for where the widget sits. `None` uses the /// A container's answer for where the widget sits. `None` uses the
/// widget's own property. /// widget's own property.
pub align: Option<RegionAlign>, pub align: Option<RegionAlign>,
@@ -32,7 +32,7 @@ pub(super) struct DrawInfo {
pub struct UiRenderState { pub struct UiRenderState {
pub active: HashMap<WidgetId, ActiveData>, pub active: HashMap<WidgetId, ActiveData>,
pub layers: DrawLayers, pub layers: DrawLayers,
pub(super) output_size: Vec2, pub(super) output_size: PxVec2,
old_root: Option<WidgetId>, old_root: Option<WidgetId>,
/// The slot every chain bottoms out in, holding the output as a box. /// The slot every chain bottoms out in, holding the output as a box.
@@ -58,7 +58,7 @@ impl UiRenderState {
Self { Self {
active: Default::default(), active: Default::default(),
layers: Default::default(), layers: Default::default(),
output_size: Vec2::ZERO, output_size: PxVec2::ZERO,
old_root: None, old_root: None,
slots: Default::default(), slots: Default::default(),
answer_invalid: Default::default(), answer_invalid: Default::default(),
@@ -75,8 +75,14 @@ impl UiRenderState {
/// downstream has to know the output's size to resolve a position. /// downstream has to know the output's size to resolve a position.
fn write_root(&mut self) { fn write_root(&mut self) {
let region = UiRegion::new( let region = UiRegion::new(
UiSpan::new(UiScalar::ZERO, UiScalar::px(self.output_size.x)), UiSpan::new(
UiSpan::new(UiScalar::ZERO, UiScalar::px(self.output_size.y)), UiScalar::ZERO,
UiScalar::from_parts(Rel::ZERO, self.output_size.x),
),
UiSpan::new(
UiScalar::ZERO,
UiScalar::from_parts(Rel::ZERO, self.output_size.y),
),
); );
match self.root_move == MoveIdx::NONE { match self.root_move == MoveIdx::NONE {
true => self.root_move = self.moves.push(MoveIdx::NONE, region), true => self.root_move = self.moves.push(MoveIdx::NONE, region),
@@ -84,8 +90,10 @@ impl UiRenderState {
} }
} }
/// The window, in whatever the platform measures it in, onto the grid
/// everything below it is decided on.
pub fn resize(&mut self, size: impl Into<Vec2>) { pub fn resize(&mut self, size: impl Into<Vec2>) {
let size = size.into(); let size = PxVec2::from_f32(size.into());
if size == self.output_size { if size == self.output_size {
return; return;
} }
@@ -108,7 +116,7 @@ impl UiRenderState {
} }
} }
pub fn output_size(&self) -> Vec2 { pub fn output_size(&self) -> PxVec2 {
self.output_size self.output_size
} }
@@ -447,7 +455,7 @@ impl UiRenderState {
} }
/// The pixel size of a region held in `slot`'s coordinates. /// The pixel size of a region held in `slot`'s coordinates.
pub(super) fn px_of(&self, slot: MoveIdx, region: UiRegion) -> Vec2 { pub(super) fn px_of(&self, slot: MoveIdx, region: UiRegion) -> PxVec2 {
self.moves self.moves
.resolve(slot, region) .resolve(slot, region)
.size() .size()
@@ -460,7 +468,7 @@ impl UiRenderState {
pub(super) fn retained_size( pub(super) fn retained_size(
&self, &self,
id: WidgetId, id: WidgetId,
px: Vec2, px: PxVec2,
parent_move: MoveIdx, parent_move: MoveIdx,
widgets: &Widgets, widgets: &Widgets,
) -> Option<(Size, [Holds; 2])> { ) -> Option<(Size, [Holds; 2])> {
@@ -1003,8 +1011,10 @@ impl UiRenderState {
} }
} }
fn same_px(a: Vec2, b: Vec2) -> bool { /// The same box is the same number of steps, both of these being lengths on
Holds::at(a.x).contains(b.x) && Holds::at(a.y).contains(b.y) /// the grid rather than floats to be compared for nearness.
fn same_px(a: PxVec2, b: PxVec2) -> bool {
a == b
} }
fn same_pixel_region(a: PixelRegion, b: PixelRegion) -> bool { fn same_pixel_region(a: PixelRegion, b: PixelRegion) -> bool {
@@ -1045,7 +1055,13 @@ impl RegionRemap {
fn apply_scalar(self, scalar: UiScalar, from: UiSpan, to: UiSpan) -> UiScalar { fn apply_scalar(self, scalar: UiScalar, from: UiSpan, to: UiSpan) -> UiScalar {
let extent = from.end.rel - from.start.rel; let extent = from.end.rel - from.start.rel;
if extent == Rel::ZERO { // A box that only moved, or that has no relative extent to divide,
// carries its parts by moving them, which is exact. Dividing to find
// the fraction each sits at and multiplying to place it again are two
// roundings, and they land a step from where growing the tree that
// way does. Where the box changed length there is nothing else to do,
// and the fraction is what a part means.
if from.len() == to.len() || extent == Rel::ZERO {
return scalar + to.start - from.start; return scalar + to.start - from.start;
} }
let fraction = (scalar.rel - from.start.rel) / extent; let fraction = (scalar.rel - from.start.rel) / extent;
+6 -4
View File
@@ -15,8 +15,10 @@ where
let region = ctx.data.render.window_region(&id).unwrap(); let region = ctx.data.render.window_region(&id).unwrap();
let id_pos = region.top_left; let id_pos = region.top_left;
let container_pos = ctx.data.render.window_region(&container).unwrap().top_left; let container_pos = ctx.data.render.window_region(&container).unwrap().top_left;
let pos = ctx.data.pos + container_pos - id_pos; // The pointer arrives from the platform in floats; everything
let size = region.size(); // it is compared against is on the grid.
let pos = (PxVec2::from_f32(ctx.data.pos) + container_pos - id_pos).to_f32();
let size = region.size().to_f32();
select( select(
rsc, rsc,
ctx.data.render, ctx.data.render,
@@ -70,8 +72,8 @@ fn select(
if let Some(region) = render.window_region(&id) { if let Some(region) = render.window_region(&id) {
state.window.set_ime_allowed(true); state.window.set_ime_allowed(true);
state.window.set_ime_cursor_area( state.window.set_ime_cursor_area(
LogicalPosition::<f32>::from(region.top_left.tuple()), LogicalPosition::<f32>::from(region.top_left.to_f32().tuple()),
LogicalSize::<f32>::from(region.size().tuple()), LogicalSize::<f32>::from(region.size().to_f32().tuple()),
); );
} }
state.focus = Some(id); state.focus = Some(id);
+3 -3
View File
@@ -198,7 +198,7 @@ impl SensorUi for UiRenderState {
let Some(region) = region_of(id) else { let Some(region) = region_of(id) else {
continue; continue;
}; };
if !cursor.exists || !region.contains(cursor.pos) { if !cursor.exists || !region.contains(PxVec2::from_f32(cursor.pos)) {
continue; continue;
} }
hovered.now.push(id); hovered.now.push(id);
@@ -249,8 +249,8 @@ fn deliver<Rsc: HasEvents>(
region: PixelRegion, region: PixelRegion,
) -> bool { ) -> bool {
let data = CursorData { let data = CursorData {
pos: cursor.pos - region.top_left, pos: cursor.pos - region.top_left.to_f32(),
size: region.bot_right - region.top_left, size: region.size().to_f32(),
scroll_delta: cursor.scroll_delta, scroll_delta: cursor.scroll_delta,
hover, hover,
cursor: cursor.clone(), cursor: cursor.clone(),
+9 -3
View File
@@ -29,8 +29,14 @@ macro_rules! assert_corners {
assert_eq!( assert_eq!(
$harness.region(&$id).expect("widget drew nothing"), $harness.region(&$id).expect("widget drew nothing"),
$crate::core::PixelRegion { $crate::core::PixelRegion {
top_left: $crate::core::util::Vec2::new($x0 as f32, $y0 as f32), top_left: $crate::core::PxVec2::new(
bot_right: $crate::core::util::Vec2::new($x1 as f32, $y1 as f32), $crate::core::Px::from_f32($x0 as f32),
$crate::core::Px::from_f32($y0 as f32),
),
bot_right: $crate::core::PxVec2::new(
$crate::core::Px::from_f32($x1 as f32),
$crate::core::Px::from_f32($y1 as f32),
),
} }
); );
}; };
@@ -151,7 +157,7 @@ impl Harness {
} }
pub fn size(&self) -> Vec2 { pub fn size(&self) -> Vec2 {
self.render.output_size() self.render.output_size().to_f32()
} }
pub fn resize(&mut self, size: impl Into<Vec2>) { pub fn resize(&mut self, size: impl Into<Vec2>) {
+2 -5
View File
@@ -113,14 +113,11 @@ impl Widget for Branch {
let mut top = UiRegion::FULL; let mut top = UiRegion::FULL;
top.y.end = top.y.start.offset(Px::from_int(40)); top.y.end = top.y.start.offset(Px::from_int(40));
let measured = painter.widget_within(&self.probe, top).len(Axis::X); let measured = painter.widget_within(&self.probe, top).len(Axis::X);
let px = measured let px = measured.apply_leftover().to_px(painter.px_len(Axis::X));
.apply_leftover()
.to_px(Px::from_f32(painter.px_len(Axis::X)))
.to_f32();
let mut below = UiRegion::FULL; let mut below = UiRegion::FULL;
below.y.start = below.y.start.offset(Px::from_int(40)); below.y.start = below.y.start.offset(Px::from_int(40));
match px > self.threshold { match px > Px::from_f32(self.threshold) {
true => painter.widget_within(&self.wide, below), true => painter.widget_within(&self.wide, below),
false => painter.widget_within(&self.narrow, below), false => painter.widget_within(&self.narrow, below),
}; };
+23 -19
View File
@@ -3,10 +3,10 @@ use crate::prelude::*;
pub struct Scroll { pub struct Scroll {
inner: StrongWidget, inner: StrongWidget,
axis: Axis, axis: Axis,
amt: f32, amt: Px,
snap_end: bool, snap_end: bool,
container_len: f32, container_len: Px,
content_len: f32, content_len: Px,
} }
impl Widget for Scroll { impl Widget for Scroll {
@@ -20,7 +20,7 @@ impl Widget for Scroll {
}; };
let content = answer_len.apply_leftover(); let content = answer_len.apply_leftover();
self.container_len = container_len; self.container_len = container_len;
self.content_len = content.to_px(Px::from_f32(container_len)).to_f32(); self.content_len = content.to_px(container_len);
if self.snap_end { if self.snap_end {
self.amt = self.content_len - self.container_len; self.amt = self.content_len - self.container_len;
@@ -35,22 +35,24 @@ impl Widget for Scroll {
// the end, it moves with every length. // the end, it moves with every length.
let fixed_len = content.rel == Rel::ZERO; let fixed_len = content.rel == Rel::ZERO;
if fixed_len && self.content_len <= self.container_len && align == AxisAlign::NEG { if fixed_len && self.content_len <= self.container_len && align == AxisAlign::NEG {
painter.holds(self.axis, self.content_len..=f32::INFINITY); painter.holds(self.axis, self.content_len..=Px::MAX);
} else if fixed_len && !self.snap_end { } else if fixed_len && !self.snap_end {
let left = self.content_len - self.amt; let left = self.content_len - self.amt;
painter.holds(self.axis, f32::NEG_INFINITY..=left); painter.holds(self.axis, Px::MIN..=left);
} }
// Content shorter than the viewport has room to sit in, and where it // Content shorter than the viewport has room to sit in, and where it
// sits is this widget's own alignment -- the same property that would // sits is this widget's own alignment -- the same property that would
// have placed the whole scroll in a box longer than it. // have placed the whole scroll in a box longer than it.
let slack = (self.container_len - self.content_len).max(0.0); let slack = (self.container_len - self.content_len).max(Px::ZERO);
let anchor = slack * align.rel().to_f32(); let anchor = slack.mul(align.rel());
let mut region = UiRegion::FULL.offset(Vec2::from_axis(self.axis, anchor - self.amt, 0.0)); let offset = UiVec2::from_axis(
region.axis_mut(self.axis).end = region self.axis,
.axis(self.axis) UiScalar::from_parts(Rel::ZERO, anchor - self.amt),
.start UiScalar::ZERO,
.offset(Px::from_f32(self.content_len)); );
let mut region = UiRegion::FULL.offset(offset);
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
painter.widget_aligned(&self.inner, region, RegionAlign::NEAR); painter.widget_aligned(&self.inner, region, RegionAlign::NEAR);
// What it occupies is its box, on both axes: it clips its content to // What it occupies is its box, on both axes: it clips its content to
// that box, so it can neither take less of one nor honestly ask for // that box, so it can neither take less of one nor honestly ask for
@@ -65,22 +67,24 @@ impl Scroll {
Self { Self {
inner, inner,
axis, axis,
amt: 0.0, amt: Px::ZERO,
snap_end: true, snap_end: true,
container_len: 0.0, container_len: Px::ZERO,
content_len: 0.0, content_len: Px::ZERO,
} }
} }
pub fn update_amt(&mut self) { pub fn update_amt(&mut self) {
self.amt = self.amt.max(0.0); self.amt = self.amt.max(Px::ZERO);
let len = (self.content_len - self.container_len).max(0.0); let len = (self.content_len - self.container_len).max(Px::ZERO);
self.amt = self.amt.min(len); self.amt = self.amt.min(len);
self.snap_end = self.amt == len; self.snap_end = self.amt == len;
} }
/// Scrolled by a distance the platform measures, which is the last place
/// a wheel notch or a finger is a float.
pub fn scroll(&mut self, amt: f32) { pub fn scroll(&mut self, amt: f32) {
self.amt -= amt; self.amt -= Px::from_f32(amt);
self.update_amt(); self.update_amt();
} }
} }
+19 -20
View File
@@ -52,40 +52,39 @@ impl Widget for Span {
); );
// Whether anything is left over is a question in pixels: `rel(0.5)` // Whether anything is left over is a question in pixels: `rel(0.5)`
// beside 300 px is full at 600 and overfull at 400. The room to divide // beside 300 px is full at 600 and overfull at 400. The room to
// is `len * fixed - total.px`, and under `HOLDS_EPSILON_PX` of it is // divide is `len * fixed - total.px`, and the length where it runs
// none -- because the length where the room runs out is exactly the // out is exactly the box a parent sizing itself from this answer
// box a parent sizing itself from this answer hands back, and that box // hands back -- which is why this used to need a margin either side
// returns through the chain a few bits either way. Without the margin // of the boundary, and why it does not now: that box and this sum are
// 0.00003 px of rounding decides whether a leftover-only child is // whole counts of the same step, and both routes to it land on the
// drawn at all. The validity range is split at the moved boundary, and // same count. What the generated oracle checks is the consequence,
// exact there, since no box lands on it any more. // since which children exist at all turns on this.
let fixed = Rel::ONE - total.rel; let fixed = Rel::ONE - total.rel;
let margin = Px::from_f32(HOLDS_EPSILON_PX);
let mut shares = false; let mut shares = false;
if total.leftover > Weight::ZERO { if total.leftover > Weight::ZERO {
let current = Px::from_f32(painter.px_len(axis)); let current = painter.px_len(axis);
let holds = if fixed > Rel::ZERO { let holds = if fixed > Rel::ZERO {
// The box length at which the room reaches the margin. // The box length the fixed parts alone fill.
let enough = (total.px + margin).div(fixed); let full = total.px.div(fixed);
shares = current > enough; shares = current > full;
match shares { match shares {
true => Holds::exact(enough.to_f32().next_up()..=f32::INFINITY), true => Holds::from(full.next_up()..=Px::MAX),
false => Holds::exact(f32::NEG_INFINITY..=enough.to_f32()), false => Holds::from(Px::MIN..=full),
} }
} else if fixed < Rel::ZERO { } else if fixed < Rel::ZERO {
// The relative parts grow faster than the box does, so here // The relative parts grow faster than the box does, so here
// a shorter box is the one that leaves room. // a shorter box is the one that leaves room.
let enough = (total.px + margin).div(fixed); let full = total.px.div(fixed);
shares = current < enough; shares = current < full;
match shares { match shares {
true => Holds::exact(f32::NEG_INFINITY..=enough.to_f32().next_down()), true => Holds::from(Px::MIN..=full.next_down()),
false => Holds::exact(enough.to_f32()..=f32::INFINITY), false => Holds::from(full..=Px::MAX),
} }
} else { } else {
// The relative parts take exactly the box, whatever it is, so // The relative parts take exactly the box, whatever it is, so
// the only room is what negative pixels leave. // the only room is what negative pixels leave.
shares = total.px < margin.neg(); shares = total.px < Px::ZERO;
Holds::ANY Holds::ANY
}; };
painter.holds(axis, holds); painter.holds(axis, holds);
+7 -1
View File
@@ -276,7 +276,13 @@ impl<'a> TextEditCtx<'a> {
} }
pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) { pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) {
let pos = pos - self.text.region().top_left().to_px(size); let pos = pos
- self
.text
.region()
.top_left()
.to_px(PxVec2::from_f32(size))
.to_f32();
let prev_sel = self.text.selection; let prev_sel = self.text.selection;
let prev_hit = self.text.double_hit; let prev_hit = self.text.double_hit;
+4 -2
View File
@@ -48,13 +48,15 @@ impl TextView {
/// rather than invalidating anything. /// rather than invalidating anything.
fn render(&mut self, painter: &mut Painter) -> &RenderedText { fn render(&mut self, painter: &mut Painter) -> &RenderedText {
let width = self.attrs.wrap.then(|| painter.px_len(Axis::X)); let width = self.attrs.wrap.then(|| painter.px_len(Axis::X));
let text = painter.render_text(&mut self.buf, &self.attrs, width); // The shaper measures in floats, which is where a glyph advance comes
// from; what it answers goes back on the grid.
let text = painter.render_text(&mut self.buf, &self.attrs, width.map(Px::to_f32));
// A greedy break is the same break at every width from its longest // A greedy break is the same break at every width from its longest
// line up to the one it was made at: each line still fits, and none // line up to the one it was made at: each line still fits, and none
// could take a word that did not fit in the wider box. A line too // could take a word that did not fit in the wider box. A line too
// long to fit at all says nothing about narrower boxes. // long to fit at all says nothing about narrower boxes.
if let Some(width) = width { if let Some(width) = width {
painter.holds(Axis::X, text.size.x.min(width)..=width); painter.holds(Axis::X, Px::from_f32(text.size.x).min(width)..=width);
} }
text text
} }
+2 -5
View File
@@ -24,14 +24,11 @@ impl Widget for BranchesOnMeasurement {
let mut top = UiRegion::FULL; let mut top = UiRegion::FULL;
top.y.end = top.y.start.offset(Px::from_int(40)); top.y.end = top.y.start.offset(Px::from_int(40));
let measured = painter.widget_within(&self.probe, top).len(Axis::X); let measured = painter.widget_within(&self.probe, top).len(Axis::X);
let px = measured let px = measured.apply_leftover().to_px(painter.px_len(Axis::X));
.apply_leftover()
.to_px(Px::from_f32(painter.px_len(Axis::X)))
.to_f32();
let mut below = UiRegion::FULL; let mut below = UiRegion::FULL;
below.y.start = below.y.start.offset(Px::from_int(40)); below.y.start = below.y.start.offset(Px::from_int(40));
match px > self.threshold { match px > Px::from_f32(self.threshold) {
true => painter.widget_within(&self.wide, below), true => painter.widget_within(&self.wide, below),
false => painter.widget_within(&self.narrow, below), false => painter.widget_within(&self.narrow, below),
}; };
+12 -8
View File
@@ -30,19 +30,23 @@ fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
.unwrap_or(fallback) .unwrap_or(fallback)
} }
const SEEDS: [u64; 9] = [1, 2, 3, 5, 8, 10, 13, 86, 98]; const SEEDS: [u64; 9] = [1, 2, 3, 5, 8, 10, 13, 86, 98];
const REGION_EPSILON_PX: f32 = 0.05;
fn same_coordinate(got: f32, want: f32) -> bool { /// The same box, to a step of the grid per operation. Warm and cold reach a
(got - want).abs() <= REGION_EPSILON_PX /// coordinate by different arithmetic: a move lands on the same number now,
} /// and a length composed one way against the same length measured another can
/// land one step out. These cases apply two operations in turn, so they allow
/// two steps -- a thousandth of a pixel each, where this was a twentieth of
/// one before any of it was on a grid.
const AGREE_STEPS: i32 = 2;
fn same_region(got: Option<PixelRegion>, want: Option<PixelRegion>) -> bool { fn same_region(got: Option<PixelRegion>, want: Option<PixelRegion>) -> bool {
match (got, want) { match (got, want) {
(Some(got), Some(want)) => { (Some(got), Some(want)) => {
same_coordinate(got.top_left.x, want.top_left.x) let same = |a: Px, b: Px| (a - b).abs() <= Px::STEP.mul_int(AGREE_STEPS);
&& same_coordinate(got.top_left.y, want.top_left.y) same(got.top_left.x, want.top_left.x)
&& same_coordinate(got.bot_right.x, want.bot_right.x) && same(got.top_left.y, want.top_left.y)
&& same_coordinate(got.bot_right.y, want.bot_right.y) && same(got.bot_right.x, want.bot_right.x)
&& same(got.bot_right.y, want.bot_right.y)
} }
(None, None) => true, (None, None) => true,
_ => false, _ => false,
+22 -10
View File
@@ -255,7 +255,7 @@ struct ReadsBox {
impl Widget for ReadsBox { impl Widget for ReadsBox {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1); self.draws.set(self.draws.get() + 1);
Size::px(painter.px_size() / 4.0) Size::from_px(painter.px_size().div_int(4))
} }
} }
@@ -273,7 +273,10 @@ struct ReadsWidth {
impl Widget for ReadsWidth { impl Widget for ReadsWidth {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1); self.draws.set(self.draws.get() + 1);
Size::px((painter.px_len(Axis::X) / 4.0, 20.0).into()) Size::from_px(PxVec2::new(
painter.px_len(Axis::X).div_int(4),
Px::from_int(20),
))
} }
} }
@@ -369,8 +372,11 @@ fn a_resize_only_redraws_read_axes() {
assert_eq!(draws.get(), settled + 2, "width changes its answer"); assert_eq!(draws.get(), settled + 2, "width changes its answer");
} }
/// A window is measured onto the grid like everything else, so a resize too
/// small to reach the next step is not a resize at all -- and one that does
/// reach it is, however little of a pixel it is worth.
#[test] #[test]
fn subpixel_resize_changes_accumulate_from_the_last_layout() { fn a_resize_within_one_step_is_not_a_resize() {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
let draws = Rc::new(Cell::new(0)); let draws = Rc::new(Cell::new(0));
let leaf = ReadsWidth { let leaf = ReadsWidth {
@@ -380,30 +386,36 @@ fn subpixel_resize_changes_accumulate_from_the_last_layout() {
h.set_root(leaf); h.set_root(leaf);
let settled = draws.get(); let settled = draws.get();
for width in [400.02, 400.04, 400.05] { // All of these are 400 px to the nearest step.
h.resize((width, 200.0)); let step = Px::STEP.to_f32();
for part in [0.1, 0.2, 0.3] {
h.resize((400.0 + step * part, 200.0));
h.frame(); h.frame();
assert_eq!(draws.get(), settled); assert_eq!(draws.get(), settled);
} }
h.resize((400.06, 200.0)); h.resize((400.0 + step, 200.0));
h.frame(); h.frame();
assert_eq!(draws.get(), settled + 2); assert_eq!(draws.get(), settled + 2);
} }
/// The same for a box that changes because a sibling did: what is compared
/// is the length on the grid, and three lengths that land on one step are
/// one length.
#[test] #[test]
fn subpixel_box_changes_accumulate_from_the_last_draw() { fn a_box_change_within_one_step_is_not_a_change() {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
let (first, draws, _) = pair(&mut h, true); let (first, draws, _) = pair(&mut h, true);
let settled = draws.get(); let settled = draws.get();
for width in [100.02, 100.04, 100.05] { let step = Px::STEP.to_f32();
h.rsc[first].size.x = Len::px(width); for part in [0.1, 0.2, 0.3] {
h.rsc[first].size.x = Len::px(100.0 + step * part);
h.frame(); h.frame();
assert_eq!(draws.get(), settled); assert_eq!(draws.get(), settled);
} }
h.rsc[first].size.x = Len::px(100.06); h.rsc[first].size.x = Len::px(100.0 + step);
h.frame(); h.frame();
assert_eq!(draws.get(), settled + 1); assert_eq!(draws.get(), settled + 1);
} }
+4 -1
View File
@@ -515,9 +515,12 @@ fn diverges(node: &Node, case: Case) -> Option<String> {
for (i, (&w, &c)) in warm_ids.iter().zip(&cold_ids).enumerate() { for (i, (&w, &c)) in warm_ids.iter().zip(&cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c)); let (got, want) = (warm.region(&w), cold.region(&c));
// To one step of the grid. A move or a resize lands on the same
// number now; a length measured one way and composed another can
// still be a step apart.
let same = match (got, want) { let same = match (got, want) {
(Some(g), Some(c)) => { (Some(g), Some(c)) => {
let d = |a: f32, b: f32| (a - b).abs() <= 0.05; let d = |a: Px, b: Px| (a - b).abs() <= Px::STEP;
d(g.top_left.x, c.top_left.x) d(g.top_left.x, c.top_left.x)
&& d(g.top_left.y, c.top_left.y) && d(g.top_left.y, c.top_left.y)
&& d(g.bot_right.x, c.bot_right.x) && d(g.bot_right.x, c.bot_right.x)
+4 -1
View File
@@ -284,7 +284,10 @@ struct Wider {
impl Widget for Wider { impl Widget for Wider {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
Size { Size {
x: Len::px(painter.px_len(Axis::X) + self.extra), x: Len {
px: painter.px_len(Axis::X) + Px::from_f32(self.extra),
..Len::ZERO
},
y: Len::LEFTOVER, y: Len::LEFTOVER,
} }
} }