diff --git a/core/src/fixed.rs b/core/src/fixed.rs index 15e3a9e..3488c00 100644 --- a/core/src/fixed.rs +++ b/core/src/fixed.rs @@ -1,4 +1,4 @@ -use crate::UiNum; +use crate::{UiNum, util::Vec2}; use std::{ fmt::{Debug, Display, Formatter}, ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign}, @@ -27,7 +27,11 @@ pub struct Fixed(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<6>; +pub type Px = Fixed; + +/// 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 @@ -40,7 +44,11 @@ pub type Weight = Fixed<16>; /// +/-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<24>; +pub type Rel = Fixed; + +/// How many bits of a box a [`Rel`] keeps, beside [`PX_SHIFT`] and for the +/// same reason. +pub const REL_SHIFT: u32 = 24; impl Fixed { pub const ZERO: Self = Self(0); @@ -135,6 +143,15 @@ impl Fixed { 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. @@ -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 { return i32::MAX; } @@ -297,6 +326,91 @@ impl Debug for Fixed { } } +/// 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 { + pub x: Fixed, + pub y: Fixed, +} + +pub type PxVec2 = FixedVec2; + +impl FixedVec2 { + pub const ZERO: Self = Self::splat(Fixed::ZERO); + + pub const fn new(x: Fixed, y: Fixed) -> Self { + Self { x, y } + } + + pub const fn splat(v: Fixed) -> 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 Add for FixedVec2 { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + Self::new(self.x.add(rhs.x), self.y.add(rhs.y)) + } +} + +const impl Sub for FixedVec2 { + type Output = Self; + + fn sub(self, rhs: Self) -> Self { + Self::new(self.x.sub(rhs.x), self.y.sub(rhs.y)) + } +} + +const impl AddAssign for FixedVec2 { + fn add_assign(&mut self, rhs: Self) { + *self = Add::add(*self, rhs); + } +} + +const impl SubAssign for FixedVec2 { + fn sub_assign(&mut self, rhs: Self) { + *self = Sub::sub(*self, rhs); + } +} + +impl Debug for FixedVec2 { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "({}, {})", self.x, self.y) + } +} + +impl Display for FixedVec2 { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "({}, {})", self.x, self.y) + } +} + #[cfg(test)] mod tests { use super::*; @@ -332,10 +446,10 @@ mod tests { #[test] fn halves_round_away_from_zero_either_side() { - // A sixty-fourth 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); - assert_eq!(Px::ONE * one_and_a_half, Px::from_raw(2)); - assert_eq!(Px::ONE.neg() * one_and_a_half, Px::from_raw(-2)); + // 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] @@ -360,10 +474,8 @@ mod tests { // 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)); - assert_eq!( - Px::from_raw(21).to_scale::<24>().to_scale::<6>(), - Px::from_raw(21) - ); + let coarse = Fixed::<6>::from_raw(21); + assert_eq!(coarse.to_scale::<24>().to_scale::<6>(), coarse); } #[test] diff --git a/core/src/layout_diagnostics.rs b/core/src/layout_diagnostics.rs index 7a10bd9..c86ecd8 100644 --- a/core/src/layout_diagnostics.rs +++ b/core/src/layout_diagnostics.rs @@ -15,7 +15,7 @@ //! reuse, size, placement, and text events for one suspicious widget. The //! 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::{ cell::RefCell, collections::{HashMap, HashSet}, @@ -258,7 +258,7 @@ pub enum TraceEvent { id: WidgetId, parent: Option, region: UiRegion, - pixel_size: Vec2, + pixel_size: PxVec2, region_node: bool, }, Reuse { @@ -354,7 +354,7 @@ pub(crate) fn draw_request( id: WidgetId, parent: Option, region: UiRegion, - pixel_size: Vec2, + pixel_size: PxVec2, region_node: bool, ) { trace( diff --git a/core/src/orientation/axis.rs b/core/src/orientation/axis.rs index 1053b4e..17b5cc9 100644 --- a/core/src/orientation/axis.rs +++ b/core/src/orientation/axis.rs @@ -1,4 +1,5 @@ use super::*; +use crate::{Fixed, FixedVec2}; #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum Axis { @@ -40,6 +41,29 @@ pub enum Sign { Pos, } +impl FixedVec2 { + pub const fn axis(&self, axis: Axis) -> Fixed { + match axis { + Axis::X => self.x, + Axis::Y => self.y, + } + } + + pub const fn axis_mut(&mut self, axis: Axis) -> &mut Fixed { + match axis { + Axis::X => &mut self.x, + Axis::Y => &mut self.y, + } + } + + pub const fn from_axis(axis: Axis, aligned: Fixed, ortho: Fixed) -> Self { + match axis { + Axis::X => Self::new(aligned, ortho), + Axis::Y => Self::new(ortho, aligned), + } + } +} + impl Vec2 { pub fn axis(&self, axis: Axis) -> f32 { match axis { diff --git a/core/src/orientation/len.rs b/core/src/orientation/len.rs index 36a68a8..72b1b5e 100644 --- a/core/src/orientation/len.rs +++ b/core/src/orientation/len.rs @@ -1,5 +1,5 @@ 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)] pub struct Size { @@ -49,10 +49,22 @@ impl Size { 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 { + Self::from_px(PxVec2::from_f32(v)) + } + + pub const fn from_px(v: PxVec2) -> Self { Self { - x: Len::px(v.x), - y: Len::px(v.y), + x: Len { + px: v.x, + ..Len::ZERO + }, + y: Len { + px: v.y, + ..Len::ZERO + }, } } diff --git a/core/src/orientation/pos.rs b/core/src/orientation/pos.rs index 8a4a7ae..53df60d 100644 --- a/core/src/orientation/pos.rs +++ b/core/src/orientation/pos.rs @@ -1,7 +1,7 @@ use std::{fmt::Display, marker::Destruct}; use super::*; -use crate::{Px, Rel, UiNum, util::impl_op}; +use crate::{Px, PxVec2, Rel, UiNum, util::impl_op}; #[repr(C)] #[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 - /// outside layout -- a pointer position, or something being drawn. - pub fn to_px(&self, size: Vec2) -> Vec2 { - Vec2 { - x: self.x.to_px(Px::from_f32(size.x)).to_f32(), - y: self.y.to_px(Px::from_f32(size.y)).to_f32(), - } + /// 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); @@ -344,10 +341,10 @@ impl UiRegion { self } - pub fn to_px(&self, size: Vec2) -> PixelRegion { + pub fn to_px(&self, size: PxVec2) -> PixelRegion { PixelRegion { - top_left: self.top_left().get_rel() * size + self.top_left().get_px(), - bot_right: self.bot_right().get_rel() * size + self.bot_right().get_px(), + top_left: self.top_left().to_px(size), + 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 top_left: Vec2, - pub bot_right: Vec2, + pub top_left: PxVec2, + pub bot_right: PxVec2, } 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.bot_right.x && pos.y >= self.top_left.y && pos.y <= self.bot_right.y } - pub fn size(&self) -> Vec2 { + pub fn size(&self) -> PxVec2 { self.bot_right - self.top_left } } diff --git a/core/src/render/mod.rs b/core/src/render/mod.rs index 834cc8c..09ae52b 100644 --- a/core/src/render/mod.rs +++ b/core/src/render/mod.rs @@ -23,7 +23,14 @@ pub use primitive::*; const PRELUDE: &str = include_str!("./shader/prelude.wgsl"); 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 { diff --git a/core/src/render/shader/prelude.wgsl b/core/src/render/shader/prelude.wgsl index 269bc2a..413eb47 100644 --- a/core/src/render/shader/prelude.wgsl +++ b/core/src/render/shader/prelude.wgsl @@ -26,11 +26,9 @@ struct MoveOffset { parent: u32, } -// What `iris_core` stores: a whole count of a sixty-fourth of a pixel, and of -// a `1 / 2^24` of a box. Both steps are powers of two, so decoding one 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; +// `PX_STEP` and `REL_STEP` are prepended from `iris_core`'s own constants: +// what it stores is a whole count of each, both powers of two, so decoding +// is exact and the number here is the number the CPU decided. // 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 diff --git a/core/src/ui/active.rs b/core/src/ui/active.rs index 29257a1..9da0a0b 100644 --- a/core/src/ui/active.rs +++ b/core/src/ui/active.rs @@ -58,7 +58,7 @@ pub struct ActiveData { impl ActiveData { /// 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) } } diff --git a/core/src/ui/holds.rs b/core/src/ui/holds.rs index ff0ec0b..bafeea2 100644 --- a/core/src/ui/holds.rs +++ b/core/src/ui/holds.rs @@ -1,4 +1,4 @@ -use crate::{Rel, UiScalar}; +use crate::{Px, REL_SHIFT, UiScalar, fixed::div_toward, fixed::narrow}; use std::ops::RangeInclusive; /// 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 /// otherwise, and a parent holds for whatever keeps every child it asked /// 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 lo: f32, - pub hi: f32, + pub lo: Px, + 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 { pub const ANY: Self = Self { - lo: f32::NEG_INFINITY, - hi: f32::INFINITY, + lo: Px::MIN, + hi: Px::MAX, }; - pub const fn at(len: f32) -> Self { - Self::tolerant(len, len) + pub const fn at(len: Px) -> Self { + Self { lo: len, hi: len } } - const fn tolerant(lo: f32, hi: f32) -> Self { - Self { - lo: lo - HOLDS_EPSILON_PX, - hi: hi + HOLDS_EPSILON_PX, - } + pub const fn contains(&self, len: Px) -> bool { + len.raw() >= self.lo.raw() && len.raw() <= self.hi.raw() } - /// A range whose endpoints are exact, for a widget decision with a hard - /// boundary rather than an accumulated coordinate-rounding difference. - pub fn exact(range: RangeInclusive) -> 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 { + pub const fn and(self, other: Self) -> Self { Self { lo: self.lo.max(other.lo), 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 /// 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. - 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; } - let (rel, px) = (len.rel.to_f32(), len.px.to_f32()); - let a = (self.lo - px) / rel; - let b = (self.hi - px) / rel; + // Three half steps either side -- one for the rounding on the way + // in, two for the difference between a length composed down the + // 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 { - lo: a.min(b), - hi: a.max(b), + lo: Px::from_raw(narrow(lo)), + hi: Px::from_raw(narrow(hi)), } } } -impl From> for Holds { - fn from(range: RangeInclusive) -> Self { - Self::tolerant(*range.start(), *range.end()) +impl From> for Holds { + fn from(range: RangeInclusive) -> Self { + Self { + lo: *range.start(), + hi: *range.end(), + } } } #[cfg(test)] mod tests { use super::*; + use crate::Rel; #[test] fn through_reverses_a_range_for_a_negative_fraction() { - let holds = Holds::from(20.0..=40.0).through(UiScalar::new(-0.5, 10.0)); - assert!((holds.lo - -60.1).abs() < 0.001); - assert!((holds.hi - -19.9).abs() < 0.001); + // `10 - box / 2` is between 20 and 40 for boxes from -60 to -20. + let part = UiScalar::from_parts(Rel::from_f32(-0.5), Px::from_int(10)); + 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] - fn an_exact_open_boundary_does_not_admit_the_boundary() { - let boundary = 10.0_f32; - let above = Holds::exact(boundary.next_up()..=f32::INFINITY); + fn a_boundary_the_next_step_along_does_not_admit_it() { + let boundary = Px::from_int(10); + let above = Holds::from(boundary.next_up()..=Px::MAX); assert!(!above.contains(boundary)); assert!(above.contains(boundary.next_up())); } diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index 69fd38e..95cf238 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -1,7 +1,7 @@ #[cfg(feature = "layout-diagnostics")] use crate::layout_diagnostics::{self as diag, Counter}; 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, WidgetId, Widgets, render::{ @@ -9,7 +9,6 @@ use crate::{ PrimitiveKind, TexturePrimitive, }, ui::render_state::DrawInfo, - util::Vec2, }; const AXES: [Axis; 2] = [Axis::X, Axis::Y]; @@ -28,7 +27,7 @@ pub struct Painter<'a> { /// is the one recorded as its offer. pub(super) offered: Vec, /// 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 /// the ones a cold layout asks and their answers the ones to keep. 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. - fn px_within_offer(&self, local: UiRegion) -> Vec2 { + fn px_within_offer(&self, local: UiRegion) -> PxVec2 { let size = local.size(); - Vec2::new( - size.x.to_px(Px::from_f32(self.offered_px.x)).to_f32(), - size.y.to_px(Px::from_f32(self.offered_px.y)).to_f32(), + PxVec2::new( + size.x.to_px(self.offered_px.x), + 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 /// 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); for (own, len) in self.own.iter_mut().zip([px.x, px.y]) { if *own == Holds::ANY { @@ -375,7 +374,7 @@ impl<'a> Painter<'a> { /// One axis of this widget's 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) -> f32 { + pub fn px_len(&mut self, axis: Axis) -> Px { let len = self.state.px_of(self.move_idx, self.region).axis(axis); let own = &mut self.own[axis as usize]; if *own == Holds::ANY { diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index afde7a5..c21c321 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -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::{ ActiveData, Axis, DrawLayers, Holds, IdLike, Len, MaskIdx, MoveIdx, Moves, Painter, - PixelRegion, RegionAlign, Rel, Size, StrongWidget, UiRegion, UiRsc, UiScalar, UiSpan, Weight, - WidgetId, Widgets, + PixelRegion, PxVec2, RegionAlign, Rel, Size, StrongWidget, UiRegion, UiRsc, UiScalar, UiSpan, + Weight, WidgetId, Widgets, 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 /// that box in pixels. pub offer: UiRegion, - pub offered_px: Vec2, + pub offered_px: PxVec2, /// A container's answer for where the widget sits. `None` uses the /// widget's own property. pub align: Option, @@ -32,7 +32,7 @@ pub(super) struct DrawInfo { pub struct UiRenderState { pub active: HashMap, pub layers: DrawLayers, - pub(super) output_size: Vec2, + pub(super) output_size: PxVec2, old_root: Option, /// The slot every chain bottoms out in, holding the output as a box. @@ -58,7 +58,7 @@ impl UiRenderState { Self { active: Default::default(), layers: Default::default(), - output_size: Vec2::ZERO, + output_size: PxVec2::ZERO, old_root: None, slots: Default::default(), answer_invalid: Default::default(), @@ -75,8 +75,14 @@ impl UiRenderState { /// downstream has to know the output's size to resolve a position. fn write_root(&mut self) { let region = UiRegion::new( - UiSpan::new(UiScalar::ZERO, UiScalar::px(self.output_size.x)), - UiSpan::new(UiScalar::ZERO, UiScalar::px(self.output_size.y)), + UiSpan::new( + 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 { 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) { - let size = size.into(); + let size = PxVec2::from_f32(size.into()); if size == self.output_size { return; } @@ -108,7 +116,7 @@ impl UiRenderState { } } - pub fn output_size(&self) -> Vec2 { + pub fn output_size(&self) -> PxVec2 { self.output_size } @@ -447,7 +455,7 @@ impl UiRenderState { } /// 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 .resolve(slot, region) .size() @@ -460,7 +468,7 @@ impl UiRenderState { pub(super) fn retained_size( &self, id: WidgetId, - px: Vec2, + px: PxVec2, parent_move: MoveIdx, widgets: &Widgets, ) -> Option<(Size, [Holds; 2])> { @@ -1003,8 +1011,10 @@ impl UiRenderState { } } -fn same_px(a: Vec2, b: Vec2) -> bool { - Holds::at(a.x).contains(b.x) && Holds::at(a.y).contains(b.y) +/// The same box is the same number of steps, both of these being lengths on +/// 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 { @@ -1045,7 +1055,13 @@ impl RegionRemap { fn apply_scalar(self, scalar: UiScalar, from: UiSpan, to: UiSpan) -> UiScalar { 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; } let fraction = (scalar.rel - from.start.rel) / extent; diff --git a/src/default/attr.rs b/src/default/attr.rs index ff4e313..33a3d8d 100644 --- a/src/default/attr.rs +++ b/src/default/attr.rs @@ -15,8 +15,10 @@ where let region = ctx.data.render.window_region(&id).unwrap(); let id_pos = region.top_left; let container_pos = ctx.data.render.window_region(&container).unwrap().top_left; - let pos = ctx.data.pos + container_pos - id_pos; - let size = region.size(); + // The pointer arrives from the platform in floats; everything + // 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( rsc, ctx.data.render, @@ -70,8 +72,8 @@ fn select( if let Some(region) = render.window_region(&id) { state.window.set_ime_allowed(true); state.window.set_ime_cursor_area( - LogicalPosition::::from(region.top_left.tuple()), - LogicalSize::::from(region.size().tuple()), + LogicalPosition::::from(region.top_left.to_f32().tuple()), + LogicalSize::::from(region.size().to_f32().tuple()), ); } state.focus = Some(id); diff --git a/src/default/sense.rs b/src/default/sense.rs index 14d5b3a..b4a6010 100644 --- a/src/default/sense.rs +++ b/src/default/sense.rs @@ -198,7 +198,7 @@ impl SensorUi for UiRenderState { let Some(region) = region_of(id) else { continue; }; - if !cursor.exists || !region.contains(cursor.pos) { + if !cursor.exists || !region.contains(PxVec2::from_f32(cursor.pos)) { continue; } hovered.now.push(id); @@ -249,8 +249,8 @@ fn deliver( region: PixelRegion, ) -> bool { let data = CursorData { - pos: cursor.pos - region.top_left, - size: region.bot_right - region.top_left, + pos: cursor.pos - region.top_left.to_f32(), + size: region.size().to_f32(), scroll_delta: cursor.scroll_delta, hover, cursor: cursor.clone(), diff --git a/src/harness.rs b/src/harness.rs index 4b7f569..281514d 100644 --- a/src/harness.rs +++ b/src/harness.rs @@ -29,8 +29,14 @@ macro_rules! assert_corners { assert_eq!( $harness.region(&$id).expect("widget drew nothing"), $crate::core::PixelRegion { - top_left: $crate::core::util::Vec2::new($x0 as f32, $y0 as f32), - bot_right: $crate::core::util::Vec2::new($x1 as f32, $y1 as f32), + top_left: $crate::core::PxVec2::new( + $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 { - self.render.output_size() + self.render.output_size().to_f32() } pub fn resize(&mut self, size: impl Into) { diff --git a/src/random.rs b/src/random.rs index a1895dc..d287547 100644 --- a/src/random.rs +++ b/src/random.rs @@ -113,14 +113,11 @@ impl Widget for Branch { let mut top = UiRegion::FULL; top.y.end = top.y.start.offset(Px::from_int(40)); let measured = painter.widget_within(&self.probe, top).len(Axis::X); - let px = measured - .apply_leftover() - .to_px(Px::from_f32(painter.px_len(Axis::X))) - .to_f32(); + let px = measured.apply_leftover().to_px(painter.px_len(Axis::X)); let mut below = UiRegion::FULL; 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), false => painter.widget_within(&self.narrow, below), }; diff --git a/src/widget/position/scroll.rs b/src/widget/position/scroll.rs index 9f06180..4dd2c10 100644 --- a/src/widget/position/scroll.rs +++ b/src/widget/position/scroll.rs @@ -3,10 +3,10 @@ use crate::prelude::*; pub struct Scroll { inner: StrongWidget, axis: Axis, - amt: f32, + amt: Px, snap_end: bool, - container_len: f32, - content_len: f32, + container_len: Px, + content_len: Px, } impl Widget for Scroll { @@ -20,7 +20,7 @@ impl Widget for Scroll { }; let content = answer_len.apply_leftover(); 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 { self.amt = self.content_len - self.container_len; @@ -35,22 +35,24 @@ impl Widget for Scroll { // the end, it moves with every length. let fixed_len = content.rel == Rel::ZERO; 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 { 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 // sits is this widget's own alignment -- the same property that would // have placed the whole scroll in a box longer than it. - let slack = (self.container_len - self.content_len).max(0.0); - let anchor = slack * align.rel().to_f32(); - let mut region = UiRegion::FULL.offset(Vec2::from_axis(self.axis, anchor - self.amt, 0.0)); - region.axis_mut(self.axis).end = region - .axis(self.axis) - .start - .offset(Px::from_f32(self.content_len)); + let slack = (self.container_len - self.content_len).max(Px::ZERO); + let anchor = slack.mul(align.rel()); + let offset = UiVec2::from_axis( + self.axis, + UiScalar::from_parts(Rel::ZERO, anchor - self.amt), + UiScalar::ZERO, + ); + 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); // 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 @@ -65,22 +67,24 @@ impl Scroll { Self { inner, axis, - amt: 0.0, + amt: Px::ZERO, snap_end: true, - container_len: 0.0, - content_len: 0.0, + container_len: Px::ZERO, + content_len: Px::ZERO, } } pub fn update_amt(&mut self) { - self.amt = self.amt.max(0.0); - let len = (self.content_len - self.container_len).max(0.0); + self.amt = self.amt.max(Px::ZERO); + let len = (self.content_len - self.container_len).max(Px::ZERO); self.amt = self.amt.min(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) { - self.amt -= amt; + self.amt -= Px::from_f32(amt); self.update_amt(); } } diff --git a/src/widget/position/span.rs b/src/widget/position/span.rs index 8e51fcf..6bd4753 100644 --- a/src/widget/position/span.rs +++ b/src/widget/position/span.rs @@ -52,40 +52,39 @@ impl Widget for Span { ); // 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 - // is `len * fixed - total.px`, and under `HOLDS_EPSILON_PX` of it is - // none -- because the length where the room runs out is exactly the - // box a parent sizing itself from this answer hands back, and that box - // returns through the chain a few bits either way. Without the margin - // 0.00003 px of rounding decides whether a leftover-only child is - // drawn at all. The validity range is split at the moved boundary, and - // exact there, since no box lands on it any more. + // beside 300 px is full at 600 and overfull at 400. The room to + // divide is `len * fixed - total.px`, and the length where it runs + // out is exactly the box a parent sizing itself from this answer + // hands back -- which is why this used to need a margin either side + // of the boundary, and why it does not now: that box and this sum are + // whole counts of the same step, and both routes to it land on the + // same count. What the generated oracle checks is the consequence, + // since which children exist at all turns on this. let fixed = Rel::ONE - total.rel; - let margin = Px::from_f32(HOLDS_EPSILON_PX); let mut shares = false; 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 { - // The box length at which the room reaches the margin. - let enough = (total.px + margin).div(fixed); - shares = current > enough; + // The box length the fixed parts alone fill. + let full = total.px.div(fixed); + shares = current > full; match shares { - true => Holds::exact(enough.to_f32().next_up()..=f32::INFINITY), - false => Holds::exact(f32::NEG_INFINITY..=enough.to_f32()), + true => Holds::from(full.next_up()..=Px::MAX), + false => Holds::from(Px::MIN..=full), } } else if fixed < Rel::ZERO { // The relative parts grow faster than the box does, so here // a shorter box is the one that leaves room. - let enough = (total.px + margin).div(fixed); - shares = current < enough; + let full = total.px.div(fixed); + shares = current < full; match shares { - true => Holds::exact(f32::NEG_INFINITY..=enough.to_f32().next_down()), - false => Holds::exact(enough.to_f32()..=f32::INFINITY), + true => Holds::from(Px::MIN..=full.next_down()), + false => Holds::from(full..=Px::MAX), } } else { // The relative parts take exactly the box, whatever it is, so // the only room is what negative pixels leave. - shares = total.px < margin.neg(); + shares = total.px < Px::ZERO; Holds::ANY }; painter.holds(axis, holds); diff --git a/src/widget/text/edit.rs b/src/widget/text/edit.rs index c7bd195..6126046 100644 --- a/src/widget/text/edit.rs +++ b/src/widget/text/edit.rs @@ -276,7 +276,13 @@ impl<'a> TextEditCtx<'a> { } 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_hit = self.text.double_hit; diff --git a/src/widget/text/mod.rs b/src/widget/text/mod.rs index 764d12e..303d713 100644 --- a/src/widget/text/mod.rs +++ b/src/widget/text/mod.rs @@ -48,13 +48,15 @@ impl TextView { /// rather than invalidating anything. fn render(&mut self, painter: &mut Painter) -> &RenderedText { 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 // 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 // long to fit at all says nothing about narrower boxes. 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 } diff --git a/tests/determinism.rs b/tests/determinism.rs index b2d6b9a..721da4e 100644 --- a/tests/determinism.rs +++ b/tests/determinism.rs @@ -24,14 +24,11 @@ impl Widget for BranchesOnMeasurement { let mut top = UiRegion::FULL; top.y.end = top.y.start.offset(Px::from_int(40)); let measured = painter.widget_within(&self.probe, top).len(Axis::X); - let px = measured - .apply_leftover() - .to_px(Px::from_f32(painter.px_len(Axis::X))) - .to_f32(); + let px = measured.apply_leftover().to_px(painter.px_len(Axis::X)); let mut below = UiRegion::FULL; 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), false => painter.widget_within(&self.narrow, below), }; diff --git a/tests/generated.rs b/tests/generated.rs index 26b1358..b4e9fba 100644 --- a/tests/generated.rs +++ b/tests/generated.rs @@ -30,19 +30,23 @@ fn env(name: &str, fallback: T) -> T { .unwrap_or(fallback) } 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 { - (got - want).abs() <= REGION_EPSILON_PX -} +/// The same box, to a step of the grid per operation. Warm and cold reach a +/// 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, want: Option) -> bool { match (got, want) { (Some(got), Some(want)) => { - same_coordinate(got.top_left.x, want.top_left.x) - && same_coordinate(got.top_left.y, want.top_left.y) - && same_coordinate(got.bot_right.x, want.bot_right.x) - && same_coordinate(got.bot_right.y, want.bot_right.y) + let same = |a: Px, b: Px| (a - b).abs() <= Px::STEP.mul_int(AGREE_STEPS); + same(got.top_left.x, want.top_left.x) + && same(got.top_left.y, want.top_left.y) + && same(got.bot_right.x, want.bot_right.x) + && same(got.bot_right.y, want.bot_right.y) } (None, None) => true, _ => false, diff --git a/tests/retained.rs b/tests/retained.rs index 42eabb1..4b517b6 100644 --- a/tests/retained.rs +++ b/tests/retained.rs @@ -255,7 +255,7 @@ struct ReadsBox { impl Widget for ReadsBox { fn draw(&mut self, painter: &mut Painter) -> Size { 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 { fn draw(&mut self, painter: &mut Painter) -> Size { 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"); } +/// 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] -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 draws = Rc::new(Cell::new(0)); let leaf = ReadsWidth { @@ -380,30 +386,36 @@ fn subpixel_resize_changes_accumulate_from_the_last_layout() { h.set_root(leaf); let settled = draws.get(); - for width in [400.02, 400.04, 400.05] { - h.resize((width, 200.0)); + // All of these are 400 px to the nearest step. + 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(); assert_eq!(draws.get(), settled); } - h.resize((400.06, 200.0)); + h.resize((400.0 + step, 200.0)); h.frame(); 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] -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 (first, draws, _) = pair(&mut h, true); let settled = draws.get(); - for width in [100.02, 100.04, 100.05] { - h.rsc[first].size.x = Len::px(width); + let step = Px::STEP.to_f32(); + for part in [0.1, 0.2, 0.3] { + h.rsc[first].size.x = Len::px(100.0 + step * part); h.frame(); 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(); assert_eq!(draws.get(), settled + 1); } diff --git a/tests/shrink.rs b/tests/shrink.rs index 5eb6612..321068b 100644 --- a/tests/shrink.rs +++ b/tests/shrink.rs @@ -515,9 +515,12 @@ fn diverges(node: &Node, case: Case) -> Option { for (i, (&w, &c)) in warm_ids.iter().zip(&cold_ids).enumerate() { 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) { (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.y, c.top_left.y) && d(g.bot_right.x, c.bot_right.x) diff --git a/tests/unsettled.rs b/tests/unsettled.rs index 98536ae..e732381 100644 --- a/tests/unsettled.rs +++ b/tests/unsettled.rs @@ -284,7 +284,10 @@ struct Wider { impl Widget for Wider { fn draw(&mut self, painter: &mut Painter) -> 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, } }