diff --git a/core/src/fixed.rs b/core/src/fixed.rs index c1df01f..637fd61 100644 --- a/core/src/fixed.rs +++ b/core/src/fixed.rs @@ -38,6 +38,9 @@ pub type Rel = Fixed<24>; impl Fixed { pub const ZERO: Self = Self(0); pub const ONE: Self = Self::one(); + /// The gap between neighbouring values, which is also how far apart two + /// numbers can be and still mean the same place. + pub const STEP: Self = Self(1); /// Also what stands in for an unbounded end, since arithmetic saturates /// here rather than wrapping past it. pub const MIN: Self = Self(i32::MIN); @@ -65,14 +68,25 @@ impl Fixed { /// Rounds to the nearest step, and saturates rather than wrapping. A NaN /// has no nearest step and becomes zero, which is a caller's mistake /// rather than a value worth carrying. - pub fn from_f32(v: f32) -> Self { + /// + /// Half-away is written out rather than called through `f32::round`, + /// which is not `const`: a layout constant has to stay a constant. + pub const fn from_f32(v: f32) -> Self { debug_assert!(!v.is_nan(), "a NaN has no place on the grid"); - // Float-to-int casts saturate and send NaN to zero, which is the - // behaviour wanted at both ends. - Self((v * Self::one().0 as f32).round() as i32) + let scaled = v * Self::one().0 as f32; + // Above 2^23 an `f32` has no fractional part left to round, and + // adding a half there rounds the number itself up instead. The cast + // saturates at both ends and sends NaN to zero, which is the + // behaviour wanted at both. + const WHOLE: f32 = (1 << 23) as f32; + Self(match (scaled >= WHOLE, scaled <= -WHOLE, scaled < 0.0) { + (true, _, _) | (_, true, _) => scaled as i32, + (_, _, true) => (scaled - 0.5) as i32, + _ => (scaled + 0.5) as i32, + }) } - pub fn to_f32(self) -> f32 { + pub const fn to_f32(self) -> f32 { self.0 as f32 / Self::one().0 as f32 } diff --git a/core/src/layout_diagnostics.rs b/core/src/layout_diagnostics.rs index 4aef559..7a10bd9 100644 --- a/core/src/layout_diagnostics.rs +++ b/core/src/layout_diagnostics.rs @@ -244,6 +244,7 @@ pub enum ReuseOutcome { Moved, Dirty, WrongParent, + WrongLayer, Remapped, Outside, Undrawn, diff --git a/core/src/orientation/align.rs b/core/src/orientation/align.rs index 7bc309d..e47d372 100644 --- a/core/src/orientation/align.rs +++ b/core/src/orientation/align.rs @@ -1,4 +1,4 @@ -use crate::vec2; +use crate::{Px, Rel, vec2}; use super::*; @@ -175,14 +175,13 @@ impl Vec2 { impl UiScalar { pub const fn align(&self, align: AxisAlign) -> UiSpan { - let rel = align.rel(); - let mut start = UiScalar::rel(rel); - start.px -= self.px * rel; - start.rel -= self.rel * rel; - let mut end = UiScalar::rel(rel); - end.px += self.px * (1.0 - rel); - end.rel += self.rel * (1.0 - rel); - UiSpan { start, end } + let rel = Rel::from_f32(align.rel()); + let rest = Rel::ONE.sub(rel); + let at = UiScalar::from_parts(rel, Px::ZERO); + UiSpan { + start: UiScalar::from_parts(at.rel.sub(self.rel.mul(rel)), at.px.sub(self.px.mul(rel))), + end: UiScalar::from_parts(at.rel.add(self.rel.mul(rest)), at.px.add(self.px.mul(rest))), + } } } diff --git a/core/src/orientation/len.rs b/core/src/orientation/len.rs index 757c7f6..e77bbb0 100644 --- a/core/src/orientation/len.rs +++ b/core/src/orientation/len.rs @@ -109,10 +109,8 @@ impl Len { }; pub fn apply_leftover(&self) -> UiScalar { - UiScalar { - rel: self.rel + if self.leftover > 0.0 { 1.0 } else { 0.0 }, - px: self.px, - } + let share = if self.leftover > 0.0 { 1.0 } else { 0.0 }; + UiScalar::new(self.rel + share, self.px) } pub fn px(px: impl UiNum) -> Self { diff --git a/core/src/orientation/pos.rs b/core/src/orientation/pos.rs index d271ad9..8a97ad6 100644 --- a/core/src/orientation/pos.rs +++ b/core/src/orientation/pos.rs @@ -1,10 +1,7 @@ -use std::{fmt::Display, hash::Hash, marker::Destruct}; +use std::{fmt::Display, marker::Destruct}; use super::*; -use crate::{ - UiNum, - util::{LerpUtil, impl_op}, -}; +use crate::{Px, Rel, UiNum, util::impl_op}; #[repr(C)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, bytemuck::Zeroable, Default)] @@ -70,10 +67,12 @@ impl UiVec2 { } } - pub fn to_px(&self, rel: Vec2) -> Vec2 { + /// 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(rel.x), - y: self.y.to_px(rel.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(), } } @@ -93,18 +92,11 @@ impl UiVec2 { } pub fn get_px(&self) -> Vec2 { - (self.x.px, self.y.px).into() + (self.x.px.to_f32(), self.y.px.to_f32()).into() } pub fn get_rel(&self) -> Vec2 { - (self.x.rel, self.y.rel).into() - } - - pub fn abs_mut(&mut self) -> Vec2View<'_> { - Vec2View { - x: &mut self.x.px, - y: &mut self.y.px, - } + (self.x.rel.to_f32(), self.y.rel.to_f32()).into() } } @@ -114,8 +106,8 @@ impl Display for UiVec2 { } } -impl_op!(UiVec2 Add add; x y); -impl_op!(UiVec2 Sub sub; x y); +impl_op!(same UiVec2 Add add; x y); +impl_op!(same UiVec2 Sub sub; x y); const impl From for UiVec2 { fn from(px: Vec2) -> Self { @@ -132,46 +124,53 @@ where } } +/// A position along one axis, as a fraction of the box it sits in plus an +/// offset: `rel * len + px`. Both parts are fixed point, so composing one +/// through a chain of boxes rounds only where it multiplies and lands on the +/// same number as any other route to the same place. #[repr(C)] -#[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, Default, bytemuck::Zeroable)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, Default, bytemuck::Zeroable)] pub struct UiScalar { - pub rel: f32, - pub px: f32, + pub rel: Rel, + pub px: Px, } -impl Eq for UiScalar {} -impl Hash for UiScalar { - fn hash(&self, state: &mut H) { - state.write_u32(self.rel.to_bits()); - state.write_u32(self.px.to_bits()); - } -} - -impl_op!(UiScalar Add add; rel px); -impl_op!(UiScalar Sub sub; rel px); +impl_op!(same UiScalar Add add; rel px); +impl_op!(same UiScalar Sub sub; rel px); impl UiScalar { - pub const ZERO: Self = Self { rel: 0.0, px: 0.0 }; - pub const FULL: Self = Self { rel: 1.0, px: 0.0 }; + pub const ZERO: Self = Self { + rel: Rel::ZERO, + px: Px::ZERO, + }; + pub const FULL: Self = Self { + rel: Rel::ONE, + px: Px::ZERO, + }; pub const fn new(rel: f32, px: f32) -> Self { + Self::from_parts(Rel::from_f32(rel), Px::from_f32(px)) + } + + /// From parts already on the grid, rather than numbers to be put on it. + pub const fn from_parts(rel: Rel, px: Px) -> Self { Self { rel, px } } pub const fn rel(rel: f32) -> Self { - Self { rel, px: 0.0 } + Self::from_parts(Rel::from_f32(rel), Px::ZERO) } pub const fn px(px: f32) -> Self { - Self { rel: 0.0, px } + Self::from_parts(Rel::ZERO, Px::from_f32(px)) } pub const fn rel_min() -> Self { - Self::new(0.0, 0.0) + Self::ZERO } pub const fn rel_max() -> Self { - Self::new(1.0, 0.0) + Self::FULL } pub const fn max(&self, other: Self) -> Self { @@ -191,23 +190,22 @@ impl UiScalar { /// Both channels by the same factor, which is what a fraction of a /// length means when the length is part pixels and part a share. pub const fn scale(&self, by: f32) -> Self { + let by = Rel::from_f32(by); Self { - rel: self.rel * by, - px: self.px * by, + rel: self.rel.mul(by), + px: self.px.mul(by), } } pub const fn offset(mut self, amt: f32) -> Self { - self.px += amt; + self.px = self.px.add(Px::from_f32(amt)); self } pub const fn within(&self, span: &UiSpan) -> Self { - let anchor = self.rel.lerp(span.start.rel, span.end.rel); - let offset = self.px + self.rel.lerp(span.start.px, span.end.px); Self { - rel: anchor, - px: offset, + rel: self.rel.lerp(span.start.rel, span.end.rel), + px: self.px.add(self.rel.lerp(span.start.px, span.end.px)), } } @@ -223,16 +221,18 @@ impl UiScalar { } pub const fn flip(&mut self) { - self.rel = 1.0 - self.rel; - self.px = -self.px; + self.rel = Rel::ONE.sub(self.rel); + self.px = self.px.neg(); } pub const fn to(&self, end: Self) -> UiSpan { UiSpan { start: *self, end } } - pub const fn to_px(&self, rel: f32) -> f32 { - self.rel * rel + self.px + /// Resolved against a box of `len`, which is the only place a fraction + /// becomes a number of pixels. + pub const fn to_px(&self, len: Px) -> Px { + self.px.add(len.mul(self.rel)) } } @@ -427,20 +427,3 @@ impl Display for PixelRegion { write!(f, "{} -> {}", self.top_left, self.bot_right) } } - -pub struct Vec2View<'a> { - pub x: &'a mut f32, - pub y: &'a mut f32, -} - -impl Vec2View<'_> { - pub fn set(&mut self, other: Vec2) { - *self.x = other.x; - *self.y = other.y; - } - - pub fn add(&mut self, other: Vec2) { - *self.x += other.x; - *self.y += other.y; - } -} diff --git a/core/src/render/data.rs b/core/src/render/data.rs index 16a2747..8bd7eb5 100644 --- a/core/src/render/data.rs +++ b/core/src/render/data.rs @@ -16,11 +16,13 @@ pub struct PrimitiveInstance { } impl PrimitiveInstance { + // The region's four scalars, each a `Rel` beside a `Px`: whole counts + // that the shader decodes, rather than the numbers themselves. const ATTRIBS: [VertexAttribute; 6] = vertex_attr_array![ - 0 => Float32x2, - 1 => Float32x2, - 2 => Float32x2, - 3 => Float32x2, + 0 => Sint32x2, + 1 => Sint32x2, + 2 => Sint32x2, + 3 => Sint32x2, 4 => Uint32, 5 => Uint32, ]; diff --git a/core/src/render/shader/prelude.wgsl b/core/src/render/shader/prelude.wgsl index 469e231..269bc2a 100644 --- a/core/src/render/shader/prelude.wgsl +++ b/core/src/render/shader/prelude.wgsl @@ -15,17 +15,54 @@ struct WindowUniform { }; struct Mask { - x: UiSpan, - y: UiSpan, + x: RawSpan, + y: RawSpan, move_idx: u32, } struct MoveOffset { - x: UiSpan, - y: UiSpan, + x: RawSpan, + y: RawSpan, 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; + +// 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 +// belongs to the pixel above it. Flooring the product instead drops a pixel +// wherever a fraction divides a window exactly: a fifth of 1920 comes out of +// `REL_STEP` as 383.99998, and five tabs each lose their last column. +fn snap_floor(v: vec2) -> vec2 { + return floor(v + PX_STEP * 0.5); +} + +struct RawScalar { + rel: i32, + px: i32, +} + +struct RawSpan { + start: RawScalar, + end: RawScalar, +} + +fn scalar_of(raw: RawScalar) -> UiScalar { + return UiScalar(f32(raw.rel) * REL_STEP, f32(raw.px) * PX_STEP); +} + +fn span_of(raw: RawSpan) -> UiSpan { + return UiSpan(scalar_of(raw.start), scalar_of(raw.end)); +} + +fn scalar_of_pair(raw: vec2) -> UiScalar { + return UiScalar(f32(raw.x) * REL_STEP, f32(raw.y) * PX_STEP); +} + struct Region { x: UiSpan, y: UiSpan, @@ -37,9 +74,10 @@ const MOVE_NONE: u32 = 4294967295u; // resolve a deep one the same way. const CHAIN_LIMIT: u32 = 64u; -// Written the way `UiScalar::within` writes it rather than as `mix`, so the -// CPU and the shader compose a position with the same arithmetic and answer -// the same thing about where a widget is. +// The same expression `UiScalar::within` uses, in floats rather than on the +// CPU's grid: a move is resolved here so that scrolling a subtree writes one +// entry instead of walking it. What has to hold is that this agrees with +// itself frame to frame, not that it matches the CPU to the last bit. fn scalar_within(s: UiScalar, p: UiSpan) -> UiScalar { return UiScalar( p.start.rel + (p.end.rel - p.start.rel) * s.rel, @@ -59,7 +97,7 @@ fn resolve_move(idx: u32, local: Region) -> Region { break; } let entry = move_offsets[at]; - r = Region(span_within(r.x, entry.x), span_within(r.y, entry.y)); + r = Region(span_within(r.x, span_of(entry.x)), span_within(r.y, span_of(entry.y))); at = entry.parent; } return r; @@ -76,10 +114,10 @@ struct UiScalar { } struct InstanceInput { - @location(0) x_start: vec2, - @location(1) x_end: vec2, - @location(2) y_start: vec2, - @location(3) y_end: vec2, + @location(0) x_start: vec2, + @location(1) x_end: vec2, + @location(2) y_start: vec2, + @location(3) y_end: vec2, @location(4) mask_idx: u32, @location(5) move_idx: u32, } @@ -102,8 +140,8 @@ fn vs_main( var out: VertexOutput; let local = Region( - UiSpan(UiScalar(in.x_start.x, in.x_start.y), UiScalar(in.x_end.x, in.x_end.y)), - UiSpan(UiScalar(in.y_start.x, in.y_start.y), UiScalar(in.y_end.x, in.y_end.y)), + UiSpan(scalar_of_pair(in.x_start), scalar_of_pair(in.x_end)), + UiSpan(scalar_of_pair(in.y_start), scalar_of_pair(in.y_end)), ); let r = resolve_move(in.move_idx, local); let top_left_rel = vec2(r.x.start.rel, r.y.start.rel); @@ -111,8 +149,8 @@ fn vs_main( let bot_right_rel = vec2(r.x.end.rel, r.y.end.rel); let bot_right_px = vec2(r.x.end.px, r.y.end.px); - let top_left = floor(top_left_rel * window.dim) + floor(top_left_px); - let bot_right = floor(bot_right_rel * window.dim) + floor(bot_right_px); + let top_left = snap_floor(top_left_rel * window.dim) + snap_floor(top_left_px); + let bot_right = snap_floor(bot_right_rel * window.dim) + snap_floor(bot_right_px); let size = bot_right - top_left; let uv = vec2( @@ -137,14 +175,14 @@ fn masked(in: VertexOutput, color: vec4) -> vec4 { let mask = masks[in.mask_idx]; // Its own chain, not the drawn primitive's, so a stationary viewport // clips content that moves inside it. - let m = resolve_move(mask.move_idx, Region(mask.x, mask.y)); + let m = resolve_move(mask.move_idx, Region(span_of(mask.x), span_of(mask.y))); let tl = vec2(m.x.start.rel, m.y.start.rel); let tl_px = vec2(m.x.start.px, m.y.start.px); let br = vec2(m.x.end.rel, m.y.end.rel); let br_px = vec2(m.x.end.px, m.y.end.px); - let top_left = floor(tl * window.dim) + floor(tl_px); - let bot_right = floor(br * window.dim) + floor(br_px); + let top_left = snap_floor(tl * window.dim) + snap_floor(tl_px); + let bot_right = snap_floor(br * window.dim) + snap_floor(br_px); let pos = in.clip_position.xy; if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y { return color * 0.0; diff --git a/core/src/ui/holds.rs b/core/src/ui/holds.rs index 61e8e6d..ff0ec0b 100644 --- a/core/src/ui/holds.rs +++ b/core/src/ui/holds.rs @@ -1,4 +1,4 @@ -use crate::UiScalar; +use crate::{Rel, UiScalar}; use std::ops::RangeInclusive; /// The lengths of a box, in pixels, that one drawing of a widget holds for: @@ -59,11 +59,12 @@ impl Holds { /// 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 == 0.0 { + if len.rel == Rel::ZERO { return Self::ANY; } - let a = (self.lo - len.px) / len.rel; - let b = (self.hi - len.px) / len.rel; + let (rel, px) = (len.rel.to_f32(), len.px.to_f32()); + let a = (self.lo - px) / rel; + let b = (self.hi - px) / rel; Self { lo: a.min(b), hi: a.max(b), diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index f37a673..fa7428b 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, RegionAlign, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer, + Axis, Holds, Len, Px, RegionAlign, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, WidgetId, Widgets, render::{ GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst, @@ -293,8 +293,8 @@ impl<'a> Painter<'a> { fn px_within_offer(&self, local: UiRegion) -> Vec2 { let size = local.size(); Vec2::new( - size.x.to_px(self.offered_px.x), - size.y.to_px(self.offered_px.y), + 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(), ) } diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index 69540c5..7432951 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -3,7 +3,7 @@ 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, Size, StrongWidget, UiRegion, UiRsc, UiScalar, UiSpan, WidgetId, + PixelRegion, RegionAlign, Rel, Size, StrongWidget, UiRegion, UiRsc, UiScalar, UiSpan, WidgetId, Widgets, util::{HashMap, Vec2}, }; @@ -569,6 +569,16 @@ impl UiRenderState { if has_region_node != info.region_node { return None; } + // Drawn on another layer: the drawing sits in that layer's list and + // paints at its moment, which no amount of geometry says. A container + // that measures a child by drawing it and then draws it again where + // it belongs -- `Stack`, over its background -- asks the second time + // on a layer the first answer is not good for. + if active.layer != info.layer { + #[cfg(feature = "layout-diagnostics")] + diag::reuse(id, ReuseOutcome::WrongLayer); + return None; + } // Drawn somewhere else in the tree: its box is in coordinates it no // longer sits in, and its slot names the wrong parent. if active.parent_move != info.parent_move { @@ -1034,14 +1044,14 @@ impl RegionRemap { fn apply_scalar(self, scalar: UiScalar, from: UiSpan, to: UiSpan) -> UiScalar { let extent = from.end.rel - from.start.rel; - if extent == 0.0 { + if extent == Rel::ZERO { return scalar + to.start - from.start; } let fraction = (scalar.rel - from.start.rel) / extent; - let from_px = from.start.px + fraction * (from.end.px - from.start.px); - let to_rel = to.start.rel + fraction * (to.end.rel - to.start.rel); - let to_px = to.start.px + fraction * (to.end.px - to.start.px); - UiScalar::new(to_rel, scalar.px - from_px + to_px) + let from_px = fraction.lerp(from.start.px, from.end.px); + let to_rel = fraction.lerp(to.start.rel, to.end.rel); + let to_px = fraction.lerp(to.start.px, to.end.px); + UiScalar::from_parts(to_rel, scalar.px - from_px + to_px) } } diff --git a/core/src/util/math.rs b/core/src/util/math.rs index f62ec7a..95e7eef 100644 --- a/core/src/util/math.rs +++ b/core/src/util/math.rs @@ -56,6 +56,34 @@ macro_rules! impl_op { } } }; + // Without the `f32` operations, for a type whose fields are not all the + // same kind of number: there is nothing a bare float means to a fraction + // and an offset at once. + (same $T:ident $op:ident $fn:ident $opa:ident $fna:ident; $($field:ident)*) => { + #[allow(non_snake_case)] + mod ${concat($T, _op_, $fn, _same_impl)} { + use super::*; + #[allow(unused_imports)] + use std::ops::*; + const impl $op for $T { + type Output = Self; + + fn $fn(self, rhs: Self) -> Self::Output { + Self { + $($field: self.$field.$fn(rhs.$field),)* + } + } + } + const impl $opa for $T { + fn $fna(&mut self, rhs: Self) { + *self = self.$fn(rhs); + } + } + } + }; + (same $T:ident $op:ident $fn:ident; $($field:ident)*) => { + impl_op!(same $T $op $fn ${concat($op,Assign)} ${concat($fn,_assign)}; $($field)*); + }; ($T:ident $op:ident $fn:ident; $($field:ident)*) => { impl_op!($T $op $fn ${concat($op,Assign)} ${concat($fn,_assign)}; $($field)*); }; diff --git a/src/random.rs b/src/random.rs index eb131f6..e12a5b5 100644 --- a/src/random.rs +++ b/src/random.rs @@ -113,7 +113,10 @@ impl Widget for Branch { let mut top = UiRegion::FULL; top.y.end = top.y.start.offset(40.0); let measured = painter.widget_within(&self.probe, top).len(Axis::X); - let px = measured.apply_leftover().to_px(painter.px_len(Axis::X)); + let px = measured + .apply_leftover() + .to_px(Px::from_f32(painter.px_len(Axis::X))) + .to_f32(); let mut below = UiRegion::FULL; below.y.start = below.y.start.offset(40.0); diff --git a/src/widget/position/pad.rs b/src/widget/position/pad.rs index d16beb5..36eb3f6 100644 --- a/src/widget/position/pad.rs +++ b/src/widget/position/pad.rs @@ -49,10 +49,10 @@ impl Padding { } pub fn region(&self) -> UiRegion { let mut region = UiRegion::FULL; - region.x.start.px += self.left; - region.y.start.px += self.top; - region.x.end.px -= self.right; - region.y.end.px -= self.bottom; + region.x.start.px += Px::from_f32(self.left); + region.y.start.px += Px::from_f32(self.top); + region.x.end.px -= Px::from_f32(self.right); + region.y.end.px -= Px::from_f32(self.bottom); region } pub fn x(amt: impl UiNum) -> Self { diff --git a/src/widget/position/scroll.rs b/src/widget/position/scroll.rs index 91be954..dd57e0e 100644 --- a/src/widget/position/scroll.rs +++ b/src/widget/position/scroll.rs @@ -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(container_len); + self.content_len = content.to_px(Px::from_f32(container_len)).to_f32(); if self.snap_end { self.amt = self.content_len - self.container_len; @@ -33,9 +33,10 @@ impl Widget for Scroll { // the drawing holds for that length alone. One scrolled part way sits // where it is until the box shrinks past what is left of it. Kept to // the end, it moves with every length. - if content.rel == 0.0 && self.content_len <= self.container_len && align == AxisAlign::NEG { + 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); - } else if content.rel == 0.0 && !self.snap_end { + } else if fixed_len && !self.snap_end { let left = self.content_len - self.amt; painter.holds(self.axis, f32::NEG_INFINITY..=left); } diff --git a/src/widget/position/span.rs b/src/widget/position/span.rs index 7d61233..b5ffddc 100644 --- a/src/widget/position/span.rs +++ b/src/widget/position/span.rs @@ -35,8 +35,9 @@ impl Widget for Span { Some(len) => len, None => painter.widget_within(child, region).len(axis), }; - cursor.px += len.px + self.gap; - cursor.rel += len.rel; + // Onto the grid at the seam: `Len` is still in floats. + cursor.px += Px::from_f32(len.px + self.gap); + cursor.rel += Rel::from_f32(len.rel); lens.push(len); } @@ -90,7 +91,7 @@ impl Widget for Span { // pixels or a fraction keeps those and overflows. if len.leftover > 0.0 && len.px == 0.0 && len.rel == 0.0 && !shares { painter.undraw(child); - start.px += self.gap; + start.px += Px::from_f32(self.gap); continue; } let mut span = UiSpan::FULL; @@ -101,8 +102,8 @@ impl Widget for Span { let end = (UiScalar::rel_max() + start) - offset; start = rel_end.within(&start.to(end)); } - start.px += len.px; - start.rel += len.rel; + start.px += Px::from_f32(len.px); + start.rel += Rel::from_f32(len.rel); span.end = start; let mut region = UiRegion::from_axis(axis, span, UiSpan::FULL); if self.dir.sign == Sign::Neg { @@ -121,7 +122,7 @@ impl Widget for Span { ortho.px = ortho.px.max(used.px); } } - start.px += self.gap; + start.px += Px::from_f32(self.gap); } // Carried whole rather than collapsed to one share: a span that sizes diff --git a/tests/chain_cost.rs b/tests/chain_cost.rs index 8b2ec45..038ff7d 100644 --- a/tests/chain_cost.rs +++ b/tests/chain_cost.rs @@ -79,7 +79,7 @@ fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize) { slot = render.moves.push(slot, UiRegion::FULL); } - let px = |v: f32| UiScalar { rel: 0.0, px: v }; + let px = |v: f32| UiScalar::px(v); for i in 0..INSTANCES { let x = (i % (SIZE as usize / 2)) as f32 * 2.0; let y = (i / (SIZE as usize / 2)) as f32; diff --git a/tests/determinism.rs b/tests/determinism.rs index 3700035..b18e96c 100644 --- a/tests/determinism.rs +++ b/tests/determinism.rs @@ -24,7 +24,10 @@ impl Widget for BranchesOnMeasurement { let mut top = UiRegion::FULL; top.y.end = top.y.start.offset(40.0); let measured = painter.widget_within(&self.probe, top).len(Axis::X); - let px = measured.apply_leftover().to_px(painter.px_len(Axis::X)); + let px = measured + .apply_leftover() + .to_px(Px::from_f32(painter.px_len(Axis::X))) + .to_f32(); let mut below = UiRegion::FULL; below.y.start = below.y.start.offset(40.0); diff --git a/tests/layout.rs b/tests/layout.rs index 3e3ca21..2640625 100644 --- a/tests/layout.rs +++ b/tests/layout.rs @@ -295,12 +295,15 @@ fn an_uneven_nesting_still_gives_every_share_the_same_length() { } /// Where the shader puts an edge: the two parts of a scalar are floored -/// apart, so a fraction and a pixel offset snap independently. +/// apart, so a fraction and a pixel offset snap independently, and each is +/// taken to the boundary it composes to within half a step of. Kept in step +/// with `snap_floor` in `prelude.wgsl`. fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) { let active = &h.render.active[&id]; let region = h.render.moves.resolve(active.parent_move, active.region); let dim = h.size().axis(axis); - let edge = |s: UiScalar| (s.rel * dim).floor() + s.px.floor(); + let snap = |v: f32| (v + Px::STEP.to_f32() * 0.5).floor(); + let edge = |s: UiScalar| snap(s.rel.to_f32() * dim) + snap(s.px.to_f32()); let span = region.axis(axis); (edge(span.start), edge(span.end)) } diff --git a/tests/retained.rs b/tests/retained.rs index 0494e54..42eabb1 100644 --- a/tests/retained.rs +++ b/tests/retained.rs @@ -550,3 +550,36 @@ fn a_declared_length_child_is_not_redrawn_when_the_box_around_it_grows() { assert_eq!(draws.get(), settled, "its own length did not change"); assert_corners!(h, fixed, (200, 0), (280, 200)); } + +/// `Stack` measures the child that sizes it by drawing it, then draws it +/// again above the background it stacks over. The second ask is for the same +/// box, so nothing geometric says the answer has gone stale, and reusing it +/// leaves the drawing under the background. The same tree got away with it +/// while the two asks differed by a rounding. +#[test] +fn a_widget_asked_again_on_another_layer_is_drawn_there() { + let mut h = Harness::new((400, 200)); + let background = rect(Color::RED).add(&mut h.rsc); + let (front, draws) = counted(&mut h, Size::from((100, 50)), false); + let stack = Stack { + children: vec![ + background.add_strong(&mut h.rsc), + front.add_strong(&mut h.rsc), + ], + size: StackSize::Child(1), + } + .add(&mut h.rsc); + h.set_root(stack); + h.frame(); + + let layer = |id| h.render.active[&id].layer; + assert_ne!( + layer(front.id()), + layer(stack.id()), + "the measured drawing was left on the stack's own layer" + ); + assert_ne!(layer(front.id()), layer(background.id())); + // Measured once for the size and once where it goes, which is what the + // stack costs and not something this test is asserting a number for. + assert_eq!(draws.get(), 2); +}