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:
1 parent
bd6de71a55
commit
39e4ca20e6
24 files changed
+420
-194
No files matched your search
+124
-12
@@ -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<const SHIFT: u32>(i32);
|
||||
/// A length or a coordinate in pixels, to a sixty-fourth. Finer than anything
|
||||
/// a display can show, and exact in `f32` up to 262,144 px, which is what lets
|
||||
/// the same number reach the GPU.
|
||||
pub type Px = Fixed<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
|
||||
/// 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<REL_SHIFT>;
|
||||
|
||||
/// How many bits of a box a [`Rel`] keeps, beside [`PX_SHIFT`] and for the
|
||||
/// same reason.
|
||||
pub const REL_SHIFT: u32 = 24;
|
||||
|
||||
impl<const SHIFT: u32> Fixed<SHIFT> {
|
||||
pub const ZERO: Self = Self(0);
|
||||
@@ -135,6 +143,15 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
|
||||
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<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)]
|
||||
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]
|
||||
|
||||
@@ -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<WidgetId>,
|
||||
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<WidgetId>,
|
||||
region: UiRegion,
|
||||
pixel_size: Vec2,
|
||||
pixel_size: PxVec2,
|
||||
region_node: bool,
|
||||
) {
|
||||
trace(
|
||||
|
||||
@@ -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<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 {
|
||||
pub fn axis(&self, axis: Axis) -> f32 {
|
||||
match axis {
|
||||
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+13
-16
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
+74
-48
@@ -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<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 {
|
||||
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<RangeInclusive<f32>> for Holds {
|
||||
fn from(range: RangeInclusive<f32>) -> Self {
|
||||
Self::tolerant(*range.start(), *range.end())
|
||||
impl From<RangeInclusive<Px>> for Holds {
|
||||
fn from(range: RangeInclusive<Px>) -> 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()));
|
||||
}
|
||||
|
||||
@@ -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<WidgetId>,
|
||||
/// 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 {
|
||||
|
||||
+30
-14
@@ -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<RegionAlign>,
|
||||
@@ -32,7 +32,7 @@ pub(super) struct DrawInfo {
|
||||
pub struct UiRenderState {
|
||||
pub active: HashMap<WidgetId, ActiveData>,
|
||||
pub layers: DrawLayers,
|
||||
pub(super) output_size: Vec2,
|
||||
pub(super) output_size: PxVec2,
|
||||
|
||||
old_root: Option<WidgetId>,
|
||||
/// 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<Vec2>) {
|
||||
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;
|
||||
|
||||
Reference in new issue
Block a user